remove support for boot_nonce and old boot CDs
[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                     raise PLCAuthenticationFailure, "SessionAuth: Not allowed to call method %s, missing 'node' role"%method.name
136
137                 method.caller = node
138
139             elif session['person_id'] is not None and session['expires'] > time.time():
140                 persons = Persons(method.api, {'person_id': session['person_id'], 'enabled': True, 'peer_id': None})
141                 if not persons:
142                     raise PLCAuthenticationFailure, "SessionAuth: No such enabled account"
143                 person = persons[0]
144
145                 if not set(person['roles']).intersection(method.roles):
146                     method_message="method %s has roles [%s]"%(method.name,','.join(method.roles))
147                     person_message="caller %s has roles [%s]"%(person['email'],','.join(person['roles']))
148                     # not PLCAuthenticationFailure b/c that would end the session..
149                     raise PLCPermissionDenied, "SessionAuth: missing role, %s -- %s"%(method_message,person_message)
150
151                 method.caller = person
152
153             else:
154                 raise PLCAuthenticationFailure, "SessionAuth: Invalid session"
155
156         except PLCAuthenticationFailure, fault:
157             session.delete()
158             raise fault
159
160 class BootAuth(Auth):
161     """
162     PlanetLab version 3.x node authentication structure. Used by the
163     Boot Manager to make authenticated calls to the API based on a
164     unique node key or boot nonce value.
165
166     The original parameter serialization code did not define the byte
167     encoding of strings, or the string encoding of all other types. We
168     define the byte encoding to be UTF-8, and the string encoding of
169     all other types to be however Python version 2.3 unicode() encodes
170     them.
171     """
172
173     def __init__(self):
174         Auth.__init__(self, {
175             'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional = False),
176             'node_id': Parameter(int, "Node identifier", optional = False),
177             'value': Parameter(str, "HMAC of node key and method call", optional = False)
178             })
179
180     def canonicalize(self, args):
181         values = []
182
183         for arg in args:
184             if isinstance(arg, list) or isinstance(arg, tuple):
185                 # The old implementation did not recursively handle
186                 # lists of lists. But neither did the old API itself.
187                 values += self.canonicalize(arg)
188             elif isinstance(arg, dict):
189                 # Yes, the comments in the old implementation are
190                 # misleading. Keys of dicts are not included in the
191                 # hash.
192                 values += self.canonicalize(arg.values())
193             else:
194                 # We use unicode() instead of str().
195                 values.append(unicode(arg))
196
197         return values
198
199     def check(self, method, auth, *args):
200         # Method.type_check() should have checked that all of the
201         # mandatory fields were present.
202         assert auth.has_key('node_id')
203
204         if 'node' not in method.roles:
205             raise PLCAuthenticationFailure, "BootAuth: Not allowed to call method, missing 'node' role"
206
207         try:
208             nodes = Nodes(method.api, {'node_id': auth['node_id'], 'peer_id': None})
209             if not nodes:
210                 raise PLCAuthenticationFailure, "BootAuth: No such node"
211             node = nodes[0]
212
213             # Jan 2011 : removing support for old boot CDs
214             if node['key']:
215                 key = node['key']
216             else:
217                 write_debug_line("BootAuth.check: could not get key")
218                 raise PLCAuthenticationFailure, "BootAuth: No node key"
219
220             # Yes, this is the "canonicalization" method used.
221             args = self.canonicalize(args)
222             args.sort()
223             msg = "[" + "".join(args) + "]"
224
225             # We encode in UTF-8 before calculating the HMAC, which is
226             # an 8-bit algorithm.
227             # python 2.6 insists on receiving a 'str' as opposed to a 'unicode'
228             digest = hmac.new(str(key), msg.encode('utf-8'), sha).hexdigest()
229
230             if digest != auth['value']:
231                 raise PLCAuthenticationFailure, "BootAuth: Call could not be authenticated"
232
233             method.caller = node
234
235         except PLCAuthenticationFailure, fault:
236             if nodes:
237                 notify_owners(method, node, 'authfail', include_pis = True, include_techs = True, fault = fault)
238             raise fault
239
240 class AnonymousAuth(Auth):
241     """
242     PlanetLab version 3.x anonymous authentication structure.
243     """
244
245     def __init__(self):
246         Auth.__init__(self, {
247             'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
248             })
249
250     def check(self, method, auth, *args):
251         if 'anonymous' not in method.roles:
252             raise PLCAuthenticationFailure, "AnonymousAuth: method cannot be called anonymously"
253
254         method.caller = None
255
256 class PasswordAuth(Auth):
257     """
258     PlanetLab version 3.x password authentication structure.
259     """
260
261     def __init__(self):
262         Auth.__init__(self, {
263             'AuthMethod': Parameter(str, "Authentication method to use, always 'password' or 'capability'", optional = False),
264             'Username': Parameter(str, "PlanetLab username, typically an e-mail address", optional = False),
265             'AuthString': Parameter(str, "Authentication string, typically a password", optional = False),
266             })
267
268     def check(self, method, auth, *args):
269         # Method.type_check() should have checked that all of the
270         # mandatory fields were present.
271         assert auth.has_key('Username')
272
273         # Get record (must be enabled)
274         persons = Persons(method.api, {'email': auth['Username'].lower(), 'enabled': True, 'peer_id': None})
275         if len(persons) != 1:
276             raise PLCAuthenticationFailure, "PasswordAuth: No such account"
277
278         person = persons[0]
279
280         if auth['Username'] == method.api.config.PLC_API_MAINTENANCE_USER:
281             # "Capability" authentication, whatever the hell that was
282             # supposed to mean. It really means, login as the special
283             # "maintenance user" using password authentication. Can
284             # only be used on particular machines (those in a list).
285             sources = method.api.config.PLC_API_MAINTENANCE_SOURCES.split()
286             if method.source is not None and method.source[0] not in sources:
287                 raise PLCAuthenticationFailure, "PasswordAuth: Not allowed to login to maintenance account"
288
289             # Not sure why this is not stored in the DB
290             password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
291
292             if auth['AuthString'] != password:
293                 raise PLCAuthenticationFailure, "PasswordAuth: Maintenance account password verification failed"
294         else:
295             # Compare encrypted plaintext against encrypted password stored in the DB
296             plaintext = auth['AuthString'].encode(method.api.encoding)
297             password = person['password']
298
299             # Protect against blank passwords in the DB
300             if password is None or password[:12] == "" or \
301                crypt.crypt(plaintext, password[:12]) != password:
302                 raise PLCAuthenticationFailure, "PasswordAuth: Password verification failed"
303
304         if not set(person['roles']).intersection(method.roles):
305             method_message="method %s has roles [%s]"%(method.name,','.join(method.roles))
306             person_message="caller %s has roles [%s]"%(person['email'],','.join(person['roles']))
307             raise PLCAuthenticationFailure, "PasswordAuth: missing role, %s -- %s"%(method_message,person_message)
308
309         method.caller = person
310
311 auth_methods = {'session': SessionAuth,
312                 'password': PasswordAuth,
313                 'capability': PasswordAuth,
314                 'gpg': GPGAuth,
315                 'hmac': BootAuth,
316                 'hmac_dummybox': BootAuth,
317                 'anonymous': AnonymousAuth}
318
319 path = os.path.dirname(__file__) + "/Auth.d"
320 try:
321     extensions = os.listdir(path)
322 except OSError, e:
323     extensions = []
324 for extension in extensions:
325     if extension.startswith("."):
326         continue
327     if not extension.endswith(".py"):
328         continue
329     execfile("%s/%s" % (path, extension))
330 del extensions