2 # PLCAPI authentication parameters
4 # Mark Huang <mlhuang@cs.princeton.edu>
5 # Copyright (C) 2006 The Trustees of Princeton University
9 from hashlib import sha1 as sha
14 from PLC.Faults import *
15 from PLC.Parameter import Parameter, Mixed
16 from PLC.Persons import Persons
17 from PLC.Nodes import Node, Nodes
18 from PLC.Interfaces import Interface, Interfaces
19 from PLC.Sessions import Session, Sessions
20 from PLC.Peers import Peer, Peers
21 from PLC.Keys import Keys
22 from PLC.Boot import notify_owners
23 from PLC.Logger import logger
25 class Auth(Parameter):
27 Base class for all API authentication methods, as well as a class
28 that can be used to represent all supported API authentication
32 def __init__(self, auth=None):
34 auth = {'AuthMethod': Parameter(str, "Authentication method to use",
36 Parameter.__init__(self, auth, "API authentication structure")
38 def check(self, method, auth, *args):
41 # Method.type_check() should have checked that all of the
42 # mandatory fields were present.
43 assert 'AuthMethod' in auth
45 if auth['AuthMethod'] in auth_methods:
46 expected = auth_methods[auth['AuthMethod']]()
48 sm = "'" + "', '".join(list(auth_methods.keys())) + "'"
49 raise PLCInvalidArgument("must be " + sm, "AuthMethod")
51 # Re-check using the specified authentication method
52 method.type_check("auth", auth, expected, (auth,) + args)
57 Proposed PlanetLab federation authentication structure.
62 'AuthMethod': Parameter(str, "Authentication method to use, always 'gpg'", optional=False),
63 'name': Parameter(str, "Peer or user name", optional=False),
64 'signature': Parameter(str, "Message signature", optional=False)
67 def check(self, method, auth, *args):
69 peers = Peers(method.api, [auth['name']])
71 if 'peer' not in method.roles:
72 raise PLCAuthenticationFailure(
73 "GPGAuth: Not allowed to call method, missing 'peer' role")
75 method.caller = peer = peers[0]
76 gpg_keys = [peer['key']]
79 method.api, {'email': auth['name'], 'enabled': True, 'peer_id': None})
81 raise PLCAuthenticationFailure(
82 "GPGAuth: No such user '%s'" % auth['name'])
84 method.caller = person = persons[0]
85 if not set(person['roles']).intersection(method.roles):
86 raise PLCAuthenticationFailure(
87 "GPGAuth: Not allowed to call method, missing role")
90 method.api, {'key_id': person['key_ids'], 'key_type': "gpg", 'peer_id': None})
91 gpg_keys = [key['key'] for key in keys]
94 raise PLCAuthenticationFailure(
95 "GPGAuth: No GPG key on record for peer or user '%s'" % auth['name'])
97 for gpg_key in gpg_keys:
99 from PLC.GPG import gpg_verify
100 gpg_verify(args, gpg_key, auth['signature'], method.name)
102 except PLCAuthenticationFailure as fault:
107 except PLCAuthenticationFailure as fault:
112 class SessionAuth(Auth):
114 Secondary authentication method. After authenticating with a
115 primary authentication method, call GetSession() to generate a
116 session key that may be used for subsequent calls.
120 Auth.__init__(self, {
121 'AuthMethod': Parameter(str, "Authentication method to use, always 'session'", optional=False),
122 'session': Parameter(str, "Session key", optional=False)
125 def check(self, method, auth, *args):
126 # Method.type_check() should have checked that all of the
127 # mandatory fields were present.
128 assert 'session' in auth
131 sessions = Sessions(method.api, [auth['session']], expires=None)
133 raise PLCAuthenticationFailure("SessionAuth: No such session")
134 session = sessions[0]
137 if session['node_id'] is not None:
139 method.api, {'node_id': session['node_id'], 'peer_id': None})
141 raise PLCAuthenticationFailure("SessionAuth: No such node")
144 if 'node' not in method.roles:
145 # using PermissionDenied rather than AuthenticationFailure here because
146 # if that fails we don't want to delete the session..
147 raise PLCPermissionDenied(
148 "SessionAuth: Not allowed to call method %s, missing 'node' role" % method.name)
152 elif session['person_id'] is not None and session['expires'] > time.time():
153 persons = Persons(method.api, {
154 'person_id': session['person_id'], 'enabled': True, 'peer_id': None})
156 raise PLCAuthenticationFailure(
157 "SessionAuth: No such enabled account")
160 if not set(person['roles']).intersection(method.roles):
161 method_message = "method %s has roles [%s]" % (
162 method.name, ','.join(method.roles))
163 person_message = "caller %s has roles [%s]" % (
164 person['email'], ','.join(person['roles']))
165 # not PLCAuthenticationFailure b/c that would end the session..
166 raise PLCPermissionDenied(
167 "SessionAuth: missing role, %s -- %s" % (method_message, person_message))
169 method.caller = person
172 raise PLCAuthenticationFailure("SessionAuth: Invalid session")
174 except PLCAuthenticationFailure as fault:
179 class BootAuth(Auth):
181 PlanetLab version 3.x node authentication structure. Used by the
182 Boot Manager to make authenticated calls to the API based on a
183 unique node key or boot nonce value.
187 Auth.__init__(self, {
188 'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional=False),
189 'node_id': Parameter(int, "Node identifier", optional=False),
190 'value': Parameter(str, "HMAC of node key and method call", optional=False)
193 def canonicalize(self, args):
197 if isinstance(arg, list) or isinstance(arg, tuple):
198 # The old implementation did not recursively handle
199 # lists of lists. But neither did the old API itself.
200 values += self.canonicalize(arg)
201 elif isinstance(arg, dict):
202 # Yes, the comments in the old implementation are
203 # misleading. Keys of dicts are not included in the
205 values += self.canonicalize(list(arg.values()))
207 # We use unicode() instead of str().
208 values.append(str(arg))
212 def check(self, method, auth, *args):
213 # Method.type_check() should have checked that all of the
214 # mandatory fields were present.
215 assert 'node_id' in auth
217 if 'node' not in method.roles:
218 raise PLCAuthenticationFailure(
219 "BootAuth: Not allowed to call method, missing 'node' role")
223 method.api, {'node_id': auth['node_id'], 'peer_id': None})
225 raise PLCAuthenticationFailure("BootAuth: No such node")
228 # Jan 2011 : removing support for old boot CDs
232 raise PLCAuthenticationFailure("BootAuth: No node key")
234 # Yes, this is the "canonicalization" method used.
235 args = self.canonicalize(args)
237 msg = "[" + "".join(args) + "]"
239 digest = hmac.new(key.encode('utf-8'), msg.encode('utf-8'), sha).hexdigest()
241 if digest != auth['value']:
242 raise PLCAuthenticationFailure(
243 "BootAuth: Call could not be authenticated")
247 except PLCAuthenticationFailure as fault:
249 notify_owners(method, node, 'authfail',
250 include_pis=True, include_techs=True, fault=fault)
254 class AnonymousAuth(Auth):
256 PlanetLab version 3.x anonymous authentication structure.
260 Auth.__init__(self, {
261 'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
264 def check(self, method, auth, *args):
265 if 'anonymous' not in method.roles:
266 raise PLCAuthenticationFailure(
267 "AnonymousAuth: method cannot be called anonymously")
272 class PasswordAuth(Auth):
274 PlanetLab version 3.x password authentication structure.
278 Auth.__init__(self, {
279 'AuthMethod': Parameter(str, "Authentication method to use, always 'password' or 'capability'", optional=False),
280 'Username': Parameter(str, "PlanetLab username, typically an e-mail address", optional=False),
281 'AuthString': Parameter(str, "Authentication string, typically a password", optional=False),
284 def check(self, method, auth, *args):
285 # Method.type_check() should have checked that all of the
286 # mandatory fields were present.
287 assert 'Username' in auth
289 # Get record (must be enabled)
290 normalized = auth['Username'].lower()
291 persons = Persons(method.api, {
292 'email': normalized, 'enabled': True, 'peer_id': None})
293 if len(persons) != 1:
294 logger.info(f"PasswordAuth failed with {normalized}, got {len(persons)} matches")
295 raise PLCAuthenticationFailure("PasswordAuth: No such account")
299 if auth['Username'] == method.api.config.PLC_API_MAINTENANCE_USER:
300 # "Capability" authentication, whatever the hell that was
301 # supposed to mean. It really means, login as the special
302 # "maintenance user" using password authentication. Can
303 # only be used on particular machines (those in a list).
304 sources = method.api.config.PLC_API_MAINTENANCE_SOURCES.split()
305 if method.source is not None and method.source[0] not in sources:
306 raise PLCAuthenticationFailure(
307 "PasswordAuth: Not allowed to login to maintenance account")
309 # Not sure why this is not stored in the DB
310 password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
312 if auth['AuthString'] != password:
313 raise PLCAuthenticationFailure(
314 "PasswordAuth: Maintenance account password verification failed")
316 # Compare encrypted plaintext against encrypted password stored in the DB
317 plaintext = auth['AuthString']
318 password = person['password']
320 # Protect against blank passwords in the DB
321 if password is None or password[:12] == "" or \
322 crypt.crypt(plaintext, password[:12]) != password:
323 raise PLCAuthenticationFailure(
324 "PasswordAuth: Password verification failed")
326 if not set(person['roles']).intersection(method.roles):
327 method_message = "method %s has roles [%s]" % (
328 method.name, ','.join(method.roles))
329 person_message = "caller %s has roles [%s]" % (
330 person['email'], ','.join(person['roles']))
331 raise PLCAuthenticationFailure(
332 "PasswordAuth: missing role, %s -- %s" % (method_message, person_message))
334 method.caller = person
337 auth_methods = {'session': SessionAuth,
338 'password': PasswordAuth,
339 'capability': PasswordAuth,
342 'hmac_dummybox': BootAuth,
343 'anonymous': AnonymousAuth}
345 path = os.path.dirname(__file__) + "/Auth.d"
347 extensions = os.listdir(path)
350 for extension in extensions:
351 if extension.startswith("."):
353 if not extension.endswith(".py"):
355 exec(compile(open("%s/%s" % (path, extension)).read(),
356 "%s/%s" % (path, extension), 'exec'))