more info on person when missing roles
[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
8 import crypt
9 try:
10     from hashlib import sha1 as sha
11 except ImportError:
12     import sha
13 import hmac
14 import time
15 import os
16
17 from PLC.Faults import *
18 from PLC.Parameter import Parameter, Mixed
19 from PLC.Persons import Persons
20 from PLC.Nodes import Node, Nodes
21 from PLC.Interfaces import Interface, Interfaces
22 from PLC.Sessions import Session, Sessions
23 from PLC.Peers import Peer, Peers
24 from PLC.Boot import notify_owners
25
26 class Auth(Parameter):
27     """
28     Base class for all API authentication methods, as well as a class
29     that can be used to represent all supported API authentication
30     methods.
31     """
32
33     def __init__(self, auth = None):
34         if auth is None:
35             auth = {'AuthMethod': Parameter(str, "Authentication method to use", optional = False)}
36         Parameter.__init__(self, auth, "API authentication structure")
37
38     def check(self, method, auth, *args):
39         global auth_methods
40
41         # Method.type_check() should have checked that all of the
42         # mandatory fields were present.
43         assert 'AuthMethod' in auth
44
45         if auth['AuthMethod'] in auth_methods:
46             expected = auth_methods[auth['AuthMethod']]()
47         else:
48             sm = "'" + "', '".join(auth_methods.keys()) + "'"
49             raise PLCInvalidArgument("must be " + sm, "AuthMethod")
50
51         # Re-check using the specified authentication method
52         method.type_check("auth", auth, expected, (auth,) + args)
53
54 class GPGAuth(Auth):
55     """
56     Proposed PlanetLab federation authentication structure.
57     """
58
59     def __init__(self):
60         Auth.__init__(self, {
61             'AuthMethod': Parameter(str, "Authentication method to use, always 'gpg'", optional = False),
62             'name': Parameter(str, "Peer or user name", optional = False),
63             'signature': Parameter(str, "Message signature", optional = False)
64             })
65
66     def check(self, method, auth, *args):
67         try:
68             peers = Peers(method.api, [auth['name']])
69             if peers:
70                 if 'peer' not in method.roles:
71                     raise PLCAuthenticationFailure, "GPGAuth: Not allowed to call method, missing 'peer' role"
72
73                 method.caller = peer = peers[0]
74                 keys = [peer['key']]
75             else:
76                 persons = Persons(method.api, {'email': auth['name'], 'enabled': True, 'peer_id': None})
77                 if not persons:
78                     raise PLCAuthenticationFailure, "GPGAuth: No such user '%s'" % auth['name']
79
80                 if not set(person['roles']).intersection(method.roles):
81                     raise PLCAuthenticationFailure, "GPGAuth: Not allowed to call method, missing role"
82
83                 method.caller = person = persons[0]
84                 keys = Keys(method.api, {'key_id': person['key_ids'], 'key_type': "gpg", 'peer_id': None})
85
86             if not keys:
87                 raise PLCAuthenticationFailure, "GPGAuth: No GPG key on record for peer or user '%s'"
88
89             for key in keys:
90                 try:
91                     from PLC.GPG import gpg_verify
92                     gpg_verify(args, key, auth['signature'], method.name)
93                     return
94                 except PLCAuthenticationFailure, fault:
95                     pass
96
97             raise fault
98
99         except PLCAuthenticationFailure, fault:
100             # XXX Send e-mail
101             raise fault
102
103 class SessionAuth(Auth):
104     """
105     Secondary authentication method. After authenticating with a
106     primary authentication method, call GetSession() to generate a
107     session key that may be used for subsequent calls.
108     """
109
110     def __init__(self):
111         Auth.__init__(self, {
112             'AuthMethod': Parameter(str, "Authentication method to use, always 'session'", optional = False),
113             'session': Parameter(str, "Session key", optional = False)
114             })
115
116     def check(self, method, auth, *args):
117         # Method.type_check() should have checked that all of the
118         # mandatory fields were present.
119         assert auth.has_key('session')
120
121         # Get session record
122         sessions = Sessions(method.api, [auth['session']], expires = None)
123         if not sessions:
124             raise PLCAuthenticationFailure, "SessionAuth: No such session"
125         session = sessions[0]
126
127         try:
128             if session['node_id'] is not None:
129                 nodes = Nodes(method.api, {'node_id': session['node_id'], 'peer_id': None})
130                 if not nodes:
131                     raise PLCAuthenticationFailure, "SessionAuth: No such node"
132                 node = nodes[0]
133
134                 if 'node' not in method.roles:
135                     raise PLCAuthenticationFailure, "SessionAuth: Not allowed to call method %s, missing 'node' role"%method.name
136
137                 method.caller = node
138
139             elif session['person_id'] is not None and session['expires'] > time.time():
140                 persons = Persons(method.api, {'person_id': session['person_id'], 'enabled': True, 'peer_id': None})
141                 if not persons:
142                     raise PLCAuthenticationFailure, "SessionAuth: No such enabled account"
143                 person = persons[0]
144
145                 if not set(person['roles']).intersection(method.roles):
146                     method_message="method %s has roles [%s]"%(method.name,','.join(method.roles))
147                     person_message="caller %s has roles [%s]"%(person['email'],','.join(person['roles']))
148                     # not PLCAuthenticationFailure b/c that would end the session..
149                     raise PLCPermissionDenied, "SessionAuth: missing role, %s -- %s"%(method_message,person_message)
150
151                 method.caller = person
152
153             else:
154                 raise PLCAuthenticationFailure, "SessionAuth: Invalid session"
155
156         except PLCAuthenticationFailure, fault:
157             session.delete()
158             raise fault
159
160 class BootAuth(Auth):
161     """
162     PlanetLab version 3.x node authentication structure. Used by the
163     Boot Manager to make authenticated calls to the API based on a
164     unique node key or boot nonce value.
165
166     The original parameter serialization code did not define the byte
167     encoding of strings, or the string encoding of all other types. We
168     define the byte encoding to be UTF-8, and the string encoding of
169     all other types to be however Python version 2.3 unicode() encodes
170     them.
171     """
172
173     def __init__(self):
174         Auth.__init__(self, {
175             'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional = False),
176             'node_id': Parameter(int, "Node identifier", optional = False),
177             'value': Parameter(str, "HMAC of node key and method call", optional = False)
178             })
179
180     def canonicalize(self, args):
181         values = []
182
183         for arg in args:
184             if isinstance(arg, list) or isinstance(arg, tuple):
185                 # The old implementation did not recursively handle
186                 # lists of lists. But neither did the old API itself.
187                 values += self.canonicalize(arg)
188             elif isinstance(arg, dict):
189                 # Yes, the comments in the old implementation are
190                 # misleading. Keys of dicts are not included in the
191                 # hash.
192                 values += self.canonicalize(arg.values())
193             else:
194                 # We use unicode() instead of str().
195                 values.append(unicode(arg))
196
197         return values
198
199     def check(self, method, auth, *args):
200         # Method.type_check() should have checked that all of the
201         # mandatory fields were present.
202         assert auth.has_key('node_id')
203
204         if 'node' not in method.roles:
205             raise PLCAuthenticationFailure, "BootAuth: Not allowed to call method, missing 'node' role"
206
207         try:
208             nodes = Nodes(method.api, {'node_id': auth['node_id'], 'peer_id': None})
209             if not nodes:
210                 raise PLCAuthenticationFailure, "BootAuth: No such node"
211             node = nodes[0]
212
213             if node['key']:
214                 key = node['key']
215             elif node['boot_nonce']:
216                 # Allow very old nodes that do not have a node key in
217                 # their configuration files to use their "boot nonce"
218                 # instead. The boot nonce is a random value generated
219                 # by the node itself and POSTed by the Boot CD when it
220                 # requests the Boot Manager. This is obviously not
221                 # very secure, so we only allow it to be used if the
222                 # requestor IP is the same as the IP address we have
223                 # on record for the node.
224                 key = node['boot_nonce']
225
226                 interface = None
227                 if node['interface_ids']:
228                     interfaces = Interfaces(method.api, node['interface_ids'])
229                     for interface in interfaces:
230                         if interface['is_primary']:
231                             break
232
233                 if not interface or not interface['is_primary']:
234                     raise PLCAuthenticationFailure, "BootAuth: No primary network interface on record"
235
236                 if method.source is None:
237                     raise PLCAuthenticationFailure, "BootAuth: Cannot determine IP address of requestor"
238
239                 if interface['ip'] != method.source[0]:
240                     raise PLCAuthenticationFailure, "BootAuth: Requestor IP %s does not match node IP %s" % \
241                           (method.source[0], interface['ip'])
242             else:
243                 raise PLCAuthenticationFailure, "BootAuth: No node key or boot nonce"
244
245             # Yes, this is the "canonicalization" method used.
246             args = self.canonicalize(args)
247             args.sort()
248             msg = "[" + "".join(args) + "]"
249
250             # We encode in UTF-8 before calculating the HMAC, which is
251             # an 8-bit algorithm.
252             # python 2.6 insists on receiving a 'str' as opposed to a 'unicode'
253             digest = hmac.new(str(key), msg.encode('utf-8'), sha).hexdigest()
254
255             if digest != auth['value']:
256                 raise PLCAuthenticationFailure, "BootAuth: Call could not be authenticated"
257
258             method.caller = node
259
260         except PLCAuthenticationFailure, fault:
261             if nodes:
262                 notify_owners(method, node, 'authfail', include_pis = True, include_techs = True, fault = fault)
263             raise fault
264
265 class AnonymousAuth(Auth):
266     """
267     PlanetLab version 3.x anonymous authentication structure.
268     """
269
270     def __init__(self):
271         Auth.__init__(self, {
272             'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
273             })
274
275     def check(self, method, auth, *args):
276         if 'anonymous' not in method.roles:
277             raise PLCAuthenticationFailure, "AnonymousAuth: method cannot be called anonymously"
278
279         method.caller = None
280
281 class PasswordAuth(Auth):
282     """
283     PlanetLab version 3.x password authentication structure.
284     """
285
286     def __init__(self):
287         Auth.__init__(self, {
288             'AuthMethod': Parameter(str, "Authentication method to use, always 'password' or 'capability'", optional = False),
289             'Username': Parameter(str, "PlanetLab username, typically an e-mail address", optional = False),
290             'AuthString': Parameter(str, "Authentication string, typically a password", optional = False),
291             })
292
293     def check(self, method, auth, *args):
294         # Method.type_check() should have checked that all of the
295         # mandatory fields were present.
296         assert auth.has_key('Username')
297
298         # Get record (must be enabled)
299         persons = Persons(method.api, {'email': auth['Username'].lower(), 'enabled': True, 'peer_id': None})
300         if len(persons) != 1:
301             raise PLCAuthenticationFailure, "PasswordAuth: No such account"
302
303         person = persons[0]
304
305         if auth['Username'] == method.api.config.PLC_API_MAINTENANCE_USER:
306             # "Capability" authentication, whatever the hell that was
307             # supposed to mean. It really means, login as the special
308             # "maintenance user" using password authentication. Can
309             # only be used on particular machines (those in a list).
310             sources = method.api.config.PLC_API_MAINTENANCE_SOURCES.split()
311             if method.source is not None and method.source[0] not in sources:
312                 raise PLCAuthenticationFailure, "PasswordAuth: Not allowed to login to maintenance account"
313
314             # Not sure why this is not stored in the DB
315             password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
316
317             if auth['AuthString'] != password:
318                 raise PLCAuthenticationFailure, "PasswordAuth: Maintenance account password verification failed"
319         else:
320             # Compare encrypted plaintext against encrypted password stored in the DB
321             plaintext = auth['AuthString'].encode(method.api.encoding)
322             password = person['password']
323
324             # Protect against blank passwords in the DB
325             if password is None or password[:12] == "" or \
326                crypt.crypt(plaintext, password[:12]) != password:
327                 raise PLCAuthenticationFailure, "PasswordAuth: Password verification failed"
328
329         if not set(person['roles']).intersection(method.roles):
330             method_message="method %s has roles [%s]"%(method.name,','.join(method.roles))
331             person_message="caller %s has roles [%s]"%(person['email'],','.join(person['roles']))
332             raise PLCAuthenticationFailure, "PasswordAuth: missing role, %s -- %s"%(method_message,person_message)
333
334         method.caller = person
335
336 auth_methods = {'session': SessionAuth,
337                 'password': PasswordAuth,
338                 'capability': PasswordAuth,
339                 'gpg': GPGAuth,
340                 'hmac': BootAuth,
341                 'hmac_dummybox': BootAuth,
342                 'anonymous': AnonymousAuth}
343
344 path = os.path.dirname(__file__) + "/Auth.d"
345 try:
346     extensions = os.listdir(path)
347 except OSError, e:
348     extensions = []
349 for extension in extensions:
350     if extension.startswith("."):
351         continue
352     if not extension.endswith(".py"):
353         continue
354     execfile("%s/%s" % (path, extension))
355 del extensions