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