svn keywords
[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             digest = hmac.new(key, msg.encode('utf-8'), sha).hexdigest()
260
261             if digest != auth['value']:
262                 raise PLCAuthenticationFailure, "Call could not be authenticated"
263
264             method.caller = node
265
266         except PLCAuthenticationFailure, fault:
267             if nodes:
268                 notify_owners(method, node, 'authfail', include_pis = True, include_techs = True, fault = fault)
269             raise fault
270
271 class AnonymousAuth(Auth):
272     """
273     PlanetLab version 3.x anonymous authentication structure.
274     """
275
276     def __init__(self):
277         Auth.__init__(self, {
278             'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
279             })
280
281     def check(self, method, auth, *args):
282         if 'anonymous' not in method.roles:
283             raise PLCAuthenticationFailure, "Not allowed to call method anonymously"
284
285         method.caller = None
286
287 class PasswordAuth(Auth):
288     """
289     PlanetLab version 3.x password authentication structure.
290     """
291
292     def __init__(self):
293         Auth.__init__(self, {
294             'AuthMethod': Parameter(str, "Authentication method to use, always 'password' or 'capability'", optional = False),
295             'Username': Parameter(str, "PlanetLab username, typically an e-mail address", optional = False),
296             'AuthString': Parameter(str, "Authentication string, typically a password", optional = False),
297             })
298
299     def check(self, method, auth, *args):
300         # Method.type_check() should have checked that all of the
301         # mandatory fields were present.
302         assert auth.has_key('Username')
303
304         # Get record (must be enabled)
305         persons = Persons(method.api, {'email': auth['Username'].lower(), 'enabled': True, 'peer_id': None})
306         if len(persons) != 1:
307             raise PLCAuthenticationFailure, "No such account"
308
309         person = persons[0]
310
311         if auth['Username'] == method.api.config.PLC_API_MAINTENANCE_USER:
312             # "Capability" authentication, whatever the hell that was
313             # supposed to mean. It really means, login as the special
314             # "maintenance user" using password authentication. Can
315             # only be used on particular machines (those in a list).
316             sources = method.api.config.PLC_API_MAINTENANCE_SOURCES.split()
317             if method.source is not None and method.source[0] not in sources:
318                 raise PLCAuthenticationFailure, "Not allowed to login to maintenance account"
319
320             # Not sure why this is not stored in the DB
321             password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
322
323             if auth['AuthString'] != password:
324                 raise PLCAuthenticationFailure, "Maintenance account password verification failed"
325         else:
326             # Compare encrypted plaintext against encrypted password stored in the DB
327             plaintext = auth['AuthString'].encode(method.api.encoding)
328             password = person['password']
329
330             # Protect against blank passwords in the DB
331             if password is None or password[:12] == "" or \
332                crypt.crypt(plaintext, password[:12]) != password:
333                 raise PLCAuthenticationFailure, "Password verification failed"
334
335         if not set(person['roles']).intersection(method.roles):
336             raise PLCAuthenticationFailure, "Not allowed to call method"
337
338         method.caller = person