### 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):
"""
A remote service transported over a standard Unix command.
- This is left rather generic. We need to know some command lists SET and
- CLEAR containing the relevant service names and arguments. These are
- simply executed, after simple placeholder substitution.
-
- The SET command should read a password as its first line on stdin, and set
- that as the user's new password. The CLEAR command should simply prevent
- the user from logging in with a password. On success, the commands should
- print a line `OK' to standard output, and on any kind of anticipated
- failure, they should print `ERR' followed by an HTTP status code and a
- message; in either case, the program should exit with status zero. In
- disastrous cases, it's acceptable to print an error message to stderr
- and/or exit with a nonzero status.
-
- The placeholders are as follows.
+ This is left rather generic. Two strategies are available (and can be
+ combined using appropriate configuration). A DEFAULT command list can be
+ specified, and will be invoked as `DEFAULT OP ARGS...', where OP ARGS form
+ a Chopwood remote command. Additionally, an OPMAP dictionary can be
+ provided, mapping OP names (remote command names) to command lists
+ containing `%' placeholders, as follows:
`%u' the user's name
`%%' a single `%' character
+
+ On success, the commands should print a line `OK' to standard output, and
+ on any kind of anticipated failure, they should print `ERR' followed by an
+ HTTP status code and a message; in either case, the program should exit
+ with status zero. In disastrous cases, it's acceptable to print an error
+ message to stderr and/or exit with a nonzero status.
+
+ Configuration hint: if you can only handle some subset of the available
+ commands, then your easy approach is to set commands for the operations you
+ can handle in the OPMAP, and set the DEFAULT to something like
+
+ ['echo', 'ERR 500', 'unsupported command:']
+
+ to reject other commands.
"""
R_PAT = RX.compile('%(.)')
- def __init__(me, set, clear, *args, **kw):
+ def __init__(me, default = ['ERR', '500', 'unimplemented command:'],
+ opmap = {}, *args, **kw):
"""Initialize the command remote service."""
super(CommandRemoteService, me).__init__(*args, **kw)
- me._set = set
- me._clear = clear
+ me._default = default
+ me._opmap = opmap
def _describe(me):
"""Description of the remote service."""
def _mkcmd(me, cmd, map):
"""Construct the command to be executed, by substituting placeholders."""
+ if map is None: return cmd
return [me.R_PAT.sub(lambda m: me._subst(m.group(1), map), arg)
for arg in cmd]
+ def _dispatch(me, func, op, args, input = None):
+ """
+ Work out how to invoke a particular command.
+
+ Invoke FUNC, which works like `_run', with appropriate arguments. The OP
+ is a remote command name; ARGS is a sequence of (C, ARG) pairs, where C
+ is a placeholder character and ARG is a string value; INPUT is the text
+ to provide to the command on standard input.
+ """
+ try:
+ cmd = me._opmap[op]
+ except KeyError:
+ cmd = me._default + [op] + [v for k, v in args]
+ map = None
+ else:
+ map = dict(args)
+ return func(cmd, input = input, state = map)
+
def setpasswd(me, user, passwd):
"""Service protocol: set the USER's password to PASSWD."""
- me._run_noout(me._set, passwd + '\n', state = dict(u = user))
+ me._dispatch(me._run_noout, 'set', [('u', user)], passwd + '\n')
def clearpasswd(me, user):
"""Service protocol: clear the USER's password."""
- me._run_noout(me._clear, state = dict(u = user))
+ 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')