2 # PLCAPI authentication parameters
4 # Mark Huang <mlhuang@cs.princeton.edu>
5 # Copyright (C) 2006 The Trustees of Princeton University
10 from hashlib import sha1 as sha
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 from PLC.Logger import logger
28 class Auth(Parameter):
30 Base class for all API authentication methods, as well as a class
31 that can be used to represent all supported API authentication
35 def __init__(self, auth=None):
37 auth = {'AuthMethod': Parameter(str, "Authentication method to use",
39 Parameter.__init__(self, auth, "API authentication structure")
41 def check(self, method, auth, *args):
44 # Method.type_check() should have checked that all of the
45 # mandatory fields were present.
46 assert 'AuthMethod' in auth
48 if auth['AuthMethod'] in auth_methods:
49 expected = auth_methods[auth['AuthMethod']]()
51 sm = "'" + "', '".join(list(auth_methods.keys())) + "'"
52 raise PLCInvalidArgument("must be " + sm, "AuthMethod")
54 # Re-check using the specified authentication method
55 method.type_check("auth", auth, expected, (auth,) + args)
60 Proposed PlanetLab federation authentication structure.
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)
70 def check(self, method, auth, *args):
72 peers = Peers(method.api, [auth['name']])
74 if 'peer' not in method.roles:
75 raise PLCAuthenticationFailure(
76 "GPGAuth: Not allowed to call method, missing 'peer' role")
78 method.caller = peer = peers[0]
79 gpg_keys = [peer['key']]
82 method.api, {'email': auth['name'], 'enabled': True, 'peer_id': None})
84 raise PLCAuthenticationFailure(
85 "GPGAuth: No such user '%s'" % auth['name'])
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")
93 method.api, {'key_id': person['key_ids'], 'key_type': "gpg", 'peer_id': None})
94 gpg_keys = [key['key'] for key in keys]
97 raise PLCAuthenticationFailure(
98 "GPGAuth: No GPG key on record for peer or user '%s'" % auth['name'])
100 for gpg_key in gpg_keys:
102 from PLC.GPG import gpg_verify
103 gpg_verify(args, gpg_key, auth['signature'], method.name)
105 except PLCAuthenticationFailure as fault:
110 except PLCAuthenticationFailure as fault:
115 class SessionAuth(Auth):
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.
123 Auth.__init__(self, {
124 'AuthMethod': Parameter(str, "Authentication method to use, always 'session'", optional=False),
125 'session': Parameter(str, "Session key", optional=False)
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
134 sessions = Sessions(method.api, [auth['session']], expires=None)
136 raise PLCAuthenticationFailure("SessionAuth: No such session")
137 session = sessions[0]
140 if session['node_id'] is not None:
142 method.api, {'node_id': session['node_id'], 'peer_id': None})
144 raise PLCAuthenticationFailure("SessionAuth: No such node")
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)
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})
159 raise PLCAuthenticationFailure(
160 "SessionAuth: No such enabled account")
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))
172 method.caller = person
175 raise PLCAuthenticationFailure("SessionAuth: Invalid session")
177 except PLCAuthenticationFailure as fault:
182 class BootAuth(Auth):
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.
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
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)
202 def canonicalize(self, 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
214 values += self.canonicalize(list(arg.values()))
216 # We use unicode() instead of str().
217 values.append(str(arg))
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
226 if 'node' not in method.roles:
227 raise PLCAuthenticationFailure(
228 "BootAuth: Not allowed to call method, missing 'node' role")
232 method.api, {'node_id': auth['node_id'], 'peer_id': None})
234 raise PLCAuthenticationFailure("BootAuth: No such node")
237 # Jan 2011 : removing support for old boot CDs
241 raise PLCAuthenticationFailure("BootAuth: No node key")
243 # Yes, this is the "canonicalization" method used.
244 args = self.canonicalize(args)
246 msg = "[" + "".join(args) + "]"
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()
253 if digest != auth['value']:
254 raise PLCAuthenticationFailure(
255 "BootAuth: Call could not be authenticated")
259 except PLCAuthenticationFailure as fault:
261 notify_owners(method, node, 'authfail',
262 include_pis=True, include_techs=True, fault=fault)
266 class AnonymousAuth(Auth):
268 PlanetLab version 3.x anonymous authentication structure.
272 Auth.__init__(self, {
273 'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
276 def check(self, method, auth, *args):
277 if 'anonymous' not in method.roles:
278 raise PLCAuthenticationFailure(
279 "AnonymousAuth: method cannot be called anonymously")
284 class PasswordAuth(Auth):
286 PlanetLab version 3.x password authentication structure.
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),
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
301 # Get record (must be enabled)
302 normalized = auth['Username'].lower()
303 persons = Persons(method.api, {
304 'email': normalized, 'enabled': True, 'peer_id': None})
305 if len(persons) != 1:
306 logger.info(f"PasswordAuth failed with {normalized}, got {len(persons)} matches")
307 raise PLCAuthenticationFailure("PasswordAuth: No such account")
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(
319 "PasswordAuth: Not allowed to login to maintenance account")
321 # Not sure why this is not stored in the DB
322 password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
324 if auth['AuthString'] != password:
325 raise PLCAuthenticationFailure(
326 "PasswordAuth: Maintenance account password verification failed")
328 # Compare encrypted plaintext against encrypted password stored in the DB
329 plaintext = auth['AuthString']
330 password = person['password']
332 # Protect against blank passwords in the DB
333 if password is None or password[:12] == "" or \
334 crypt.crypt(plaintext, password[:12]) != password:
335 raise PLCAuthenticationFailure(
336 "PasswordAuth: Password verification failed")
338 if not set(person['roles']).intersection(method.roles):
339 method_message = "method %s has roles [%s]" % (
340 method.name, ','.join(method.roles))
341 person_message = "caller %s has roles [%s]" % (
342 person['email'], ','.join(person['roles']))
343 raise PLCAuthenticationFailure(
344 "PasswordAuth: missing role, %s -- %s" % (method_message, person_message))
346 method.caller = person
349 auth_methods = {'session': SessionAuth,
350 'password': PasswordAuth,
351 'capability': PasswordAuth,
354 'hmac_dummybox': BootAuth,
355 'anonymous': AnonymousAuth}
357 path = os.path.dirname(__file__) + "/Auth.d"
359 extensions = os.listdir(path)
362 for extension in extensions:
363 if extension.startswith("."):
365 if not extension.endswith(".py"):
367 exec(compile(open("%s/%s" % (path, extension)).read(),
368 "%s/%s" % (path, extension), 'exec'))