from __future__ import with_statement
+import itertools as I
import os as OS; ENV = OS.environ
import config as CONF; CFG = CONF.CFG
## A directory in which we can create lockfiles.
LOCKDIR = OS.path.join(ENV['HOME'], 'var', 'lock', 'chpwd'))
+###--------------------------------------------------------------------------
+### Utilities.
+
+def fill_in_fields(fno_user, fno_passwd, fno_map, user, passwd, args):
+ """
+ Return a vector of filled-in fields.
+
+ The FNO_... arguments give field numbers: FNO_USER and FNO_PASSWD give the
+ positions for the username and password fields, respectively; and FNO_MAP
+ is a sequence of (NAME, POS) pairs. The USER and PASSWD arguments give the
+ actual user name and password values; ARGS are the remaining arguments,
+ maybe in the form `NAME=VALUE'.
+ """
+
+ ## Prepare the result vector, and set up some data structures.
+ n = 2 + len(fno_map)
+ fmap = {}
+ rmap = map(int, xrange(n))
+ ok = True
+ if fno_user >= n or fno_passwd >= n: ok = False
+ for k, i in fno_map:
+ fmap[k] = i
+ rmap[i] = "`%s'" % k
+ if i >= n: ok = False
+ if not ok:
+ raise U.ExpectedError, \
+ (500, "Fields specified aren't contiguous")
+
+ ## Prepare the new record's fields.
+ f = [None]*n
+ f[fno_user] = user
+ f[fno_passwd] = passwd
+
+ for a in args:
+ if '=' in a:
+ k, v = a.split('=', 1)
+ try: i = fmap[k]
+ except KeyError: raise U.ExpectedError, (400, "Unknown field `%s'" % k)
+ else:
+ for i in xrange(n):
+ if f[i] is None: break
+ else:
+ raise U.ExpectedError, (500, "All fields already populated")
+ v = a
+ if f[i] is not None:
+ raise U.ExpectedError, (400, "Field %s is already set" % rmap[i])
+ f[i] = v
+
+ ## Check that the vector of fields is properly set up.
+ for i in xrange(n):
+ if f[i] is None:
+ raise U.ExpectedError, (500, "Field %s is unset" % rmap[i])
+
+ ## Done.
+ return f
+
###--------------------------------------------------------------------------
### Protocol.
###
me._be = backend
def write(me):
me._be._update(me)
+ def remove(me):
+ me._be._remove(me)
class TrivialRecord (BasicRecord):
"""
return rec
raise UnknownUser, user
+ def create(me, user, passwd, args):
+ """
+ Create a new record for the USER.
+
+ The new record has the given PASSWD, and other fields are set from ARGS.
+ Those ARGS of the form `KEY=VALUE' set the appropriately named fields (as
+ set up by the constructor); other ARGS fill in unset fields, left to
+ right.
+ """
+
+ f = fill_in_fields(me._fmap['user'], me._fmap['passwd'],
+ [(k[2:], i)
+ for k, i in me._fmap.iteritems()
+ if k.startswith('f_')],
+ user, passwd, args)
+ r = FlatFileRecord(':'.join(f), me._delim, me._fmap, backend = me)
+ me._rewrite('create', r)
+
def _rewrite(me, op, rec):
"""
Rewrite the file, according to OP.
"""Update the record REC in the file."""
me._rewrite('update', rec)
+ def _remove(me, rec):
+ """Update the record REC in the file."""
+ me._rewrite('remove', rec)
+
CONF.export('FlatFileBackend')
###--------------------------------------------------------------------------
setattr(rec, 'f_' + f, v)
return rec
+ def create(me, user, passwd, args):
+ """
+ Create a new record for the named USER.
+
+ The new record has the given PASSWD, and other fields are set from ARGS.
+ Those ARGS of the form `KEY=VALUE' set the appropriately named fields (as
+ set up by the constructor); other ARGS fill in unset fields, left to
+ right, in the order given to the constructor.
+ """
+
+ tags = ['user', 'passwd'] + \
+ ['t_%d' % 0 for i in xrange(len(me._fields))]
+ f = fill_in_fields(0, 1, list(I.izip(me._fields, I.count(2))),
+ user, passwd, args)
+ me._connect()
+ with me._db:
+ me._db.execute("INSERT INTO %s (%s) VALUES (%s)" %
+ (me._table,
+ ', '.join([me._user, me._passwd] + me._fields),
+ ', '.join(['$%s' % t for t in tags])),
+ **dict(I.izip(tags, f)))
+
+ def _remove(me, rec):
+ """Remove the record REC from the database."""
+ me._connect()
+ with me._db:
+ me._db.execute("DELETE FROM %s WHERE %s = $user" %
+ (me._table, me._user),
+ user = rec.user)
+
def _update(me, rec):
"""Update the record REC in the database."""
me._connect()
from __future__ import with_statement
import agpl as AGPL
+import backend as BE
import cmdutil as CU
+import config as CONF; CFG = CONF.CFG
import dbmaint as D
+import operation as OP
from output import OUT, PRINT
+import service as S
import subcommand as SC
import util as U
def cmd_deluser(user, email = None):
with D.DB:
CU.check_user(user)
+ for service, alias in D.DB.execute(
+ "SELECT service, alias FROM services WHERE user = $user",
+ user = user):
+ if service == 'master': continue
+ try:
+ svc = S.SERVICES[service]
+ except KeyError:
+ OUT.warn("User `%s' has account for unknown service `%s'" %
+ (user, service))
+ else:
+ if svc.manage_pwent_p:
+ if alias is None: alias = user
+ svc.rmpwent(alias)
D.DB.execute("DELETE FROM users WHERE user = $user", user = user)
@SC.subcommand(
opts = [SC.Opt('alias', '-a', '--alias',
"alias by which USER is known to SERVICE",
argname = 'ALIAS')],
- params = [SC.Arg('user'), SC.Arg('service')])
-def cmd_addacct(user, service, alias = None):
+ params = [SC.Arg('user'), SC.Arg('service')],
+ rparam = SC.Arg('fields'))
+def cmd_addacct(user, service, fields, alias = None):
with D.DB:
CU.check_user(user)
- CU.check_service(service)
+ svc = CU.check_service(service)
D.DB.execute("""SELECT 1 FROM services
WHERE user = $user AND service = $service""",
user = user, service = service)
VALUES ($service, $user, $alias)""",
service = service, user = user, alias = alias)
+ if svc.manage_pwent_p:
+ if alias is None: alias = user
+ passwd = CFG.RQCLASS.reset([OP.acct(svc, alias)]).pwgen()
+ svc.mkpwent(alias, passwd, fields)
+ elif fields:
+ raise U.ExpectedError, (
+ 400, "Password entry fields supplied, "
+ "but `%s' entries must be created manually" % service)
+
@SC.subcommand(
'delacct', ['admin'], "Remove USER's SERVICE account.",
params = [SC.Arg('user'), SC.Arg('service')])
def cmd_delacct(user, service):
with D.DB:
- CU.resolve_account(service, user)
+ svc, alias = CU.resolve_account(service, user)
if service == 'master':
raise U.ExpectedError, \
(400, "Can't delete master accounts: use `deluser'")
D.DB.execute("""DELETE FROM services
WHERE service = $service AND user = $user""",
service = service, user = user)
+ if svc.manage_pwent_p:
+ svc.rmpwent(alias)
+ else:
+ OUT.warn("You must remove the `%s' password entry for `%s' by hand" %
+ (service, user))
@SC.subcommand(
'editacct', ['admin'], "Modify USER's SERVICE account record.",
svc = CU.check_service(service)
svc.clearpasswd(user)
+@SC.subcommand(
+ 'mkpwent', ['remote'], 'Create a new user record',
+ params = [SC.Arg('user'), SC.Arg('service')],
+ rparam = SC.Arg('fields'))
+def cmd_mkpwent_svc(user, service, fields):
+ passwd = U.readline('new password')
+ svc = CU.check_service(service)
+ svc.mkpwent(user, passwd, fields)
+
+@SC.subcommand(
+ 'rmpwent', ['remote'], 'Remove an existing user record',
+ params = [SC.Arg('user'), SC.Arg('service')])
+def cmd_rmpwent_svc(user, service):
+ svc = CU.check_service(service)
+ svc.rmpwent(user)
+
###----- That's all, folks --------------------------------------------------
### Protocol.
###
### A service is a thing for which a user might have an account, with a login
-### name and password. The service protocol is fairly straightforward: a
-### password can be set to a particular value using `setpasswd' (which
-### handles details of hashing and so on), or cleared (i.e., preventing
-### logins using a password) using `clearpasswd'. Services also present
-### `friendly' names, used by the user interface.
+### name and password. The service protocol is fairly straightforward: there
+### are methods corresponding to the various low-level operations which can
+### be performed on services. Services also present `friendly' names, used
+### by the user interface.
###
### A service may be local or remote. Local services are implemented in
### terms of a backend and hashing scheme. Information about a particular
class BasicService (object):
"""
A simple base class for services.
+
+ The `manage_pwent_p' flag indicates whether administration commands should
+ attempt to add or remove password entries in the corresponding database
+ when users are added or removed.
"""
- def __init__(me, friendly, name = None, *args, **kw):
+ def __init__(me, friendly, name = None, manage_pwent_p = True,
+ *args, **kw):
super(BasicService, me).__init__(*args)
me.name = name
me.friendly = friendly
+ me.manage_pwent_p = manage_pwent_p
me.meta = kw
###--------------------------------------------------------------------------
me._rec.passwd = passwd
me._rec.write()
+ def remove(me):
+ """Service protocol: remove the user's password entry."""
+ me._rec.remove()
+
class LocalService (BasicService):
"""
A local service has immediate knowledge of a hashing scheme and a password
"""Service protocol: clear USER's password, preventing logins."""
me.find(user).clearpasswd()
+ def mkpwent(me, user, passwd, fields):
+ """Service protocol: create a record for USER."""
+ if me.hash.NULL is not None: passwd = me.hash.NULL
+ me._be.create(user, passwd, fields)
+
+ def rmpwent(me, user):
+ """Service protocol: delete the record for USER."""
+ me.find(user).remove()
+
CONF.export('LocalService')
###--------------------------------------------------------------------------
"""Service protocol: clear the USER's password."""
me._run_noout(['clear', me.name, user])
+ def mkpwent(me, user, passwd, fields):
+ """Service protocol: create a record for USER."""
+ me._run_noout(['mkpwent', user, me.name] + fields, passwd + '\n')
+
+ def rmpwent(me, user):
+ """Service protocol: delete the record for USER."""
+ me._run_noout(['rmpwent', user, me.name])
+
CONF.export('SSHRemoteService')
class CommandRemoteService (BasicRemoteService):
"""Service protocol: clear the USER's password."""
me._dispatch(me._run_noout, 'clear', [('u', user)])
+ def mkpwent(me, user, passwd, fields):
+ """Service protocol: create a record for USER."""
+ me._dispatch(me._run_noout, 'mkpwent', [('u', user)])
+
+ def rmpwent(me, user):
+ """Service protocol: delete the record for USER."""
+ me._dispatch(me._run_noout, 'rmpwent', [('u', user)])
+
CONF.export('CommandRemoteService')
###--------------------------------------------------------------------------