Setting tag plcapi-7.1-0
[plcapi.git] / PLC / Auth.py
index b11e855..927850e 100644 (file)
@@ -4,14 +4,12 @@
 # Mark Huang <mlhuang@cs.princeton.edu>
 # Copyright (C) 2006 The Trustees of Princeton University
 #
 # Mark Huang <mlhuang@cs.princeton.edu>
 # Copyright (C) 2006 The Trustees of Princeton University
 #
-# $Id$
-# $URL$
-#
 
 import crypt
 
 import crypt
-import sha
+from hashlib import sha1 as sha
 import hmac
 import time
 import hmac
 import time
+import os
 
 from PLC.Faults import *
 from PLC.Parameter import Parameter, Mixed
 
 from PLC.Faults import *
 from PLC.Parameter import Parameter, Mixed
@@ -20,24 +18,9 @@ from PLC.Nodes import Node, Nodes
 from PLC.Interfaces import Interface, Interfaces
 from PLC.Sessions import Session, Sessions
 from PLC.Peers import Peer, Peers
 from PLC.Interfaces import Interface, Interfaces
 from PLC.Sessions import Session, Sessions
 from PLC.Peers import Peer, Peers
+from PLC.Keys import Keys
 from PLC.Boot import notify_owners
 from PLC.Boot import notify_owners
-
-def map_auth(auth):
-    if auth['AuthMethod'] == "session":
-        expected = SessionAuth()
-    elif auth['AuthMethod'] == "password" or \
-         auth['AuthMethod'] == "capability":
-        expected = PasswordAuth()
-    elif auth['AuthMethod'] == "gpg":
-        expected = GPGAuth()
-    elif auth['AuthMethod'] == "hmac" or \
-         auth['AuthMethod'] == "hmac_dummybox":
-        expected = BootAuth()
-    elif auth['AuthMethod'] == "anonymous":
-        expected = AnonymousAuth()
-    else:
-        raise PLCInvalidArgument("must be 'session', 'password', 'gpg', 'hmac', 'hmac_dummybox', or 'anonymous'", "AuthMethod")
-    return expected
+from PLC.Logger import logger
 
 class Auth(Parameter):
     """
 
 class Auth(Parameter):
     """
@@ -46,21 +29,29 @@ class Auth(Parameter):
     methods.
     """
 
     methods.
     """
 
-    def __init__(self, auth = None):
+    def __init__(self, auth=None):
         if auth is None:
         if auth is None:
-            auth = {'AuthMethod': Parameter(str, "Authentication method to use", optional = False)}
+            auth = {'AuthMethod': Parameter(str, "Authentication method to use",
+                                            optional=False)}
         Parameter.__init__(self, auth, "API authentication structure")
 
     def check(self, method, auth, *args):
         Parameter.__init__(self, auth, "API authentication structure")
 
     def check(self, method, auth, *args):
+        global auth_methods
+
         # Method.type_check() should have checked that all of the
         # mandatory fields were present.
         assert 'AuthMethod' in auth
 
         # Method.type_check() should have checked that all of the
         # mandatory fields were present.
         assert 'AuthMethod' in auth
 
-        expected = map_auth(auth)
+        if auth['AuthMethod'] in auth_methods:
+            expected = auth_methods[auth['AuthMethod']]()
+        else:
+            sm = "'" + "', '".join(list(auth_methods.keys())) + "'"
+            raise PLCInvalidArgument("must be " + sm, "AuthMethod")
 
         # Re-check using the specified authentication method
         method.type_check("auth", auth, expected, (auth,) + args)
 
 
         # Re-check using the specified authentication method
         method.type_check("auth", auth, expected, (auth,) + args)
 
+
 class GPGAuth(Auth):
     """
     Proposed PlanetLab federation authentication structure.
 class GPGAuth(Auth):
     """
     Proposed PlanetLab federation authentication structure.
@@ -68,48 +59,56 @@ class GPGAuth(Auth):
 
     def __init__(self):
         Auth.__init__(self, {
 
     def __init__(self):
         Auth.__init__(self, {
-            'AuthMethod': Parameter(str, "Authentication method to use, always 'gpg'", optional = False),
-            'name': Parameter(str, "Peer or user name", optional = False),
-            'signature': Parameter(str, "Message signature", optional = False)
-            })
+            'AuthMethod': Parameter(str, "Authentication method to use, always 'gpg'", optional=False),
+            'name': Parameter(str, "Peer or user name", optional=False),
+            'signature': Parameter(str, "Message signature", optional=False)
+        })
 
     def check(self, method, auth, *args):
         try:
             peers = Peers(method.api, [auth['name']])
             if peers:
                 if 'peer' not in method.roles:
 
     def check(self, method, auth, *args):
         try:
             peers = Peers(method.api, [auth['name']])
             if peers:
                 if 'peer' not in method.roles:
-                    raise PLCAuthenticationFailure, "Not allowed to call method"
+                    raise PLCAuthenticationFailure(
+                        "GPGAuth: Not allowed to call method, missing 'peer' role")
 
                 method.caller = peer = peers[0]
 
                 method.caller = peer = peers[0]
-                keys = [peer['key']]
+                gpg_keys = [peer['key']]
             else:
             else:
-                persons = Persons(method.api, {'email': auth['name'], 'enabled': True, 'peer_id': None})
+                persons = Persons(
+                    method.api, {'email': auth['name'], 'enabled': True, 'peer_id': None})
                 if not persons:
                 if not persons:
-                    raise PLCAuthenticationFailure, "No such user '%s'" % auth['name']
+                    raise PLCAuthenticationFailure(
+                        "GPGAuth: No such user '%s'" % auth['name'])
 
 
+                method.caller = person = persons[0]
                 if not set(person['roles']).intersection(method.roles):
                 if not set(person['roles']).intersection(method.roles):
-                    raise PLCAuthenticationFailure, "Not allowed to call method"
+                    raise PLCAuthenticationFailure(
+                        "GPGAuth: Not allowed to call method, missing role")
 
 
-                method.caller = person = persons[0]
-                keys = Keys(method.api, {'key_id': person['key_ids'], 'key_type': "gpg", 'peer_id': None})
+                keys = Keys(
+                    method.api, {'key_id': person['key_ids'], 'key_type': "gpg", 'peer_id': None})
+                gpg_keys = [key['key'] for key in keys]
 
 
-            if not keys:
-                raise PLCAuthenticationFailure, "No GPG key on record for peer or user '%s'"
+            if not gpg_keys:
+                raise PLCAuthenticationFailure(
+                    "GPGAuth: No GPG key on record for peer or user '%s'" % auth['name'])
 
 
-            for key in keys:
+            for gpg_key in gpg_keys:
                 try:
                     from PLC.GPG import gpg_verify
                 try:
                     from PLC.GPG import gpg_verify
-                    gpg_verify(args, key, auth['signature'], method.name)
+                    gpg_verify(args, gpg_key, auth['signature'], method.name)
                     return
                     return
-                except PLCAuthenticationFailure, fault:
+                except PLCAuthenticationFailure as fault:
                     pass
 
             raise fault
 
                     pass
 
             raise fault
 
-        except PLCAuthenticationFailure, fault:
+        except PLCAuthenticationFailure as fault:
             # XXX Send e-mail
             raise fault
 
             # XXX Send e-mail
             raise fault
 
+
 class SessionAuth(Auth):
     """
     Secondary authentication method. After authenticating with a
 class SessionAuth(Auth):
     """
     Secondary authentication method. After authenticating with a
@@ -119,70 +118,77 @@ class SessionAuth(Auth):
 
     def __init__(self):
         Auth.__init__(self, {
 
     def __init__(self):
         Auth.__init__(self, {
-            'AuthMethod': Parameter(str, "Authentication method to use, always 'session'", optional = False),
-            'session': Parameter(str, "Session key", optional = False)
-            })
+            'AuthMethod': Parameter(str, "Authentication method to use, always 'session'", optional=False),
+            'session': Parameter(str, "Session key", optional=False)
+        })
 
     def check(self, method, auth, *args):
         # Method.type_check() should have checked that all of the
         # mandatory fields were present.
 
     def check(self, method, auth, *args):
         # Method.type_check() should have checked that all of the
         # mandatory fields were present.
-        assert auth.has_key('session')
+        assert 'session' in auth
 
         # Get session record
 
         # Get session record
-        sessions = Sessions(method.api, [auth['session']], expires = None)
+        sessions = Sessions(method.api, [auth['session']], expires=None)
         if not sessions:
         if not sessions:
-            raise PLCAuthenticationFailure, "No such session"
+            raise PLCAuthenticationFailure("SessionAuth: No such session")
         session = sessions[0]
 
         try:
             if session['node_id'] is not None:
         session = sessions[0]
 
         try:
             if session['node_id'] is not None:
-                nodes = Nodes(method.api, {'node_id': session['node_id'], 'peer_id': None})
+                nodes = Nodes(
+                    method.api, {'node_id': session['node_id'], 'peer_id': None})
                 if not nodes:
                 if not nodes:
-                    raise PLCAuthenticationFailure, "No such node"
+                    raise PLCAuthenticationFailure("SessionAuth: No such node")
                 node = nodes[0]
 
                 if 'node' not in method.roles:
                 node = nodes[0]
 
                 if 'node' not in method.roles:
-                    raise PLCAuthenticationFailure, "Not allowed to call method"
+                    # using PermissionDenied rather than AuthenticationFailure here because
+                    # if that fails we don't want to delete the session..
+                    raise PLCPermissionDenied(
+                        "SessionAuth: Not allowed to call method %s, missing 'node' role" % method.name)
 
                 method.caller = node
 
             elif session['person_id'] is not None and session['expires'] > time.time():
 
                 method.caller = node
 
             elif session['person_id'] is not None and session['expires'] > time.time():
-                persons = Persons(method.api, {'person_id': session['person_id'], 'enabled': True, 'peer_id': None})
+                persons = Persons(method.api, {
+                                  'person_id': session['person_id'], 'enabled': True, 'peer_id': None})
                 if not persons:
                 if not persons:
-                    raise PLCAuthenticationFailure, "No such account"
+                    raise PLCAuthenticationFailure(
+                        "SessionAuth: No such enabled account")
                 person = persons[0]
 
                 if not set(person['roles']).intersection(method.roles):
                 person = persons[0]
 
                 if not set(person['roles']).intersection(method.roles):
-                    raise PLCPermissionDenied, "Not allowed to call method"
+                    method_message = "method %s has roles [%s]" % (
+                        method.name, ','.join(method.roles))
+                    person_message = "caller %s has roles [%s]" % (
+                        person['email'], ','.join(person['roles']))
+                    # not PLCAuthenticationFailure b/c that would end the session..
+                    raise PLCPermissionDenied(
+                        "SessionAuth: missing role, %s -- %s" % (method_message, person_message))
 
 
-                method.caller = persons[0]
+                method.caller = person
 
             else:
 
             else:
-                raise PLCAuthenticationFailure, "Invalid session"
+                raise PLCAuthenticationFailure("SessionAuth: Invalid session")
 
 
-        except PLCAuthenticationFailure, fault:
+        except PLCAuthenticationFailure as fault:
             session.delete()
             raise fault
 
             session.delete()
             raise fault
 
+
 class BootAuth(Auth):
     """
     PlanetLab version 3.x node authentication structure. Used by the
     Boot Manager to make authenticated calls to the API based on a
     unique node key or boot nonce value.
 class BootAuth(Auth):
     """
     PlanetLab version 3.x node authentication structure. Used by the
     Boot Manager to make authenticated calls to the API based on a
     unique node key or boot nonce value.
-
-    The original parameter serialization code did not define the byte
-    encoding of strings, or the string encoding of all other types. We
-    define the byte encoding to be UTF-8, and the string encoding of
-    all other types to be however Python version 2.3 unicode() encodes
-    them.
     """
 
     def __init__(self):
         Auth.__init__(self, {
     """
 
     def __init__(self):
         Auth.__init__(self, {
-            'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional = False),
-            'node_id': Parameter(int, "Node identifier", optional = False),
-            'value': Parameter(str, "HMAC of node key and method call", optional = False)
-            })
+            'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional=False),
+            'node_id': Parameter(int, "Node identifier", optional=False),
+            'value': Parameter(str, "HMAC of node key and method call", optional=False)
+        })
 
     def canonicalize(self, args):
         values = []
 
     def canonicalize(self, args):
         values = []
@@ -196,79 +202,55 @@ class BootAuth(Auth):
                 # Yes, the comments in the old implementation are
                 # misleading. Keys of dicts are not included in the
                 # hash.
                 # Yes, the comments in the old implementation are
                 # misleading. Keys of dicts are not included in the
                 # hash.
-                values += self.canonicalize(arg.values())
+                values += self.canonicalize(list(arg.values()))
             else:
                 # We use unicode() instead of str().
             else:
                 # We use unicode() instead of str().
-                values.append(unicode(arg))
+                values.append(str(arg))
 
         return values
 
     def check(self, method, auth, *args):
         # Method.type_check() should have checked that all of the
         # mandatory fields were present.
 
         return values
 
     def check(self, method, auth, *args):
         # Method.type_check() should have checked that all of the
         # mandatory fields were present.
-        assert auth.has_key('node_id')
+        assert 'node_id' in auth
 
         if 'node' not in method.roles:
 
         if 'node' not in method.roles:
-            raise PLCAuthenticationFailure, "Not allowed to call method"
+            raise PLCAuthenticationFailure(
+                "BootAuth: Not allowed to call method, missing 'node' role")
 
         try:
 
         try:
-            nodes = Nodes(method.api, {'node_id': auth['node_id'], 'peer_id': None})
+            nodes = Nodes(
+                method.api, {'node_id': auth['node_id'], 'peer_id': None})
             if not nodes:
             if not nodes:
-                raise PLCAuthenticationFailure, "No such node"
+                raise PLCAuthenticationFailure("BootAuth: No such node")
             node = nodes[0]
 
             node = nodes[0]
 
+            # Jan 2011 : removing support for old boot CDs
             if node['key']:
                 key = node['key']
             if node['key']:
                 key = node['key']
-            elif node['boot_nonce']:
-                # Allow very old nodes that do not have a node key in
-                # their configuration files to use their "boot nonce"
-                # instead. The boot nonce is a random value generated
-                # by the node itself and POSTed by the Boot CD when it
-                # requests the Boot Manager. This is obviously not
-                # very secure, so we only allow it to be used if the
-                # requestor IP is the same as the IP address we have
-                # on record for the node.
-                key = node['boot_nonce']
-
-                interface = None
-                if node['interface_ids']:
-                    interfaces = Interfaces(method.api, node['interface_ids'])
-                    for interface in interfaces:
-                        if interface['is_primary']:
-                            break
-            
-                if not interface or not interface['is_primary']:
-                    raise PLCAuthenticationFailure, "No primary network interface on record"
-            
-                if method.source is None:
-                    raise PLCAuthenticationFailure, "Cannot determine IP address of requestor"
-
-                if interface['ip'] != method.source[0]:
-                    raise PLCAuthenticationFailure, "Requestor IP %s does not match node IP %s" % \
-                          (method.source[0], interface['ip'])
             else:
             else:
-                raise PLCAuthenticationFailure, "No node key or boot nonce"
+                raise PLCAuthenticationFailure("BootAuth: No node key")
 
             # Yes, this is the "canonicalization" method used.
             args = self.canonicalize(args)
             args.sort()
             msg = "[" + "".join(args) + "]"
 
 
             # Yes, this is the "canonicalization" method used.
             args = self.canonicalize(args)
             args.sort()
             msg = "[" + "".join(args) + "]"
 
-            # We encode in UTF-8 before calculating the HMAC, which is
-            # an 8-bit algorithm.
-            # python 2.6 insists on receiving a 'str' as opposed to a 'unicode'
-            digest = hmac.new(str(key), msg.encode('utf-8'), sha).hexdigest()
+            digest = hmac.new(key.encode('utf-8'), msg.encode('utf-8'), sha).hexdigest()
 
             if digest != auth['value']:
 
             if digest != auth['value']:
-                raise PLCAuthenticationFailure, "Call could not be authenticated"
+                raise PLCAuthenticationFailure(
+                    "BootAuth: Call could not be authenticated")
 
             method.caller = node
 
 
             method.caller = node
 
-        except PLCAuthenticationFailure, fault:
+        except PLCAuthenticationFailure as fault:
             if nodes:
             if nodes:
-                notify_owners(method, node, 'authfail', include_pis = True, include_techs = True, fault = fault)
+                notify_owners(method, node, 'authfail',
+                              include_pis=True, include_techs=True, fault=fault)
             raise fault
 
             raise fault
 
+
 class AnonymousAuth(Auth):
     """
     PlanetLab version 3.x anonymous authentication structure.
 class AnonymousAuth(Auth):
     """
     PlanetLab version 3.x anonymous authentication structure.
@@ -277,14 +259,16 @@ class AnonymousAuth(Auth):
     def __init__(self):
         Auth.__init__(self, {
             'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
     def __init__(self):
         Auth.__init__(self, {
             'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
-            })
+        })
 
     def check(self, method, auth, *args):
         if 'anonymous' not in method.roles:
 
     def check(self, method, auth, *args):
         if 'anonymous' not in method.roles:
-            raise PLCAuthenticationFailure, "Not allowed to call method anonymously"
+            raise PLCAuthenticationFailure(
+                "AnonymousAuth: method cannot be called anonymously")
 
         method.caller = None
 
 
         method.caller = None
 
+
 class PasswordAuth(Auth):
     """
     PlanetLab version 3.x password authentication structure.
 class PasswordAuth(Auth):
     """
     PlanetLab version 3.x password authentication structure.
@@ -292,20 +276,23 @@ class PasswordAuth(Auth):
 
     def __init__(self):
         Auth.__init__(self, {
 
     def __init__(self):
         Auth.__init__(self, {
-            'AuthMethod': Parameter(str, "Authentication method to use, always 'password' or 'capability'", optional = False),
-            'Username': Parameter(str, "PlanetLab username, typically an e-mail address", optional = False),
-            'AuthString': Parameter(str, "Authentication string, typically a password", optional = False),
-            })
+            'AuthMethod': Parameter(str, "Authentication method to use, always 'password' or 'capability'", optional=False),
+            'Username': Parameter(str, "PlanetLab username, typically an e-mail address", optional=False),
+            'AuthString': Parameter(str, "Authentication string, typically a password", optional=False),
+        })
 
     def check(self, method, auth, *args):
         # Method.type_check() should have checked that all of the
         # mandatory fields were present.
 
     def check(self, method, auth, *args):
         # Method.type_check() should have checked that all of the
         # mandatory fields were present.
-        assert auth.has_key('Username')
+        assert 'Username' in auth
 
         # Get record (must be enabled)
 
         # Get record (must be enabled)
-        persons = Persons(method.api, {'email': auth['Username'].lower(), 'enabled': True, 'peer_id': None})
+        normalized = auth['Username'].lower()
+        persons = Persons(method.api, {
+                          'email': normalized, 'enabled': True, 'peer_id': None})
         if len(persons) != 1:
         if len(persons) != 1:
-            raise PLCAuthenticationFailure, "No such account"
+            logger.info(f"PasswordAuth failed with {normalized}, got {len(persons)} matches")
+            raise PLCAuthenticationFailure("PasswordAuth: No such account")
 
         person = persons[0]
 
 
         person = persons[0]
 
@@ -316,24 +303,55 @@ class PasswordAuth(Auth):
             # only be used on particular machines (those in a list).
             sources = method.api.config.PLC_API_MAINTENANCE_SOURCES.split()
             if method.source is not None and method.source[0] not in sources:
             # only be used on particular machines (those in a list).
             sources = method.api.config.PLC_API_MAINTENANCE_SOURCES.split()
             if method.source is not None and method.source[0] not in sources:
-                raise PLCAuthenticationFailure, "Not allowed to login to maintenance account"
+                raise PLCAuthenticationFailure(
+                    "PasswordAuth: Not allowed to login to maintenance account")
 
             # Not sure why this is not stored in the DB
             password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
 
             if auth['AuthString'] != password:
 
             # Not sure why this is not stored in the DB
             password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
 
             if auth['AuthString'] != password:
-                raise PLCAuthenticationFailure, "Maintenance account password verification failed"
+                raise PLCAuthenticationFailure(
+                    "PasswordAuth: Maintenance account password verification failed")
         else:
             # Compare encrypted plaintext against encrypted password stored in the DB
         else:
             # Compare encrypted plaintext against encrypted password stored in the DB
-            plaintext = auth['AuthString'].encode(method.api.encoding)
+            plaintext = auth['AuthString']
             password = person['password']
 
             # Protect against blank passwords in the DB
             if password is None or password[:12] == "" or \
                crypt.crypt(plaintext, password[:12]) != password:
             password = person['password']
 
             # Protect against blank passwords in the DB
             if password is None or password[:12] == "" or \
                crypt.crypt(plaintext, password[:12]) != password:
-                raise PLCAuthenticationFailure, "Password verification failed"
+                raise PLCAuthenticationFailure(
+                    "PasswordAuth: Password verification failed")
 
         if not set(person['roles']).intersection(method.roles):
 
         if not set(person['roles']).intersection(method.roles):
-           raise PLCAuthenticationFailure, "Not allowed to call method"
+            method_message = "method %s has roles [%s]" % (
+                method.name, ','.join(method.roles))
+            person_message = "caller %s has roles [%s]" % (
+                person['email'], ','.join(person['roles']))
+            raise PLCAuthenticationFailure(
+                "PasswordAuth: missing role, %s -- %s" % (method_message, person_message))
 
         method.caller = person
 
         method.caller = person
+
+
+auth_methods = {'session': SessionAuth,
+                'password': PasswordAuth,
+                'capability': PasswordAuth,
+                'gpg': GPGAuth,
+                'hmac': BootAuth,
+                'hmac_dummybox': BootAuth,
+                'anonymous': AnonymousAuth}
+
+path = os.path.dirname(__file__) + "/Auth.d"
+try:
+    extensions = os.listdir(path)
+except OSError as e:
+    extensions = []
+for extension in extensions:
+    if extension.startswith("."):
+        continue
+    if not extension.endswith(".py"):
+        continue
+    exec(compile(open("%s/%s" % (path, extension)).read(),
+                 "%s/%s" % (path, extension), 'exec'))
+del extensions