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
27 class Auth(Parameter):
29 Base class for all API authentication methods, as well as a class
30 that can be used to represent all supported API authentication
34 def __init__(self, auth = None):
36 auth = {'AuthMethod': Parameter(str, "Authentication method to use", optional = False)}
37 Parameter.__init__(self, auth, "API authentication structure")
39 def check(self, method, auth, *args):
42 # Method.type_check() should have checked that all of the
43 # mandatory fields were present.
44 assert 'AuthMethod' in auth
46 if auth['AuthMethod'] in auth_methods:
47 expected = auth_methods[auth['AuthMethod']]()
49 sm = "'" + "', '".join(auth_methods.keys()) + "'"
50 raise PLCInvalidArgument("must be " + sm, "AuthMethod")
52 # Re-check using the specified authentication method
53 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, "GPGAuth: Not allowed to call method, missing 'peer' role"
74 method.caller = peer = peers[0]
75 gpg_keys = [ peer['key'] ]
77 persons = Persons(method.api, {'email': auth['name'], 'enabled': True, 'peer_id': None})
79 raise PLCAuthenticationFailure, "GPGAuth: No such user '%s'" % auth['name']
81 method.caller = person = persons[0]
82 if not set(person['roles']).intersection(method.roles):
83 raise PLCAuthenticationFailure, "GPGAuth: Not allowed to call method, missing role"
85 keys = Keys(method.api, {'key_id': person['key_ids'], 'key_type': "gpg", 'peer_id': None})
86 gpg_keys = [ key['key'] for key in keys ]
89 raise PLCAuthenticationFailure, "GPGAuth: No GPG key on record for peer or user '%s'"%auth['name']
91 for gpg_key in gpg_keys:
93 from PLC.GPG import gpg_verify
94 gpg_verify(args, gpg_key, auth['signature'], method.name)
96 except PLCAuthenticationFailure, fault:
101 except PLCAuthenticationFailure, fault:
105 class SessionAuth(Auth):
107 Secondary authentication method. After authenticating with a
108 primary authentication method, call GetSession() to generate a
109 session key that may be used for subsequent calls.
113 Auth.__init__(self, {
114 'AuthMethod': Parameter(str, "Authentication method to use, always 'session'", optional = False),
115 'session': Parameter(str, "Session key", optional = False)
118 def check(self, method, auth, *args):
119 # Method.type_check() should have checked that all of the
120 # mandatory fields were present.
121 assert auth.has_key('session')
124 sessions = Sessions(method.api, [auth['session']], expires = None)
126 raise PLCAuthenticationFailure, "SessionAuth: No such session"
127 session = sessions[0]
130 if session['node_id'] is not None:
131 nodes = Nodes(method.api, {'node_id': session['node_id'], 'peer_id': None})
133 raise PLCAuthenticationFailure, "SessionAuth: No such node"
136 if 'node' not in method.roles:
137 # using PermissionDenied rather than AuthenticationFailure here because
138 # if that fails we don't want to delete the session..
139 raise PLCPermissionDenied, "SessionAuth: Not allowed to call method %s, missing 'node' role"%method.name
143 elif session['person_id'] is not None and session['expires'] > time.time():
144 persons = Persons(method.api, {'person_id': session['person_id'], 'enabled': True, 'peer_id': None})
146 raise PLCAuthenticationFailure, "SessionAuth: No such enabled account"
149 if not set(person['roles']).intersection(method.roles):
150 method_message="method %s has roles [%s]"%(method.name,','.join(method.roles))
151 person_message="caller %s has roles [%s]"%(person['email'],','.join(person['roles']))
152 # not PLCAuthenticationFailure b/c that would end the session..
153 raise PLCPermissionDenied, "SessionAuth: missing role, %s -- %s"%(method_message,person_message)
155 method.caller = person
158 raise PLCAuthenticationFailure, "SessionAuth: Invalid session"
160 except PLCAuthenticationFailure, fault:
164 class BootAuth(Auth):
166 PlanetLab version 3.x node authentication structure. Used by the
167 Boot Manager to make authenticated calls to the API based on a
168 unique node key or boot nonce value.
170 The original parameter serialization code did not define the byte
171 encoding of strings, or the string encoding of all other types. We
172 define the byte encoding to be UTF-8, and the string encoding of
173 all other types to be however Python version 2.3 unicode() encodes
178 Auth.__init__(self, {
179 'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional = False),
180 'node_id': Parameter(int, "Node identifier", optional = False),
181 'value': Parameter(str, "HMAC of node key and method call", optional = False)
184 def canonicalize(self, args):
188 if isinstance(arg, list) or isinstance(arg, tuple):
189 # The old implementation did not recursively handle
190 # lists of lists. But neither did the old API itself.
191 values += self.canonicalize(arg)
192 elif isinstance(arg, dict):
193 # Yes, the comments in the old implementation are
194 # misleading. Keys of dicts are not included in the
196 values += self.canonicalize(arg.values())
198 # We use unicode() instead of str().
199 values.append(unicode(arg))
203 def check(self, method, auth, *args):
204 # Method.type_check() should have checked that all of the
205 # mandatory fields were present.
206 assert auth.has_key('node_id')
208 if 'node' not in method.roles:
209 raise PLCAuthenticationFailure, "BootAuth: Not allowed to call method, missing 'node' role"
212 nodes = Nodes(method.api, {'node_id': auth['node_id'], 'peer_id': None})
214 raise PLCAuthenticationFailure, "BootAuth: No such node"
217 # Jan 2011 : removing support for old boot CDs
221 raise PLCAuthenticationFailure, "BootAuth: No node key"
223 # Yes, this is the "canonicalization" method used.
224 args = self.canonicalize(args)
226 msg = "[" + "".join(args) + "]"
228 # We encode in UTF-8 before calculating the HMAC, which is
229 # an 8-bit algorithm.
230 # python 2.6 insists on receiving a 'str' as opposed to a 'unicode'
231 digest = hmac.new(str(key), msg.encode('utf-8'), sha).hexdigest()
233 if digest != auth['value']:
234 raise PLCAuthenticationFailure, "BootAuth: Call could not be authenticated"
238 except PLCAuthenticationFailure, fault:
240 notify_owners(method, node, 'authfail', include_pis = True, include_techs = True, fault = fault)
243 class AnonymousAuth(Auth):
245 PlanetLab version 3.x anonymous authentication structure.
249 Auth.__init__(self, {
250 'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
253 def check(self, method, auth, *args):
254 if 'anonymous' not in method.roles:
255 raise PLCAuthenticationFailure, "AnonymousAuth: method cannot be called anonymously"
259 class PasswordAuth(Auth):
261 PlanetLab version 3.x password authentication structure.
265 Auth.__init__(self, {
266 'AuthMethod': Parameter(str, "Authentication method to use, always 'password' or 'capability'", optional = False),
267 'Username': Parameter(str, "PlanetLab username, typically an e-mail address", optional = False),
268 'AuthString': Parameter(str, "Authentication string, typically a password", optional = False),
271 def check(self, method, auth, *args):
272 # Method.type_check() should have checked that all of the
273 # mandatory fields were present.
274 assert auth.has_key('Username')
276 # Get record (must be enabled)
277 persons = Persons(method.api, {'email': auth['Username'].lower(), 'enabled': True, 'peer_id': None})
278 if len(persons) != 1:
279 raise PLCAuthenticationFailure, "PasswordAuth: No such account"
283 if auth['Username'] == method.api.config.PLC_API_MAINTENANCE_USER:
284 # "Capability" authentication, whatever the hell that was
285 # supposed to mean. It really means, login as the special
286 # "maintenance user" using password authentication. Can
287 # only be used on particular machines (those in a list).
288 sources = method.api.config.PLC_API_MAINTENANCE_SOURCES.split()
289 if method.source is not None and method.source[0] not in sources:
290 raise PLCAuthenticationFailure, "PasswordAuth: Not allowed to login to maintenance account"
292 # Not sure why this is not stored in the DB
293 password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
295 if auth['AuthString'] != password:
296 raise PLCAuthenticationFailure, "PasswordAuth: Maintenance account password verification failed"
298 # Compare encrypted plaintext against encrypted password stored in the DB
299 plaintext = auth['AuthString'].encode(method.api.encoding)
300 password = person['password']
302 # Protect against blank passwords in the DB
303 if password is None or password[:12] == "" or \
304 crypt.crypt(plaintext, password[:12]) != password:
305 raise PLCAuthenticationFailure, "PasswordAuth: Password verification failed"
307 if not set(person['roles']).intersection(method.roles):
308 method_message="method %s has roles [%s]"%(method.name,','.join(method.roles))
309 person_message="caller %s has roles [%s]"%(person['email'],','.join(person['roles']))
310 raise PLCAuthenticationFailure, "PasswordAuth: missing role, %s -- %s"%(method_message,person_message)
312 method.caller = person
314 auth_methods = {'session': SessionAuth,
315 'password': PasswordAuth,
316 'capability': PasswordAuth,
319 'hmac_dummybox': BootAuth,
320 'anonymous': AnonymousAuth}
322 path = os.path.dirname(__file__) + "/Auth.d"
324 extensions = os.listdir(path)
327 for extension in extensions:
328 if extension.startswith("."):
330 if not extension.endswith(".py"):
332 execfile("%s/%s" % (path, extension))