Merge remote-tracking branch 'origin/pycurl' into planetlab-4_0-branch
[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 5574 2007-10-25 20:33:17Z thierry $
8 #
9
10 import crypt
11 import sha
12 import hmac
13 import time
14
15 from PLC.Faults import *
16 from PLC.Parameter import Parameter, Mixed
17 from PLC.Persons import Persons
18 from PLC.Nodes import Node, Nodes
19 from PLC.NodeNetworks import NodeNetwork, NodeNetworks
20 from PLC.Sessions import Session, Sessions
21 from PLC.Peers import Peer, Peers
22 from PLC.Boot import notify_owners
23
24 class Auth(Parameter):
25     """
26     Base class for all API authentication methods, as well as a class
27     that can be used to represent all supported API authentication
28     methods.
29     """
30
31     def __init__(self, auth = None):
32         if auth is None:
33             auth = {'AuthMethod': Parameter(str, "Authentication method to use", optional = False)}
34         Parameter.__init__(self, auth, "API authentication structure")
35
36     def check(self, method, auth, *args):
37         # Method.type_check() should have checked that all of the
38         # mandatory fields were present.
39         assert 'AuthMethod' in auth
40
41         if auth['AuthMethod'] == "session":
42             expected = SessionAuth()
43         elif auth['AuthMethod'] == "password" or \
44              auth['AuthMethod'] == "capability":
45             expected = PasswordAuth()
46         elif auth['AuthMethod'] == "gpg":
47             expected = GPGAuth()
48         elif auth['AuthMethod'] == "hmac":
49             expected = BootAuth()
50         elif auth['AuthMethod'] == "anonymous":
51             expected = AnonymousAuth()
52         else:
53             raise PLCInvalidArgument("must be 'session', 'password', 'gpg', 'hmac', or 'anonymous'", "AuthMethod")
54
55         # Re-check using the specified authentication method
56         method.type_check("auth", auth, expected, (auth,) + args)
57
58 class GPGAuth(Auth):
59     """
60     Proposed PlanetLab federation authentication structure.
61     """
62
63     def __init__(self):
64         Auth.__init__(self, {
65             'AuthMethod': Parameter(str, "Authentication method to use, always 'gpg'", optional = False),
66             'name': Parameter(str, "Peer or user name", optional = False),
67             'signature': Parameter(str, "Message signature", optional = False)
68             })
69
70     def check(self, method, auth, *args):
71         try:
72             peers = Peers(method.api, [auth['name']])
73             if peers:
74                 if 'peer' not in method.roles:
75                     raise PLCAuthenticationFailure, "Not allowed to call method"
76
77                 method.caller = peer = peers[0]
78                 keys = [peer['key']]
79             else:
80                 persons = Persons(method.api, {'email': auth['name'], 'enabled': True, 'peer_id': None})
81                 if not persons:
82                     raise PLCAuthenticationFailure, "No such user '%s'" % auth['name']
83
84                 if not set(person['roles']).intersection(method.roles):
85                     raise PLCAuthenticationFailure, "Not allowed to call method"
86
87                 method.caller = person = persons[0]
88                 keys = Keys(method.api, {'key_id': person['key_ids'], 'key_type': "gpg", 'peer_id': None})
89
90             if not keys:
91                 raise PLCAuthenticationFailure, "No GPG key on record for peer or user '%s'"
92
93             for key in keys:
94                 try:
95                     from PLC.GPG import gpg_verify
96                     gpg_verify(args, key, auth['signature'], method.name)
97                     return
98                 except PLCAuthenticationFailure, fault:
99                     pass
100
101             raise fault
102
103         except PLCAuthenticationFailure, fault:
104             # XXX Send e-mail
105             raise fault
106
107 class SessionAuth(Auth):
108     """
109     Secondary authentication method. After authenticating with a
110     primary authentication method, call GetSession() to generate a
111     session key that may be used for subsequent calls.
112     """
113
114     def __init__(self):
115         Auth.__init__(self, {
116             'AuthMethod': Parameter(str, "Authentication method to use, always 'session'", optional = False),
117             'session': Parameter(str, "Session key", optional = False)
118             })
119
120     def check(self, method, auth, *args):
121         # Method.type_check() should have checked that all of the
122         # mandatory fields were present.
123         assert auth.has_key('session')
124
125         # Get session record
126         sessions = Sessions(method.api, [auth['session']], expires = None)
127         if not sessions:
128             raise PLCAuthenticationFailure, "No such session"
129         session = sessions[0]
130
131         try:
132             if session['node_id'] is not None:
133                 nodes = Nodes(method.api, {'node_id': session['node_id'], 'peer_id': None})
134                 if not nodes:
135                     raise PLCAuthenticationFailure, "No such node"
136                 node = nodes[0]
137
138                 if 'node' not in method.roles:
139                     raise PLCAuthenticationFailure, "Not allowed to call method"
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, "No such account"
147                 person = persons[0]
148
149                 if not set(person['roles']).intersection(method.roles):
150                     raise PLCPermissionDenied, "Not allowed to call method"
151
152                 method.caller = persons[0]
153
154             else:
155                 raise PLCAuthenticationFailure, "Invalid session"
156
157         except PLCAuthenticationFailure, fault:
158             session.delete()
159             raise fault
160
161 class BootAuth(Auth):
162     """
163     PlanetLab version 3.x node authentication structure. Used by the
164     Boot Manager to make authenticated calls to the API based on a
165     unique node key or boot nonce value.
166
167     The original parameter serialization code did not define the byte
168     encoding of strings, or the string encoding of all other types. We
169     define the byte encoding to be UTF-8, and the string encoding of
170     all other types to be however Python version 2.3 unicode() encodes
171     them.
172     """
173
174     def __init__(self):
175         Auth.__init__(self, {
176             'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional = False),
177             'node_id': Parameter(int, "Node identifier", optional = False),
178             'value': Parameter(str, "HMAC of node key and method call", optional = False)
179             })
180
181     def canonicalize(self, args):
182         values = []
183
184         for arg in args:
185             if isinstance(arg, list) or isinstance(arg, tuple):
186                 # The old implementation did not recursively handle
187                 # lists of lists. But neither did the old API itself.
188                 values += self.canonicalize(arg)
189             elif isinstance(arg, dict):
190                 # Yes, the comments in the old implementation are
191                 # misleading. Keys of dicts are not included in the
192                 # hash.
193                 values += self.canonicalize(arg.values())
194             else:
195                 # We use unicode() instead of str().
196                 values.append(unicode(arg))
197
198         return values
199
200     def check(self, method, auth, *args):
201         # Method.type_check() should have checked that all of the
202         # mandatory fields were present.
203         assert auth.has_key('node_id')
204
205         if 'node' not in method.roles:
206             raise PLCAuthenticationFailure, "Not allowed to call method"
207
208         try:
209             nodes = Nodes(method.api, {'node_id': auth['node_id'], 'peer_id': None})
210             if not nodes:
211                 raise PLCAuthenticationFailure, "No such node"
212             node = nodes[0]
213
214             if node['key']:
215                 key = node['key']
216             elif node['boot_nonce']:
217                 # Allow very old nodes that do not have a node key in
218                 # their configuration files to use their "boot nonce"
219                 # instead. The boot nonce is a random value generated
220                 # by the node itself and POSTed by the Boot CD when it
221                 # requests the Boot Manager. This is obviously not
222                 # very secure, so we only allow it to be used if the
223                 # requestor IP is the same as the IP address we have
224                 # on record for the node.
225                 key = node['boot_nonce']
226
227                 nodenetwork = None
228                 if node['nodenetwork_ids']:
229                     nodenetworks = NodeNetworks(method.api, node['nodenetwork_ids'])
230                     for nodenetwork in nodenetworks:
231                         if nodenetwork['is_primary']:
232                             break
233             
234                 if not nodenetwork or not nodenetwork['is_primary']:
235                     raise PLCAuthenticationFailure, "No primary network interface on record"
236             
237                 if method.source is None:
238                     raise PLCAuthenticationFailure, "Cannot determine IP address of requestor"
239
240                 if nodenetwork['ip'] != method.source[0]:
241                     raise PLCAuthenticationFailure, "Requestor IP %s does not match node IP %s" % \
242                           (method.source[0], nodenetwork['ip'])
243             else:
244                 raise PLCAuthenticationFailure, "No node key or boot nonce"
245
246             # Yes, this is the "canonicalization" method used.
247             args = self.canonicalize(args)
248             args.sort()
249             msg = "[" + "".join(args) + "]"
250
251             # We encode in UTF-8 before calculating the HMAC, which is
252             # an 8-bit algorithm.
253             digest = hmac.new(key, msg.encode('utf-8'), sha).hexdigest()
254
255             if digest != auth['value']:
256                 raise PLCAuthenticationFailure, "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, "Not allowed to call method 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, "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, "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, "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, "Password verification failed"
328
329         if not set(person['roles']).intersection(method.roles):
330             raise PLCAuthenticationFailure, "Not allowed to call method"
331
332         method.caller = person