oops again
[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                     # using PermissionDenied rather than AuthenticationFailure here because
136                     # if that fails we don't want to delete the session..
137                     raise PLCPermissionDenied, "SessionAuth: Not allowed to call method %s, missing 'node' role"%method.name
138
139                 method.caller = node
140
141             elif session['person_id'] is not None and session['expires'] > time.time():
142                 persons = Persons(method.api, {'person_id': session['person_id'], 'enabled': True, 'peer_id': None})
143                 if not persons:
144                     raise PLCAuthenticationFailure, "SessionAuth: No such enabled account"
145                 person = persons[0]
146
147                 if not set(person['roles']).intersection(method.roles):
148                     method_message="method %s has roles [%s]"%(method.name,','.join(method.roles))
149                     person_message="caller %s has roles [%s]"%(person['email'],','.join(person['roles']))
150                     # not PLCAuthenticationFailure b/c that would end the session..
151                     raise PLCPermissionDenied, "SessionAuth: missing role, %s -- %s"%(method_message,person_message)
152
153                 method.caller = person
154
155             else:
156                 raise PLCAuthenticationFailure, "SessionAuth: Invalid session"
157
158         except PLCAuthenticationFailure, fault:
159             session.delete()
160             raise fault
161
162 class BootAuth(Auth):
163     """
164     PlanetLab version 3.x node authentication structure. Used by the
165     Boot Manager to make authenticated calls to the API based on a
166     unique node key or boot nonce value.
167
168     The original parameter serialization code did not define the byte
169     encoding of strings, or the string encoding of all other types. We
170     define the byte encoding to be UTF-8, and the string encoding of
171     all other types to be however Python version 2.3 unicode() encodes
172     them.
173     """
174
175     def __init__(self):
176         Auth.__init__(self, {
177             'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional = False),
178             'node_id': Parameter(int, "Node identifier", optional = False),
179             'value': Parameter(str, "HMAC of node key and method call", optional = False)
180             })
181
182     def canonicalize(self, args):
183         values = []
184
185         for arg in args:
186             if isinstance(arg, list) or isinstance(arg, tuple):
187                 # The old implementation did not recursively handle
188                 # lists of lists. But neither did the old API itself.
189                 values += self.canonicalize(arg)
190             elif isinstance(arg, dict):
191                 # Yes, the comments in the old implementation are
192                 # misleading. Keys of dicts are not included in the
193                 # hash.
194                 values += self.canonicalize(arg.values())
195             else:
196                 # We use unicode() instead of str().
197                 values.append(unicode(arg))
198
199         return values
200
201     def check(self, method, auth, *args):
202         # Method.type_check() should have checked that all of the
203         # mandatory fields were present.
204         assert auth.has_key('node_id')
205
206         if 'node' not in method.roles:
207             raise PLCAuthenticationFailure, "BootAuth: Not allowed to call method, missing 'node' role"
208
209         try:
210             nodes = Nodes(method.api, {'node_id': auth['node_id'], 'peer_id': None})
211             if not nodes:
212                 raise PLCAuthenticationFailure, "BootAuth: No such node"
213             node = nodes[0]
214
215             # Jan 2011 : removing support for old boot CDs
216             if node['key']:
217                 key = node['key']
218             else:
219                 raise PLCAuthenticationFailure, "BootAuth: No node key"
220
221             # Yes, this is the "canonicalization" method used.
222             args = self.canonicalize(args)
223             args.sort()
224             msg = "[" + "".join(args) + "]"
225
226             # We encode in UTF-8 before calculating the HMAC, which is
227             # an 8-bit algorithm.
228             # python 2.6 insists on receiving a 'str' as opposed to a 'unicode'
229             digest = hmac.new(str(key), msg.encode('utf-8'), sha).hexdigest()
230
231             if digest != auth['value']:
232                 raise PLCAuthenticationFailure, "BootAuth: Call could not be authenticated"
233
234             method.caller = node
235
236         except PLCAuthenticationFailure, fault:
237             if nodes:
238                 notify_owners(method, node, 'authfail', include_pis = True, include_techs = True, fault = fault)
239             raise fault
240
241 class AnonymousAuth(Auth):
242     """
243     PlanetLab version 3.x anonymous authentication structure.
244     """
245
246     def __init__(self):
247         Auth.__init__(self, {
248             'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
249             })
250
251     def check(self, method, auth, *args):
252         if 'anonymous' not in method.roles:
253             raise PLCAuthenticationFailure, "AnonymousAuth: method cannot be called anonymously"
254
255         method.caller = None
256
257 class PasswordAuth(Auth):
258     """
259     PlanetLab version 3.x password authentication structure.
260     """
261
262     def __init__(self):
263         Auth.__init__(self, {
264             'AuthMethod': Parameter(str, "Authentication method to use, always 'password' or 'capability'", optional = False),
265             'Username': Parameter(str, "PlanetLab username, typically an e-mail address", optional = False),
266             'AuthString': Parameter(str, "Authentication string, typically a password", optional = False),
267             })
268
269     def check(self, method, auth, *args):
270         # Method.type_check() should have checked that all of the
271         # mandatory fields were present.
272         assert auth.has_key('Username')
273
274         # Get record (must be enabled)
275         persons = Persons(method.api, {'email': auth['Username'].lower(), 'enabled': True, 'peer_id': None})
276         if len(persons) != 1:
277             raise PLCAuthenticationFailure, "PasswordAuth: No such account"
278
279         person = persons[0]
280
281         if auth['Username'] == method.api.config.PLC_API_MAINTENANCE_USER:
282             # "Capability" authentication, whatever the hell that was
283             # supposed to mean. It really means, login as the special
284             # "maintenance user" using password authentication. Can
285             # only be used on particular machines (those in a list).
286             sources = method.api.config.PLC_API_MAINTENANCE_SOURCES.split()
287             if method.source is not None and method.source[0] not in sources:
288                 raise PLCAuthenticationFailure, "PasswordAuth: Not allowed to login to maintenance account"
289
290             # Not sure why this is not stored in the DB
291             password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
292
293             if auth['AuthString'] != password:
294                 raise PLCAuthenticationFailure, "PasswordAuth: Maintenance account password verification failed"
295         else:
296             # Compare encrypted plaintext against encrypted password stored in the DB
297             plaintext = auth['AuthString'].encode(method.api.encoding)
298             password = person['password']
299
300             # Protect against blank passwords in the DB
301             if password is None or password[:12] == "" or \
302                crypt.crypt(plaintext, password[:12]) != password:
303                 raise PLCAuthenticationFailure, "PasswordAuth: Password verification failed"
304
305         if not set(person['roles']).intersection(method.roles):
306             method_message="method %s has roles [%s]"%(method.name,','.join(method.roles))
307             person_message="caller %s has roles [%s]"%(person['email'],','.join(person['roles']))
308             raise PLCAuthenticationFailure, "PasswordAuth: missing role, %s -- %s"%(method_message,person_message)
309
310         method.caller = person
311
312 auth_methods = {'session': SessionAuth,
313                 'password': PasswordAuth,
314                 'capability': PasswordAuth,
315                 'gpg': GPGAuth,
316                 'hmac': BootAuth,
317                 'hmac_dummybox': BootAuth,
318                 'anonymous': AnonymousAuth}
319
320 path = os.path.dirname(__file__) + "/Auth.d"
321 try:
322     extensions = os.listdir(path)
323 except OSError, e:
324     extensions = []
325 for extension in extensions:
326     if extension.startswith("."):
327         continue
328     if not extension.endswith(".py"):
329         continue
330     execfile("%s/%s" % (path, extension))
331 del extensions