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]
76 persons = Persons(method.api, {'email': auth['name'], 'enabled': True, 'peer_id': None})
78 raise PLCAuthenticationFailure, "GPGAuth: No such user '%s'" % auth['name']
80 if not set(person['roles']).intersection(method.roles):
81 raise PLCAuthenticationFailure, "GPGAuth: Not allowed to call method, missing role"
83 method.caller = person = persons[0]
84 keys = Keys(method.api, {'key_id': person['key_ids'], 'key_type': "gpg", 'peer_id': None})
87 raise PLCAuthenticationFailure, "GPGAuth: No GPG key on record for peer or user '%s'"
91 from PLC.GPG import gpg_verify
92 gpg_verify(args, key, auth['signature'], method.name)
94 except PLCAuthenticationFailure, fault:
99 except PLCAuthenticationFailure, fault:
103 class SessionAuth(Auth):
105 Secondary authentication method. After authenticating with a
106 primary authentication method, call GetSession() to generate a
107 session key that may be used for subsequent calls.
111 Auth.__init__(self, {
112 'AuthMethod': Parameter(str, "Authentication method to use, always 'session'", optional = False),
113 'session': Parameter(str, "Session key", optional = False)
116 def check(self, method, auth, *args):
117 # Method.type_check() should have checked that all of the
118 # mandatory fields were present.
119 assert auth.has_key('session')
122 sessions = Sessions(method.api, [auth['session']], expires = None)
124 raise PLCAuthenticationFailure, "SessionAuth: No such session"
125 session = sessions[0]
128 if session['node_id'] is not None:
129 nodes = Nodes(method.api, {'node_id': session['node_id'], 'peer_id': None})
131 raise PLCAuthenticationFailure, "SessionAuth: No such node"
134 if 'node' not in method.roles:
135 # using PermissionDenied rather than AuthenticationFailure here because
136 # if that fails we don't want to delete the session..
137 raise PLCPermissionDenied, "SessionAuth: Not allowed to call method %s, missing 'node' role"%method.name
141 elif session['person_id'] is not None and session['expires'] > time.time():
142 persons = Persons(method.api, {'person_id': session['person_id'], 'enabled': True, 'peer_id': None})
144 raise PLCAuthenticationFailure, "SessionAuth: No such enabled account"
147 if not set(person['roles']).intersection(method.roles):
148 method_message="method %s has roles [%s]"%(method.name,','.join(method.roles))
149 person_message="caller %s has roles [%s]"%(person['email'],','.join(person['roles']))
150 # not PLCAuthenticationFailure b/c that would end the session..
151 raise PLCPermissionDenied, "SessionAuth: missing role, %s -- %s"%(method_message,person_message)
153 method.caller = person
156 raise PLCAuthenticationFailure, "SessionAuth: Invalid session"
158 except PLCAuthenticationFailure, fault:
162 class BootAuth(Auth):
164 PlanetLab version 3.x node authentication structure. Used by the
165 Boot Manager to make authenticated calls to the API based on a
166 unique node key or boot nonce value.
168 The original parameter serialization code did not define the byte
169 encoding of strings, or the string encoding of all other types. We
170 define the byte encoding to be UTF-8, and the string encoding of
171 all other types to be however Python version 2.3 unicode() encodes
176 Auth.__init__(self, {
177 'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional = False),
178 'node_id': Parameter(int, "Node identifier", optional = False),
179 'value': Parameter(str, "HMAC of node key and method call", optional = False)
182 def canonicalize(self, args):
186 if isinstance(arg, list) or isinstance(arg, tuple):
187 # The old implementation did not recursively handle
188 # lists of lists. But neither did the old API itself.
189 values += self.canonicalize(arg)
190 elif isinstance(arg, dict):
191 # Yes, the comments in the old implementation are
192 # misleading. Keys of dicts are not included in the
194 values += self.canonicalize(arg.values())
196 # We use unicode() instead of str().
197 values.append(unicode(arg))
201 def check(self, method, auth, *args):
202 # Method.type_check() should have checked that all of the
203 # mandatory fields were present.
204 assert auth.has_key('node_id')
206 if 'node' not in method.roles:
207 raise PLCAuthenticationFailure, "BootAuth: Not allowed to call method, missing 'node' role"
210 nodes = Nodes(method.api, {'node_id': auth['node_id'], 'peer_id': None})
212 raise PLCAuthenticationFailure, "BootAuth: No such node"
215 # Jan 2011 : removing support for old boot CDs
219 raise PLCAuthenticationFailure, "BootAuth: No node key"
221 # Yes, this is the "canonicalization" method used.
222 args = self.canonicalize(args)
224 msg = "[" + "".join(args) + "]"
226 # We encode in UTF-8 before calculating the HMAC, which is
227 # an 8-bit algorithm.
228 # python 2.6 insists on receiving a 'str' as opposed to a 'unicode'
229 digest = hmac.new(str(key), msg.encode('utf-8'), sha).hexdigest()
231 if digest != auth['value']:
232 raise PLCAuthenticationFailure, "BootAuth: Call could not be authenticated"
236 except PLCAuthenticationFailure, fault:
238 notify_owners(method, node, 'authfail', include_pis = True, include_techs = True, fault = fault)
241 class AnonymousAuth(Auth):
243 PlanetLab version 3.x anonymous authentication structure.
247 Auth.__init__(self, {
248 'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
251 def check(self, method, auth, *args):
252 if 'anonymous' not in method.roles:
253 raise PLCAuthenticationFailure, "AnonymousAuth: method cannot be called anonymously"
257 class PasswordAuth(Auth):
259 PlanetLab version 3.x password authentication structure.
263 Auth.__init__(self, {
264 'AuthMethod': Parameter(str, "Authentication method to use, always 'password' or 'capability'", optional = False),
265 'Username': Parameter(str, "PlanetLab username, typically an e-mail address", optional = False),
266 'AuthString': Parameter(str, "Authentication string, typically a password", optional = False),
269 def check(self, method, auth, *args):
270 # Method.type_check() should have checked that all of the
271 # mandatory fields were present.
272 assert auth.has_key('Username')
274 # Get record (must be enabled)
275 persons = Persons(method.api, {'email': auth['Username'].lower(), 'enabled': True, 'peer_id': None})
276 if len(persons) != 1:
277 raise PLCAuthenticationFailure, "PasswordAuth: No such account"
281 if auth['Username'] == method.api.config.PLC_API_MAINTENANCE_USER:
282 # "Capability" authentication, whatever the hell that was
283 # supposed to mean. It really means, login as the special
284 # "maintenance user" using password authentication. Can
285 # only be used on particular machines (those in a list).
286 sources = method.api.config.PLC_API_MAINTENANCE_SOURCES.split()
287 if method.source is not None and method.source[0] not in sources:
288 raise PLCAuthenticationFailure, "PasswordAuth: Not allowed to login to maintenance account"
290 # Not sure why this is not stored in the DB
291 password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
293 if auth['AuthString'] != password:
294 raise PLCAuthenticationFailure, "PasswordAuth: Maintenance account password verification failed"
296 # Compare encrypted plaintext against encrypted password stored in the DB
297 plaintext = auth['AuthString'].encode(method.api.encoding)
298 password = person['password']
300 # Protect against blank passwords in the DB
301 if password is None or password[:12] == "" or \
302 crypt.crypt(plaintext, password[:12]) != password:
303 raise PLCAuthenticationFailure, "PasswordAuth: Password verification failed"
305 if not set(person['roles']).intersection(method.roles):
306 method_message="method %s has roles [%s]"%(method.name,','.join(method.roles))
307 person_message="caller %s has roles [%s]"%(person['email'],','.join(person['roles']))
308 raise PLCAuthenticationFailure, "PasswordAuth: missing role, %s -- %s"%(method_message,person_message)
310 method.caller = person
312 auth_methods = {'session': SessionAuth,
313 'password': PasswordAuth,
314 'capability': PasswordAuth,
317 'hmac_dummybox': BootAuth,
318 'anonymous': AnonymousAuth}
320 path = os.path.dirname(__file__) + "/Auth.d"
322 extensions = os.listdir(path)
325 for extension in extensions:
326 if extension.startswith("."):
328 if not extension.endswith(".py"):
330 execfile("%s/%s" % (path, extension))