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