do not delete session when node is trying to call a method that it is not allowed to
[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                     raise PLCAuthenticationFailure, "SessionAuth: Not allowed to call method %s, missing 'node' role"%method.name
139
140                 method.caller = node
141
142             elif session['person_id'] is not None and session['expires'] > time.time():
143                 persons = Persons(method.api, {'person_id': session['person_id'], 'enabled': True, 'peer_id': None})
144                 if not persons:
145                     raise PLCAuthenticationFailure, "SessionAuth: No such enabled account"
146                 person = persons[0]
147
148                 if not set(person['roles']).intersection(method.roles):
149                     method_message="method %s has roles [%s]"%(method.name,','.join(method.roles))
150                     person_message="caller %s has roles [%s]"%(person['email'],','.join(person['roles']))
151                     # not PLCAuthenticationFailure b/c that would end the session..
152                     raise PLCPermissionDenied, "SessionAuth: missing role, %s -- %s"%(method_message,person_message)
153
154                 method.caller = person
155
156             else:
157                 raise PLCAuthenticationFailure, "SessionAuth: Invalid session"
158
159         except PLCAuthenticationFailure, fault:
160             session.delete()
161             raise fault
162
163 class BootAuth(Auth):
164     """
165     PlanetLab version 3.x node authentication structure. Used by the
166     Boot Manager to make authenticated calls to the API based on a
167     unique node key or boot nonce value.
168
169     The original parameter serialization code did not define the byte
170     encoding of strings, or the string encoding of all other types. We
171     define the byte encoding to be UTF-8, and the string encoding of
172     all other types to be however Python version 2.3 unicode() encodes
173     them.
174     """
175
176     def __init__(self):
177         Auth.__init__(self, {
178             'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional = False),
179             'node_id': Parameter(int, "Node identifier", optional = False),
180             'value': Parameter(str, "HMAC of node key and method call", optional = False)
181             })
182
183     def canonicalize(self, args):
184         values = []
185
186         for arg in args:
187             if isinstance(arg, list) or isinstance(arg, tuple):
188                 # The old implementation did not recursively handle
189                 # lists of lists. But neither did the old API itself.
190                 values += self.canonicalize(arg)
191             elif isinstance(arg, dict):
192                 # Yes, the comments in the old implementation are
193                 # misleading. Keys of dicts are not included in the
194                 # hash.
195                 values += self.canonicalize(arg.values())
196             else:
197                 # We use unicode() instead of str().
198                 values.append(unicode(arg))
199
200         return values
201
202     def check(self, method, auth, *args):
203         # Method.type_check() should have checked that all of the
204         # mandatory fields were present.
205         assert auth.has_key('node_id')
206
207         if 'node' not in method.roles:
208             raise PLCAuthenticationFailure, "BootAuth: Not allowed to call method, missing 'node' role"
209
210         try:
211             nodes = Nodes(method.api, {'node_id': auth['node_id'], 'peer_id': None})
212             if not nodes:
213                 raise PLCAuthenticationFailure, "BootAuth: No such node"
214             node = nodes[0]
215
216             # Jan 2011 : removing support for old boot CDs
217             if node['key']:
218                 key = node['key']
219             else:
220                 write_debug_line("BootAuth.check: could not get key")
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