X-Git-Url: https://git.distorted.org.uk/~mdw/chopwood/blobdiff_plain/46eb5a384c3e7882e9d5339021c998c2f7d4d9b2..0c58273e69f08d17e69d9af6b04734bdce302532:/service.py diff --git a/service.py b/service.py index aa77388..b1f170c 100644 --- a/service.py +++ b/service.py @@ -40,11 +40,10 @@ import util as U ### 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 @@ -76,12 +75,18 @@ class IncorrectPassword (Exception): 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 ###-------------------------------------------------------------------------- @@ -128,6 +133,10 @@ class Account (object): 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 @@ -162,6 +171,15 @@ class LocalService (BasicService): """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') ###-------------------------------------------------------------------------- @@ -266,6 +284,14 @@ class SSHRemoteService (BasicRemoteService): `clear SERVICE USER' Clear the USER's password for SERVICE. + `mkpwent USER SERVICE [FIELDS ...]' + Install a record for USER in the SERVICE, supplying any other + necessary FIELDS in the appropriate format. The user's password is + provided on the next line of standard input. + + `rmpwent USER SERVICE' + Remove USER's password record for SERVICE. + Arguments are form-url-encoded, since SSH doesn't preserve token boundaries in its argument list. @@ -300,6 +326,14 @@ class SSHRemoteService (BasicRemoteService): """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): @@ -314,8 +348,14 @@ class CommandRemoteService (BasicRemoteService): containing `%' placeholders, as follows: `%u' the user's name + `%f' a user record field (list-valued) `%%' a single `%' character + If a template word contains placeholders for list-valued arguments, then + one output word is produced for each element of each list, with the + rightmost placeholder varying fastest. If any list is empty then no output + words are produced. + 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 @@ -326,14 +366,15 @@ class CommandRemoteService (BasicRemoteService): 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:'] + ['echo', 'ERR', '500', 'unsupported command:'] to reject other commands. """ R_PAT = RX.compile('%(.)') - def __init__(me, default = ['ERR', '500', 'unimplemented command:'], + def __init__(me, + default = ['echo', 'ERR', '500', 'unimplemented command:'], opmap = {}, *args, **kw): """Initialize the command remote service.""" super(CommandRemoteService, me).__init__(*args, **kw) @@ -344,15 +385,60 @@ class CommandRemoteService (BasicRemoteService): """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 map.get(c, c) + def _mkcmd(me, cmd, argmap): + """ + Construct the command to be executed, by substituting placeholders. - 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] + The ARGMAP is a dictionary mapping placeholder letters to lists of + arguments. These are substituted cartesian-product style into the + command words. + """ + + ## No command map, so assume someone's already done the hard word. + if argmap is None: return cmd + + ## Start on building a list of arguments. + ww = [] + + ## Work through each template argument in turn... + for w in cmd: + + ## Firstly, build a list of lists. We'll then take the cartesian + ## product of these, and concatenate each of the results. + pc = [] + last = 0 + for m in me.R_PAT.finditer(w): + start, end = m.start(0), m.end(0) + if start > last: pc.append([w[last:start]]) + ch = m.group(1) + if ch == '%': + pc.append(['%']) + else: + try: pc.append(argmap[m.group(1)]) + except KeyError: raise U.ExpectedError, ( + 500, "Unknown placeholder `%%%s' in command `%s'" % (ch, cmd)) + last = end + if last < len(w): pc.append([w[last:]]) + + ## If any of the components is empty then there's nothing to do for + ## this word. + if not all(pc): continue + + ## Now do all the substitutions. + ii = len(pc)*[0] + while True: + ww.append(''.join(map(lambda v, i: v[i], pc, ii))) + i = len(ii) - 1 + while i >= 0: + ii[i] += 1 + if ii[i] < len(pc[i]): break + ii[i] = 0 + i -= 1 + else: + break + + ## And finally we're done. + return ww def _dispatch(me, func, op, args, input = None): """ @@ -360,13 +446,14 @@ class CommandRemoteService (BasicRemoteService): 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. + is a placeholder character and ARG is a list of string values; 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] + cmd = me._default + [op] + reduce(lambda x, y: x + y, + [v for k, v in args], []) map = None else: map = dict(args) @@ -374,11 +461,21 @@ class CommandRemoteService (BasicRemoteService): def setpasswd(me, user, passwd): """Service protocol: set the USER's password to PASSWD.""" - me._dispatch(me._run_noout, 'set', [('u', user)], passwd + '\n') + me._dispatch(me._run_noout, 'set', [('u', [user])], + input = passwd + '\n') def clearpasswd(me, user): """Service protocol: clear the USER's password.""" - me._dispatch(me._run_noout, 'clear', [('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]), ('f', fields)], + input = passwd + '\n') + + def rmpwent(me, user): + """Service protocol: delete the record for USER.""" + me._dispatch(me._run_noout, 'rmpwent', [('u', [user])]) CONF.export('CommandRemoteService')