2 # PLCAPI authentication parameters
4 # Mark Huang <mlhuang@cs.princeton.edu>
5 # Copyright (C) 2006 The Trustees of Princeton University
13 from hashlib import sha1 as sha
20 from PLC.Faults import *
21 from PLC.Parameter import Parameter, Mixed
22 from PLC.Persons import Persons
23 from PLC.Nodes import Node, Nodes
24 from PLC.Interfaces import Interface, Interfaces
25 from PLC.Sessions import Session, Sessions
26 from PLC.Peers import Peer, Peers
27 from PLC.Boot import notify_owners
29 class Auth(Parameter):
31 Base class for all API authentication methods, as well as a class
32 that can be used to represent all supported API authentication
36 def __init__(self, auth = None):
38 auth = {'AuthMethod': Parameter(str, "Authentication method to use", optional = False)}
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(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)
59 Proposed PlanetLab federation authentication structure.
64 'AuthMethod': Parameter(str, "Authentication method to use, always 'gpg'", optional = False),
65 'name': Parameter(str, "Peer or user name", optional = False),
66 'signature': Parameter(str, "Message signature", optional = False)
69 def check(self, method, auth, *args):
71 peers = Peers(method.api, [auth['name']])
73 if 'peer' not in method.roles:
74 raise PLCAuthenticationFailure, "Not allowed to call method"
76 method.caller = peer = peers[0]
79 persons = Persons(method.api, {'email': auth['name'], 'enabled': True, 'peer_id': None})
81 raise PLCAuthenticationFailure, "No such user '%s'" % auth['name']
83 if not set(person['roles']).intersection(method.roles):
84 raise PLCAuthenticationFailure, "Not allowed to call method"
86 method.caller = person = persons[0]
87 keys = Keys(method.api, {'key_id': person['key_ids'], 'key_type': "gpg", 'peer_id': None})
90 raise PLCAuthenticationFailure, "No GPG key on record for peer or user '%s'"
94 from PLC.GPG import gpg_verify
95 gpg_verify(args, key, auth['signature'], method.name)
97 except PLCAuthenticationFailure, fault:
102 except PLCAuthenticationFailure, fault:
106 class SessionAuth(Auth):
108 Secondary authentication method. After authenticating with a
109 primary authentication method, call GetSession() to generate a
110 session key that may be used for subsequent calls.
114 Auth.__init__(self, {
115 'AuthMethod': Parameter(str, "Authentication method to use, always 'session'", optional = False),
116 'session': Parameter(str, "Session key", optional = False)
119 def check(self, method, auth, *args):
120 # Method.type_check() should have checked that all of the
121 # mandatory fields were present.
122 assert auth.has_key('session')
125 sessions = Sessions(method.api, [auth['session']], expires = None)
127 raise PLCAuthenticationFailure, "No such session"
128 session = sessions[0]
131 if session['node_id'] is not None:
132 nodes = Nodes(method.api, {'node_id': session['node_id'], 'peer_id': None})
134 raise PLCAuthenticationFailure, "No such node"
137 if 'node' not in method.roles:
138 raise PLCAuthenticationFailure, "Not allowed to call method"
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, "No such account"
148 if not set(person['roles']).intersection(method.roles):
149 raise PLCPermissionDenied, "Not allowed to call method"
151 method.caller = persons[0]
154 raise PLCAuthenticationFailure, "Invalid session"
156 except PLCAuthenticationFailure, fault:
160 class BootAuth(Auth):
162 PlanetLab version 3.x node authentication structure. Used by the
163 Boot Manager to make authenticated calls to the API based on a
164 unique node key or boot nonce value.
166 The original parameter serialization code did not define the byte
167 encoding of strings, or the string encoding of all other types. We
168 define the byte encoding to be UTF-8, and the string encoding of
169 all other types to be however Python version 2.3 unicode() encodes
174 Auth.__init__(self, {
175 'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional = False),
176 'node_id': Parameter(int, "Node identifier", optional = False),
177 'value': Parameter(str, "HMAC of node key and method call", optional = False)
180 def canonicalize(self, args):
184 if isinstance(arg, list) or isinstance(arg, tuple):
185 # The old implementation did not recursively handle
186 # lists of lists. But neither did the old API itself.
187 values += self.canonicalize(arg)
188 elif isinstance(arg, dict):
189 # Yes, the comments in the old implementation are
190 # misleading. Keys of dicts are not included in the
192 values += self.canonicalize(arg.values())
194 # We use unicode() instead of str().
195 values.append(unicode(arg))
199 def check(self, method, auth, *args):
200 # Method.type_check() should have checked that all of the
201 # mandatory fields were present.
202 assert auth.has_key('node_id')
204 if 'node' not in method.roles:
205 raise PLCAuthenticationFailure, "Not allowed to call method"
208 nodes = Nodes(method.api, {'node_id': auth['node_id'], 'peer_id': None})
210 raise PLCAuthenticationFailure, "No such node"
215 elif node['boot_nonce']:
216 # Allow very old nodes that do not have a node key in
217 # their configuration files to use their "boot nonce"
218 # instead. The boot nonce is a random value generated
219 # by the node itself and POSTed by the Boot CD when it
220 # requests the Boot Manager. This is obviously not
221 # very secure, so we only allow it to be used if the
222 # requestor IP is the same as the IP address we have
223 # on record for the node.
224 key = node['boot_nonce']
227 if node['interface_ids']:
228 interfaces = Interfaces(method.api, node['interface_ids'])
229 for interface in interfaces:
230 if interface['is_primary']:
233 if not interface or not interface['is_primary']:
234 raise PLCAuthenticationFailure, "No primary network interface on record"
236 if method.source is None:
237 raise PLCAuthenticationFailure, "Cannot determine IP address of requestor"
239 if interface['ip'] != method.source[0]:
240 raise PLCAuthenticationFailure, "Requestor IP %s does not match node IP %s" % \
241 (method.source[0], interface['ip'])
243 raise PLCAuthenticationFailure, "No node key or boot nonce"
245 # Yes, this is the "canonicalization" method used.
246 args = self.canonicalize(args)
248 msg = "[" + "".join(args) + "]"
250 # We encode in UTF-8 before calculating the HMAC, which is
251 # an 8-bit algorithm.
252 # python 2.6 insists on receiving a 'str' as opposed to a 'unicode'
253 digest = hmac.new(str(key), msg.encode('utf-8'), sha).hexdigest()
255 if digest != auth['value']:
256 raise PLCAuthenticationFailure, "Call could not be authenticated"
260 except PLCAuthenticationFailure, fault:
262 notify_owners(method, node, 'authfail', include_pis = True, include_techs = True, fault = fault)
265 class AnonymousAuth(Auth):
267 PlanetLab version 3.x anonymous authentication structure.
271 Auth.__init__(self, {
272 'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
275 def check(self, method, auth, *args):
276 if 'anonymous' not in method.roles:
277 raise PLCAuthenticationFailure, "Not allowed to call method anonymously"
281 class PasswordAuth(Auth):
283 PlanetLab version 3.x password authentication structure.
287 Auth.__init__(self, {
288 'AuthMethod': Parameter(str, "Authentication method to use, always 'password' or 'capability'", optional = False),
289 'Username': Parameter(str, "PlanetLab username, typically an e-mail address", optional = False),
290 'AuthString': Parameter(str, "Authentication string, typically a password", optional = False),
293 def check(self, method, auth, *args):
294 # Method.type_check() should have checked that all of the
295 # mandatory fields were present.
296 assert auth.has_key('Username')
298 # Get record (must be enabled)
299 persons = Persons(method.api, {'email': auth['Username'].lower(), 'enabled': True, 'peer_id': None})
300 if len(persons) != 1:
301 raise PLCAuthenticationFailure, "No such account"
305 if auth['Username'] == method.api.config.PLC_API_MAINTENANCE_USER:
306 # "Capability" authentication, whatever the hell that was
307 # supposed to mean. It really means, login as the special
308 # "maintenance user" using password authentication. Can
309 # only be used on particular machines (those in a list).
310 sources = method.api.config.PLC_API_MAINTENANCE_SOURCES.split()
311 if method.source is not None and method.source[0] not in sources:
312 raise PLCAuthenticationFailure, "Not allowed to login to maintenance account"
314 # Not sure why this is not stored in the DB
315 password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
317 if auth['AuthString'] != password:
318 raise PLCAuthenticationFailure, "Maintenance account password verification failed"
320 # Compare encrypted plaintext against encrypted password stored in the DB
321 plaintext = auth['AuthString'].encode(method.api.encoding)
322 password = person['password']
324 # Protect against blank passwords in the DB
325 if password is None or password[:12] == "" or \
326 crypt.crypt(plaintext, password[:12]) != password:
327 raise PLCAuthenticationFailure, "Password verification failed"
329 if not set(person['roles']).intersection(method.roles):
330 raise PLCAuthenticationFailure, "Not allowed to call method"
332 method.caller = person
334 auth_methods = {'session': SessionAuth,
335 'password': PasswordAuth,
336 'capability': PasswordAuth,
339 'hmac_dummybox': BootAuth,
340 'anonymous': AnonymousAuth}
342 path = os.path.dirname(__file__) + "/Auth.d"
344 extensions = os.listdir(path)
347 for extension in extensions:
348 if extension.startswith("."):
350 if not extension.endswith(".py"):
352 execfile("%s/%s" % (path, extension))