wrapper.fhtml: Add `license' relationship to the AGPL link.
[chopwood] / chpwd
diff --git a/chpwd b/chpwd
index 1e4248b..2d6c75c 100755 (executable)
--- a/chpwd
+++ b/chpwd
@@ -30,6 +30,7 @@ import optparse as OP
 import os as OS; ENV = OS.environ
 import shlex as SL
 import sys as SYS
+import syslog as L
 
 from auto import HOME, VERSION
 import cgi as CGI
@@ -38,6 +39,7 @@ import config as CONF; CFG = CONF.CFG
 import dbmaint as D
 import httpauth as HA
 import output as O; OUT = O.OUT
+import service as S
 import subcommand as SC
 import util as U
 
@@ -71,8 +73,12 @@ for short, long, props in [
     'help': 'run commands with the given CONTEXT' }),
   ('-f', '--config-file', {
     'metavar': 'FILE', 'dest': 'config',
-    'default': OS.path.join(HOME, 'chpwd.conf'),
+    'default': ENV.get('CHPWD_CONFIG',
+                       OS.path.join(HOME, 'chpwd.conf')),
     'help': 'read configuration from FILE.' }),
+  ('-i', '--ignore-policy', {
+    'dest': 'ignpol', 'default': False, 'action': 'store_true',
+    'help': 'ignore the operation policy (for administrators)' }),
   ('-s', '--ssl', {
     'dest': 'sslp', 'action': 'store_true',
     'help': 'pretend CGI connection is carried over SSL/TLS' }),
@@ -81,21 +87,35 @@ for short, long, props in [
     'help': "impersonate USER, and default context to `userv'." })]:
   OPTPARSE.add_option(short, long, **props)
 
+def parse_options():
+  """
+  Parse the main command-line options, returning the positional arguments.
+  """
+  global OPTS
+  OPTS, args = OPTPARSE.parse_args()
+  OPTPARSE.show_global_opts = False
+  CFG.OPTS = OPTS
+  ## It's tempting to load the configuration here.  Don't do that.  Some
+  ## contexts will want to check that the command line was handled properly
+  ## upstream before believing it for anything, such as executing arbitrary
+  ## Python code.
+  return args
+
 ###--------------------------------------------------------------------------
 ### CGI dispatch.
 
 ## The special variables, to be picked out by `cgiparse'.
 CGI.SPECIAL['%act'] = None
 CGI.SPECIAL['%nonce'] = None
+CGI.SPECIAL['%user'] = None
 
 ## We don't want to parse arguments until we've settled on a context; but
 ## issuing redirects in the early setup phase fails because we don't know
 ## the script name.  So package the setup here.
 def cgi_setup(ctx = 'cgi-noauth'):
-  global OPTS
   if OPTS: return
   OPTPARSE.context = ctx
-  OPTS, args = OPTPARSE.parse_args()
+  args = parse_options()
   if args: raise U.ExpectedError, (500, 'Unexpected arguments to CGI')
   CONF.loadconfig(OPTS.config)
   D.opendb()
@@ -143,6 +163,14 @@ def dispatch_cgi():
   ## `cgi-noauth'.
   if ctx != 'cgi-noauth':
 
+    ## The next part of the URL should be the user name, so that caches don't
+    ## cross things over.
+    expuser = CGI.SPECIAL['%user']
+    if expuser is None:
+      if i >= np: raise U.ExpectedError, (404, 'Missing user name')
+      expuser = CGI.PATH[i]
+      i += 1
+
     ## If there's no token cookie, then we have to bail.
     try: token = CGI.COOKIE['chpwd-token']
     except KeyError:
@@ -166,6 +194,8 @@ def dispatch_cgi():
     except HA.AuthenticationFailed, e:
       CGI.redirect(CGI.action('login', why = e.why))
       return
+    if CU.USER != expuser: raise U.ExpectedError, (401, 'User mismatch')
+    CGI.STATE.kw['user'] = CU.USER
 
   ## Invoke the subcommand handler.
   c.cgi(CGI.PARAM, CGI.PATH[i:])
@@ -187,24 +217,30 @@ def cli_errors():
 
 if __name__ == '__main__':
 
+  L.openlog(OS.path.basename(SYS.argv[0]), 0, L.LOG_AUTH)
+
   if 'REQUEST_METHOD' in ENV:
     ## This looks like a CGI request.  The heavy lifting for authentication
     ## over HTTP is done in `dispatch_cgi'.
 
     with OUT.redirect_to(CGI.HTTPOutput()):
-      with CGI.cgi_errors(cgi_setup): dispatch_cgi()
+      with U.Escape() as CGI.HEADER_DONE:
+        with CGI.cgi_errors(cgi_setup):
+          dispatch_cgi()
 
   elif 'USERV_SERVICE' in ENV:
     ## This is a Userv request.  The caller's user name is helpfully in the
     ## `USERV_USER' environment variable.
 
     with cli_errors():
-      OPTS, args = OPTPARSE.parse_args()
-      CONF.loadconfig(OPTS.config)
-      try: CU.set_user(ENV['USERV_USER'])
-      except KeyError: raise ExpectedError, (500, 'USERV_USER unset')
       with OUT.redirect_to(O.FileOutput()):
-        OPTPARSE.dispatch('userv', [ENV['USERV_SERVICE']] + args)
+        args = parse_options()
+        if not args or args[0] != 'userv':
+          raise U.ExpectedError, (500, 'missing userv token')
+        CONF.loadconfig(OPTS.config)
+        try: CU.set_user(ENV['USERV_USER'])
+        except KeyError: raise ExpectedError, (500, 'USERV_USER unset')
+        OPTPARSE.dispatch('userv', [ENV['USERV_SERVICE']] + args[1:])
 
   elif 'SSH_ORIGINAL_COMMAND' in ENV:
     ## This looks like an SSH request; but we present two different
@@ -213,11 +249,10 @@ if __name__ == '__main__':
 
     def ssh_setup():
       """Extract and parse the client's request from where SSH left it."""
-      global OPTS
-      OPTS, args = OPTPARSE.parse_args()
+      args = parse_options()
       CONF.loadconfig(OPTS.config)
       cmd = SL.split(ENV['SSH_ORIGINAL_COMMAND'])
-      if args: raise ExpectedError, (500, 'Unexpected arguments via SSH')
+      if args: raise U.ExpectedError, (500, 'Unexpected arguments via SSH')
       return cmd
 
     if 'CHPWD_SSH_USER' in ENV:
@@ -225,10 +260,9 @@ if __name__ == '__main__':
       ## of telling us that this is a user request, so treat it like Userv.
 
       with cli_errors():
-        cmd = ssh_setup()
-        CU.set_user(ENV['CHPWD_SSH_USER'])
-        SERVICES['master'].find(USER)
         with OUT.redirect_to(O.FileOutput()):
+          cmd = ssh_setup()
+          CU.set_user(ENV['CHPWD_SSH_USER'])
           OPTPARSE.dispatch('userv', cmd)
 
     elif 'CHPWD_SSH_MASTER' in ENV:
@@ -238,10 +272,10 @@ if __name__ == '__main__':
       ## a user.
 
       try:
-        cmd = ssh_setup()
         with OUT.redirect_to(O.RemoteOutput()):
-          OPTPARSE.dispatch('remote', map(urldecode, cmd))
-      except ExpectedError, e:
+          cmd = ssh_setup()
+          OPTPARSE.dispatch('remote', map(CGI.urldecode, cmd))
+      except U.ExpectedError, e:
         print 'ERR', e.code, e.msg
       else:
         print 'OK'
@@ -251,7 +285,7 @@ if __name__ == '__main__':
       ## file, but we can't do much about it from here.
 
       with cli_errors():
-        raise ExpectedError, (400, "Unabled to determine SSH context")
+        raise U.ExpectedError, (400, "Unabled to determine SSH context")
 
   else:
     ## Plain old command line, apparently.  We default to administration
@@ -260,17 +294,20 @@ if __name__ == '__main__':
     ## as we are.
 
     with cli_errors():
-      OPTS, args = OPTPARSE.parse_args()
-      CONF.loadconfig(OPTS.config)
-      CGI.SSLP = OPTS.sslp
-      ctx = OPTS.context
-      if OPTS.user:
-        CU.set_user(OPTS.user)
-        if ctx is None: ctx = 'userv'
-      else:
-        D.opendb()
-        if ctx is None: ctx = 'admin'
       with OUT.redirect_to(O.FileOutput()):
+        args = parse_options()
+        CONF.loadconfig(OPTS.config)
+        CGI.SSLP = OPTS.sslp
+        ctx = OPTS.context
+        if OPTS.user:
+          CU.set_user(OPTS.user)
+          CGI.STATE.kw['user'] = OPTS.user
+          if ctx is None: ctx = 'userv'
+        else:
+          D.opendb()
+          if ctx is None:
+            ctx = 'admin'
+            OPTPARSE.show_global_opts = True
         OPTPARSE.dispatch(ctx, args)
 
 ###----- That's all, folks --------------------------------------------------