autopep8
[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.Keys import Keys
25 from PLC.Boot import notify_owners
26
27
28 class Auth(Parameter):
29     """
30     Base class for all API authentication methods, as well as a class
31     that can be used to represent all supported API authentication
32     methods.
33     """
34
35     def __init__(self, auth=None):
36         if auth is None:
37             auth = {'AuthMethod': Parameter(str, "Authentication method to use",
38                                             optional=False)}
39         Parameter.__init__(self, auth, "API authentication structure")
40
41     def check(self, method, auth, *args):
42         global auth_methods
43
44         # Method.type_check() should have checked that all of the
45         # mandatory fields were present.
46         assert 'AuthMethod' in auth
47
48         if auth['AuthMethod'] in auth_methods:
49             expected = auth_methods[auth['AuthMethod']]()
50         else:
51             sm = "'" + "', '".join(list(auth_methods.keys())) + "'"
52             raise PLCInvalidArgument("must be " + sm, "AuthMethod")
53
54         # Re-check using the specified authentication method
55         method.type_check("auth", auth, expected, (auth,) + args)
56
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(
76                         "GPGAuth: Not allowed to call method, missing 'peer' role")
77
78                 method.caller = peer = peers[0]
79                 gpg_keys = [peer['key']]
80             else:
81                 persons = Persons(
82                     method.api, {'email': auth['name'], 'enabled': True, 'peer_id': None})
83                 if not persons:
84                     raise PLCAuthenticationFailure(
85                         "GPGAuth: No such user '%s'" % auth['name'])
86
87                 method.caller = person = persons[0]
88                 if not set(person['roles']).intersection(method.roles):
89                     raise PLCAuthenticationFailure(
90                         "GPGAuth: Not allowed to call method, missing role")
91
92                 keys = Keys(
93                     method.api, {'key_id': person['key_ids'], 'key_type': "gpg", 'peer_id': None})
94                 gpg_keys = [key['key'] for key in keys]
95
96             if not gpg_keys:
97                 raise PLCAuthenticationFailure(
98                     "GPGAuth: No GPG key on record for peer or user '%s'" % auth['name'])
99
100             for gpg_key in gpg_keys:
101                 try:
102                     from PLC.GPG import gpg_verify
103                     gpg_verify(args, gpg_key, auth['signature'], method.name)
104                     return
105                 except PLCAuthenticationFailure as fault:
106                     pass
107
108             raise fault
109
110         except PLCAuthenticationFailure as fault:
111             # XXX Send e-mail
112             raise fault
113
114
115 class SessionAuth(Auth):
116     """
117     Secondary authentication method. After authenticating with a
118     primary authentication method, call GetSession() to generate a
119     session key that may be used for subsequent calls.
120     """
121
122     def __init__(self):
123         Auth.__init__(self, {
124             'AuthMethod': Parameter(str, "Authentication method to use, always 'session'", optional=False),
125             'session': Parameter(str, "Session key", optional=False)
126         })
127
128     def check(self, method, auth, *args):
129         # Method.type_check() should have checked that all of the
130         # mandatory fields were present.
131         assert 'session' in auth
132
133         # Get session record
134         sessions = Sessions(method.api, [auth['session']], expires=None)
135         if not sessions:
136             raise PLCAuthenticationFailure("SessionAuth: No such session")
137         session = sessions[0]
138
139         try:
140             if session['node_id'] is not None:
141                 nodes = Nodes(
142                     method.api, {'node_id': session['node_id'], 'peer_id': None})
143                 if not nodes:
144                     raise PLCAuthenticationFailure("SessionAuth: No such node")
145                 node = nodes[0]
146
147                 if 'node' not in method.roles:
148                     # using PermissionDenied rather than AuthenticationFailure here because
149                     # if that fails we don't want to delete the session..
150                     raise PLCPermissionDenied(
151                         "SessionAuth: Not allowed to call method %s, missing 'node' role" % method.name)
152
153                 method.caller = node
154
155             elif session['person_id'] is not None and session['expires'] > time.time():
156                 persons = Persons(method.api, {
157                                   'person_id': session['person_id'], 'enabled': True, 'peer_id': None})
158                 if not persons:
159                     raise PLCAuthenticationFailure(
160                         "SessionAuth: No such enabled account")
161                 person = persons[0]
162
163                 if not set(person['roles']).intersection(method.roles):
164                     method_message = "method %s has roles [%s]" % (
165                         method.name, ','.join(method.roles))
166                     person_message = "caller %s has roles [%s]" % (
167                         person['email'], ','.join(person['roles']))
168                     # not PLCAuthenticationFailure b/c that would end the session..
169                     raise PLCPermissionDenied(
170                         "SessionAuth: missing role, %s -- %s" % (method_message, person_message))
171
172                 method.caller = person
173
174             else:
175                 raise PLCAuthenticationFailure("SessionAuth: Invalid session")
176
177         except PLCAuthenticationFailure as fault:
178             session.delete()
179             raise fault
180
181
182 class BootAuth(Auth):
183     """
184     PlanetLab version 3.x node authentication structure. Used by the
185     Boot Manager to make authenticated calls to the API based on a
186     unique node key or boot nonce value.
187
188     The original parameter serialization code did not define the byte
189     encoding of strings, or the string encoding of all other types. We
190     define the byte encoding to be UTF-8, and the string encoding of
191     all other types to be however Python version 2.3 unicode() encodes
192     them.
193     """
194
195     def __init__(self):
196         Auth.__init__(self, {
197             'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional=False),
198             'node_id': Parameter(int, "Node identifier", optional=False),
199             'value': Parameter(str, "HMAC of node key and method call", optional=False)
200         })
201
202     def canonicalize(self, args):
203         values = []
204
205         for arg in args:
206             if isinstance(arg, list) or isinstance(arg, tuple):
207                 # The old implementation did not recursively handle
208                 # lists of lists. But neither did the old API itself.
209                 values += self.canonicalize(arg)
210             elif isinstance(arg, dict):
211                 # Yes, the comments in the old implementation are
212                 # misleading. Keys of dicts are not included in the
213                 # hash.
214                 values += self.canonicalize(list(arg.values()))
215             else:
216                 # We use unicode() instead of str().
217                 values.append(str(arg))
218
219         return values
220
221     def check(self, method, auth, *args):
222         # Method.type_check() should have checked that all of the
223         # mandatory fields were present.
224         assert 'node_id' in auth
225
226         if 'node' not in method.roles:
227             raise PLCAuthenticationFailure(
228                 "BootAuth: Not allowed to call method, missing 'node' role")
229
230         try:
231             nodes = Nodes(
232                 method.api, {'node_id': auth['node_id'], 'peer_id': None})
233             if not nodes:
234                 raise PLCAuthenticationFailure("BootAuth: No such node")
235             node = nodes[0]
236
237             # Jan 2011 : removing support for old boot CDs
238             if node['key']:
239                 key = node['key']
240             else:
241                 raise PLCAuthenticationFailure("BootAuth: No node key")
242
243             # Yes, this is the "canonicalization" method used.
244             args = self.canonicalize(args)
245             args.sort()
246             msg = "[" + "".join(args) + "]"
247
248             # We encode in UTF-8 before calculating the HMAC, which is
249             # an 8-bit algorithm.
250             # python 2.6 insists on receiving a 'str' as opposed to a 'unicode'
251             digest = hmac.new(str(key), msg.encode('utf-8'), sha).hexdigest()
252
253             if digest != auth['value']:
254                 raise PLCAuthenticationFailure(
255                     "BootAuth: Call could not be authenticated")
256
257             method.caller = node
258
259         except PLCAuthenticationFailure as fault:
260             if nodes:
261                 notify_owners(method, node, 'authfail',
262                               include_pis=True, include_techs=True, fault=fault)
263             raise fault
264
265
266 class AnonymousAuth(Auth):
267     """
268     PlanetLab version 3.x anonymous authentication structure.
269     """
270
271     def __init__(self):
272         Auth.__init__(self, {
273             'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
274         })
275
276     def check(self, method, auth, *args):
277         if 'anonymous' not in method.roles:
278             raise PLCAuthenticationFailure(
279                 "AnonymousAuth: method cannot be called anonymously")
280
281         method.caller = None
282
283
284 class PasswordAuth(Auth):
285     """
286     PlanetLab version 3.x password authentication structure.
287     """
288
289     def __init__(self):
290         Auth.__init__(self, {
291             'AuthMethod': Parameter(str, "Authentication method to use, always 'password' or 'capability'", optional=False),
292             'Username': Parameter(str, "PlanetLab username, typically an e-mail address", optional=False),
293             'AuthString': Parameter(str, "Authentication string, typically a password", optional=False),
294         })
295
296     def check(self, method, auth, *args):
297         # Method.type_check() should have checked that all of the
298         # mandatory fields were present.
299         assert 'Username' in auth
300
301         # Get record (must be enabled)
302         persons = Persons(method.api, {
303                           'email': auth['Username'].lower(), 'enabled': True, 'peer_id': None})
304         if len(persons) != 1:
305             raise PLCAuthenticationFailure("PasswordAuth: No such account")
306
307         person = persons[0]
308
309         if auth['Username'] == method.api.config.PLC_API_MAINTENANCE_USER:
310             # "Capability" authentication, whatever the hell that was
311             # supposed to mean. It really means, login as the special
312             # "maintenance user" using password authentication. Can
313             # only be used on particular machines (those in a list).
314             sources = method.api.config.PLC_API_MAINTENANCE_SOURCES.split()
315             if method.source is not None and method.source[0] not in sources:
316                 raise PLCAuthenticationFailure(
317                     "PasswordAuth: Not allowed to login to maintenance account")
318
319             # Not sure why this is not stored in the DB
320             password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
321
322             if auth['AuthString'] != password:
323                 raise PLCAuthenticationFailure(
324                     "PasswordAuth: 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(
334                     "PasswordAuth: Password verification failed")
335
336         if not set(person['roles']).intersection(method.roles):
337             method_message = "method %s has roles [%s]" % (
338                 method.name, ','.join(method.roles))
339             person_message = "caller %s has roles [%s]" % (
340                 person['email'], ','.join(person['roles']))
341             raise PLCAuthenticationFailure(
342                 "PasswordAuth: missing role, %s -- %s" % (method_message, person_message))
343
344         method.caller = person
345
346
347 auth_methods = {'session': SessionAuth,
348                 'password': PasswordAuth,
349                 'capability': PasswordAuth,
350                 'gpg': GPGAuth,
351                 'hmac': BootAuth,
352                 'hmac_dummybox': BootAuth,
353                 'anonymous': AnonymousAuth}
354
355 path = os.path.dirname(__file__) + "/Auth.d"
356 try:
357     extensions = os.listdir(path)
358 except OSError as e:
359     extensions = []
360 for extension in extensions:
361     if extension.startswith("."):
362         continue
363     if not extension.endswith(".py"):
364         continue
365     exec(compile(open("%s/%s" % (path, extension)).read(),
366                  "%s/%s" % (path, extension), 'exec'))
367 del extensions