- add BootAuth (backward compatible Boot Manager authentication scheme)
[plcapi.git] / PLC / Auth.py
1 #
2 # PLCAPI authentication parameters
3 #
4 # Mark Huang <mlhuang@cs.princeton.edu>
5 # Copyright (C) 2006 The Trustees of Princeton University
6 #
7 # $Id: Auth.py,v 1.3 2006/09/25 14:47:32 mlhuang Exp $
8 #
9
10 import crypt
11 import sha
12 import hmac
13
14 from PLC.Faults import *
15 from PLC.Parameter import Parameter, Mixed
16 from PLC.Persons import Persons
17 from PLC.Nodes import Node, Nodes
18
19 class Auth(Parameter, dict):
20     """
21     Base class for all API authentication methods.
22     """
23
24     def __init__(self, auth):
25         Parameter.__init__(self, auth, "API authentication structure")
26         dict.__init__(auth)
27
28 class BootAuth(Auth):
29     """
30     PlanetLab version 3.x node authentication structure. Used by the
31     Boot Manager to make authenticated calls to the API based on a
32     unique node key or boot nonce value.
33
34     The original parameter serialization code did not define the byte
35     encoding of strings, or the string encoding of all other types. We
36     define the byte encoding to be UTF-8, and the string encoding of
37     all other types to be however Python version 2.3 unicode() encodes
38     them.
39     """
40
41     def __init__(self):
42         Auth.__init__(self, {
43             'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional = False),
44             'node_id': Parameter(int, "Node identifier", optional = False),
45             'value': Parameter(str, "HMAC of node key and method call", optional = False)
46             })
47
48     def canonicalize(self, args):
49         values = []
50
51         for arg in args:
52             if isinstance(arg, list) or isinstance(arg, tuple):
53                 # The old implementation did not recursively handle
54                 # lists of lists. But neither did the old API itself.
55                 values += self.canonicalize(arg)
56             elif isinstance(arg, dict):
57                 # Yes, the comments in the old implementation are
58                 # misleading. Keys of dicts are not included in the
59                 # hash.
60                 values += self.canonicalize(arg.values())
61             else:
62                 # We use unicode() instead of str().
63                 values.append(unicode(arg))
64
65         return values
66
67     def check(self, method, auth, *args):
68         # Method.type_check() should have checked that all of the
69         # mandatory fields were present.
70         assert auth.has_key('node_id')
71
72         try:
73             nodes = Nodes(method.api, [auth['node_id']]).values()
74             if not nodes:
75                 raise PLCAuthenticationFailure, "No such node"
76             node = nodes[0]
77
78             if node['key']:
79                 key = node['key']
80             elif node['boot_nonce']:
81                 # Allow very old nodes that do not have a node key in
82                 # their configuration files to use their "boot nonce"
83                 # instead. The boot nonce is a random value generated
84                 # by the node itself and POSTed by the Boot CD when it
85                 # requests the Boot Manager. This is obviously not
86                 # very secure, so we only allow it to be used if the
87                 # requestor IP is the same as the IP address we have
88                 # on record for the node.
89                 key = node['boot_nonce']
90
91                 nodenetwork = None
92                 if node['nodenetwork_ids']:
93                     nodenetworks = NodeNetworks(method.api, node['nodenetwork_ids']).values()
94                     for nodenetwork in nodenetworks:
95                         if nodenetwork['is_primary']:
96                             break
97             
98                 if not nodenetwork or not nodenetwork['is_primary']:
99                     raise PLCAuthenticationFailure, "No primary network interface on record"
100             
101                 if method.source is None:
102                     raise PLCAuthenticationFailure, "Cannot determine IP address of requestor"
103
104                 if nodenetwork['ip'] != method.source[0]:
105                     raise PLCAuthenticationFailure, "Requestor IP %s does not mach node IP %s" % \
106                           (method.source[0], nodenetwork['ip'])
107             else:
108                 raise PLCAuthenticationFailure, "No node key or boot nonce"
109
110             # Yes, this is the "canonicalization" method used.
111             args = self.canonicalize(args)
112             args.sort()
113             msg = "[" + "".join(args) + "]"
114
115             # We encode in UTF-8 before calculating the HMAC, which is
116             # an 8-bit algorithm.
117             digest = hmac.new(key, msg.encode('utf-8'), sha).hexdigest()
118
119             if digest != auth['value']:
120                 raise PLCAuthenticationFailure, "Call could not be authenticated"
121
122             method.caller = node
123
124         except PLCAuthenticationFailure, fault:
125             # XXX Send e-mail
126             raise fault
127
128 class AnonymousAuth(Auth):
129     """
130     PlanetLab version 3.x anonymous authentication structure.
131     """
132
133     def __init__(self):
134         Auth.__init__(self, {
135             'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
136             })
137
138     def check(self, method, auth, *args):
139         # Sure, dude, whatever
140         return True
141
142 class PasswordAuth(Auth):
143     """
144     PlanetLab version 3.x password authentication structure.
145     """
146
147     def __init__(self):
148         Auth.__init__(self, {
149             'AuthMethod': Parameter(str, "Authentication method to use, typically 'password'", optional = False),
150             'Username': Parameter(str, "PlanetLab username, typically an e-mail address", optional = False),
151             'AuthString': Parameter(str, "Authentication string, typically a password", optional = False),
152             })
153
154     def check(self, method, auth, *args):
155         # Method.type_check() should have checked that all of the
156         # mandatory fields were present.
157         assert auth.has_key('Username')
158
159         # Get record (must be enabled)
160         persons = Persons(method.api, [auth['Username']], enabled = True)
161         if len(persons) != 1:
162             raise PLCAuthenticationFailure, "No such account"
163
164         person = persons.values()[0]
165
166         if auth['Username'] == method.api.config.PLC_API_MAINTENANCE_USER:
167             # "Capability" authentication, whatever the hell that was
168             # supposed to mean. It really means, login as the special
169             # "maintenance user" using password authentication. Can
170             # only be used on particular machines (those in a list).
171             sources = method.api.config.PLC_API_MAINTENANCE_SOURCES.split()
172             if method.source is not None and method.source[0] not in sources:
173                 raise PLCAuthenticationFailure, "Not allowed to login to maintenance account"
174
175             # Not sure why this is not stored in the DB
176             password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
177
178             if auth['AuthString'] != password:
179                 raise PLCAuthenticationFailure, "Maintenance account password verification failed"
180         else:
181             # Compare encrypted plaintext against encrypted password stored in the DB
182             plaintext = auth['AuthString'].encode(method.api.encoding)
183             password = person['password']
184
185             # Protect against blank passwords in the DB
186             if password is None or password[:12] == "" or \
187                crypt.crypt(plaintext, password[:12]) != password:
188                 raise PLCAuthenticationFailure, "Password verification failed"
189
190         if not set(person['roles']).intersection(method.roles):
191             raise PLCAuthenticationFailure, "Not allowed to call method"
192
193         method.caller = person