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.Boot import notify_owners
26 class Auth(Parameter):
28 Base class for all API authentication methods, as well as a class
29 that can be used to represent all supported API authentication
33 def __init__(self, auth = None):
35 auth = {'AuthMethod': Parameter(str, "Authentication method to use", optional = False)}
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(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)
56 Proposed PlanetLab federation authentication structure.
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)
66 def check(self, method, auth, *args):
68 peers = Peers(method.api, [auth['name']])
70 if 'peer' not in method.roles:
71 raise PLCAuthenticationFailure, "GPGAuth: Not allowed to call method, missing 'peer' role"
73 method.caller = peer = peers[0]
74 gpg_keys = [ peer['key'] ]
76 persons = Persons(method.api, {'email': auth['name'], 'enabled': True, 'peer_id': None})
78 raise PLCAuthenticationFailure, "GPGAuth: No such user '%s'" % auth['name']
80 method.caller = person = persons[0]
81 if not set(person['roles']).intersection(method.roles):
82 raise PLCAuthenticationFailure, "GPGAuth: Not allowed to call method, missing role"
84 keys = Keys(method.api, {'key_id': person['key_ids'], 'key_type': "gpg", 'peer_id': None})
85 gpg_keys = [ key['key'] for key in keys]
88 raise PLCAuthenticationFailure, "GPGAuth: No GPG key on record for peer or user '%s'"
90 for gpg_key in gpg_keys:
92 from PLC.GPG import gpg_verify
93 gpg_verify(args, gpg_key, auth['signature'], method.name)
95 except PLCAuthenticationFailure, fault:
100 except PLCAuthenticationFailure, fault:
104 class SessionAuth(Auth):
106 Secondary authentication method. After authenticating with a
107 primary authentication method, call GetSession() to generate a
108 session key that may be used for subsequent calls.
112 Auth.__init__(self, {
113 'AuthMethod': Parameter(str, "Authentication method to use, always 'session'", optional = False),
114 'session': Parameter(str, "Session key", optional = False)
117 def check(self, method, auth, *args):
118 # Method.type_check() should have checked that all of the
119 # mandatory fields were present.
120 assert auth.has_key('session')
123 sessions = Sessions(method.api, [auth['session']], expires = None)
125 raise PLCAuthenticationFailure, "SessionAuth: No such session"
126 session = sessions[0]
129 if session['node_id'] is not None:
130 nodes = Nodes(method.api, {'node_id': session['node_id'], 'peer_id': None})
132 raise PLCAuthenticationFailure, "SessionAuth: No such node"
135 if 'node' not in method.roles:
136 # using PermissionDenied rather than AuthenticationFailure here because
137 # if that fails we don't want to delete the session..
138 raise PLCPermissionDenied, "SessionAuth: Not allowed to call method %s, missing 'node' role"%method.name
142 elif session['person_id'] is not None and session['expires'] > time.time():
143 persons = Persons(method.api, {'person_id': session['person_id'], 'enabled': True, 'peer_id': None})
145 raise PLCAuthenticationFailure, "SessionAuth: No such enabled account"
148 if not set(person['roles']).intersection(method.roles):
149 method_message="method %s has roles [%s]"%(method.name,','.join(method.roles))
150 person_message="caller %s has roles [%s]"%(person['email'],','.join(person['roles']))
151 # not PLCAuthenticationFailure b/c that would end the session..
152 raise PLCPermissionDenied, "SessionAuth: missing role, %s -- %s"%(method_message,person_message)
154 method.caller = person
157 raise PLCAuthenticationFailure, "SessionAuth: Invalid session"
159 except PLCAuthenticationFailure, fault:
163 class BootAuth(Auth):
165 PlanetLab version 3.x node authentication structure. Used by the
166 Boot Manager to make authenticated calls to the API based on a
167 unique node key or boot nonce value.
169 The original parameter serialization code did not define the byte
170 encoding of strings, or the string encoding of all other types. We
171 define the byte encoding to be UTF-8, and the string encoding of
172 all other types to be however Python version 2.3 unicode() encodes
177 Auth.__init__(self, {
178 'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional = False),
179 'node_id': Parameter(int, "Node identifier", optional = False),
180 'value': Parameter(str, "HMAC of node key and method call", optional = False)
183 def canonicalize(self, args):
187 if isinstance(arg, list) or isinstance(arg, tuple):
188 # The old implementation did not recursively handle
189 # lists of lists. But neither did the old API itself.
190 values += self.canonicalize(arg)
191 elif isinstance(arg, dict):
192 # Yes, the comments in the old implementation are
193 # misleading. Keys of dicts are not included in the
195 values += self.canonicalize(arg.values())
197 # We use unicode() instead of str().
198 values.append(unicode(arg))
202 def check(self, method, auth, *args):
203 # Method.type_check() should have checked that all of the
204 # mandatory fields were present.
205 assert auth.has_key('node_id')
207 if 'node' not in method.roles:
208 raise PLCAuthenticationFailure, "BootAuth: Not allowed to call method, missing 'node' role"
211 nodes = Nodes(method.api, {'node_id': auth['node_id'], 'peer_id': None})
213 raise PLCAuthenticationFailure, "BootAuth: No such node"
216 # Jan 2011 : removing support for old boot CDs
220 raise PLCAuthenticationFailure, "BootAuth: No node key"
222 # Yes, this is the "canonicalization" method used.
223 args = self.canonicalize(args)
225 msg = "[" + "".join(args) + "]"
227 # We encode in UTF-8 before calculating the HMAC, which is
228 # an 8-bit algorithm.
229 # python 2.6 insists on receiving a 'str' as opposed to a 'unicode'
230 digest = hmac.new(str(key), msg.encode('utf-8'), sha).hexdigest()
232 if digest != auth['value']:
233 raise PLCAuthenticationFailure, "BootAuth: Call could not be authenticated"
237 except PLCAuthenticationFailure, fault:
239 notify_owners(method, node, 'authfail', include_pis = True, include_techs = True, fault = fault)
242 class AnonymousAuth(Auth):
244 PlanetLab version 3.x anonymous authentication structure.
248 Auth.__init__(self, {
249 'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
252 def check(self, method, auth, *args):
253 if 'anonymous' not in method.roles:
254 raise PLCAuthenticationFailure, "AnonymousAuth: method cannot be called anonymously"
258 class PasswordAuth(Auth):
260 PlanetLab version 3.x password authentication structure.
264 Auth.__init__(self, {
265 'AuthMethod': Parameter(str, "Authentication method to use, always 'password' or 'capability'", optional = False),
266 'Username': Parameter(str, "PlanetLab username, typically an e-mail address", optional = False),
267 'AuthString': Parameter(str, "Authentication string, typically a password", optional = False),
270 def check(self, method, auth, *args):
271 # Method.type_check() should have checked that all of the
272 # mandatory fields were present.
273 assert auth.has_key('Username')
275 # Get record (must be enabled)
276 persons = Persons(method.api, {'email': auth['Username'].lower(), 'enabled': True, 'peer_id': None})
277 if len(persons) != 1:
278 raise PLCAuthenticationFailure, "PasswordAuth: No such account"
282 if auth['Username'] == method.api.config.PLC_API_MAINTENANCE_USER:
283 # "Capability" authentication, whatever the hell that was
284 # supposed to mean. It really means, login as the special
285 # "maintenance user" using password authentication. Can
286 # only be used on particular machines (those in a list).
287 sources = method.api.config.PLC_API_MAINTENANCE_SOURCES.split()
288 if method.source is not None and method.source[0] not in sources:
289 raise PLCAuthenticationFailure, "PasswordAuth: Not allowed to login to maintenance account"
291 # Not sure why this is not stored in the DB
292 password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
294 if auth['AuthString'] != password:
295 raise PLCAuthenticationFailure, "PasswordAuth: Maintenance account password verification failed"
297 # Compare encrypted plaintext against encrypted password stored in the DB
298 plaintext = auth['AuthString'].encode(method.api.encoding)
299 password = person['password']
301 # Protect against blank passwords in the DB
302 if password is None or password[:12] == "" or \
303 crypt.crypt(plaintext, password[:12]) != password:
304 raise PLCAuthenticationFailure, "PasswordAuth: Password verification failed"
306 if not set(person['roles']).intersection(method.roles):
307 method_message="method %s has roles [%s]"%(method.name,','.join(method.roles))
308 person_message="caller %s has roles [%s]"%(person['email'],','.join(person['roles']))
309 raise PLCAuthenticationFailure, "PasswordAuth: missing role, %s -- %s"%(method_message,person_message)
311 method.caller = person
313 auth_methods = {'session': SessionAuth,
314 'password': PasswordAuth,
315 'capability': PasswordAuth,
318 'hmac_dummybox': BootAuth,
319 'anonymous': AnonymousAuth}
321 path = os.path.dirname(__file__) + "/Auth.d"
323 extensions = os.listdir(path)
326 for extension in extensions:
327 if extension.startswith("."):
329 if not extension.endswith(".py"):
331 execfile("%s/%s" % (path, extension))