### 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')
###--------------------------------------------------------------------------
configuration.
"""
- def _run(me, cmd, input = None):
+ def _run(me, cmd, input = None, state = None):
"""
This is the core of the remote service machinery. It issues a command
and parses the response. It will generate strings of informational
output from the command; error responses cause appropriate exceptions to
be raised.
- The command is determined by passing the CMD argument to the `_mkcmd'
- method, which a subclass must implement; it should return a list of
- command-line arguments suitable for `subprocess.Popen'. The INPUT is a
- string to make available on the command's stdin; if None, then no input
+ The command is determined by passing the CMD and STATE arguments to the
+ `_mkcmd' method, which a subclass must implement; it should return a list
+ of command-line arguments suitable for `subprocess.Popen'. The INPUT is
+ a string to make available on the command's stdin; if None, then no input
is provided to the command. The `_describe' method must provide a
description of the remote service for use in timeout messages.
## Run the command and collect its output and status.
with U.timeout(30, "waiting for remote service %s" % me._describe()):
- proc = SUB.Popen(me._mkcmd(cmd),
+ proc = SUB.Popen(me._mkcmd(cmd, state),
stdin = input is not None and SUB.PIPE or None,
stdout = SUB.PIPE, stderr = SUB.PIPE)
out, err = proc.communicate(input)
if not win:
raise U.ExpectedError, (500, 'No reply from remote service')
- def _run_noout(me, cmd, input = None):
+ def _run_noout(me, cmd, input = None, state = None):
"""Like `_run', but expect no output."""
- for _ in me._run(cmd, input):
+ for _ in me._run(cmd, input, state):
raise U.ExpectedError, (500, 'Unexpected output from remote service')
class SSHRemoteService (BasicRemoteService):
(probably of the form `LOGIN@HOSTNAME') and referring to the service
NAME.
"""
- super(SSHRemoteService, me).__init__(*args, **kw)
+ super(SSHRemoteService, me).__init__(name = name, *args, **kw)
me._remote = remote
- me._name = name
def _describe(me):
"""Description of the remote service."""
- return "`%s' via SSH to `%s'" % (me._name, me._remote),
+ return "`%s' via SSH to `%s'" % (me.name, me._remote),
- def _mkcmd(me, cmd):
+ def _mkcmd(me, cmd, state):
"""Format a command for SSH. Mainly escaping arguments."""
return ['ssh', me._remote, ' '.join(map(CGI.urlencode, cmd))]
def setpasswd(me, user, passwd):
"""Service protocol: set the USER's password to PASSWD."""
- me._run_noout(['set', me._name, user], passwd + '\n')
+ me._run_noout(['set', me.name, user], passwd + '\n')
def clearpasswd(me, user):
"""Service protocol: clear the USER's password."""
- me._run_noout(['clear', me._name, user])
+ 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')
"""
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._map = dict(u = user)
+ me._default = default
+ me._opmap = opmap
- def _subst(me, c):
+ def _describe(me):
+ """Description of the remote service."""
+ return "`%s' command service (%s)" % (me.name, ' '.join(me._default))
+
+ def _subst(me, c, map):
"""Return the substitution for the placeholder `%C'."""
- return me._map.get(c, c)
+ return map.get(c, c)
- def _mkcmd(me, cmd):
+ def _mkcmd(me, cmd, map):
"""Construct the command to be executed, by substituting placeholders."""
- return [me.R_PAT.sub(lambda m: me._subst(m.group(1))) for arg in cmd]
+ 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')
+ 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)
+ 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')