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 auth_methods = {'session': SessionAuth,
30 'password': PasswordAuth,
31 'capability': PasswordAuth,
34 'hmac_dummybox': BootAuth,
35 'anonymous': AnonymousAuth}
37 class Auth(Parameter):
39 Base class for all API authentication methods, as well as a class
40 that can be used to represent all supported API authentication
44 def __init__(self, auth = None):
46 auth = {'AuthMethod': Parameter(str, "Authentication method to use", optional = False)}
47 Parameter.__init__(self, auth, "API authentication structure")
49 def check(self, method, auth, *args):
52 # Method.type_check() should have checked that all of the
53 # mandatory fields were present.
54 assert 'AuthMethod' in auth
56 if auth['AuthMethod'] in auth_methods:
57 expected = auth_methods[auth['AuthMethod']]()
59 sm = "'" + "', '".join(auth_methods.keys()) + "'"
60 raise PLCInvalidArgument("must be " + sm, "AuthMethod")
62 # Re-check using the specified authentication method
63 method.type_check("auth", auth, expected, (auth,) + args)
67 Proposed PlanetLab federation authentication structure.
72 'AuthMethod': Parameter(str, "Authentication method to use, always 'gpg'", optional = False),
73 'name': Parameter(str, "Peer or user name", optional = False),
74 'signature': Parameter(str, "Message signature", optional = False)
77 def check(self, method, auth, *args):
79 peers = Peers(method.api, [auth['name']])
81 if 'peer' not in method.roles:
82 raise PLCAuthenticationFailure, "Not allowed to call method"
84 method.caller = peer = peers[0]
87 persons = Persons(method.api, {'email': auth['name'], 'enabled': True, 'peer_id': None})
89 raise PLCAuthenticationFailure, "No such user '%s'" % auth['name']
91 if not set(person['roles']).intersection(method.roles):
92 raise PLCAuthenticationFailure, "Not allowed to call method"
94 method.caller = person = persons[0]
95 keys = Keys(method.api, {'key_id': person['key_ids'], 'key_type': "gpg", 'peer_id': None})
98 raise PLCAuthenticationFailure, "No GPG key on record for peer or user '%s'"
102 from PLC.GPG import gpg_verify
103 gpg_verify(args, key, auth['signature'], method.name)
105 except PLCAuthenticationFailure, fault:
110 except PLCAuthenticationFailure, fault:
114 class SessionAuth(Auth):
116 Secondary authentication method. After authenticating with a
117 primary authentication method, call GetSession() to generate a
118 session key that may be used for subsequent calls.
122 Auth.__init__(self, {
123 'AuthMethod': Parameter(str, "Authentication method to use, always 'session'", optional = False),
124 'session': Parameter(str, "Session key", optional = False)
127 def check(self, method, auth, *args):
128 # Method.type_check() should have checked that all of the
129 # mandatory fields were present.
130 assert auth.has_key('session')
133 sessions = Sessions(method.api, [auth['session']], expires = None)
135 raise PLCAuthenticationFailure, "No such session"
136 session = sessions[0]
139 if session['node_id'] is not None:
140 nodes = Nodes(method.api, {'node_id': session['node_id'], 'peer_id': None})
142 raise PLCAuthenticationFailure, "No such node"
145 if 'node' not in method.roles:
146 raise PLCAuthenticationFailure, "Not allowed to call method"
150 elif session['person_id'] is not None and session['expires'] > time.time():
151 persons = Persons(method.api, {'person_id': session['person_id'], 'enabled': True, 'peer_id': None})
153 raise PLCAuthenticationFailure, "No such account"
156 if not set(person['roles']).intersection(method.roles):
157 raise PLCPermissionDenied, "Not allowed to call method"
159 method.caller = persons[0]
162 raise PLCAuthenticationFailure, "Invalid session"
164 except PLCAuthenticationFailure, fault:
168 class BootAuth(Auth):
170 PlanetLab version 3.x node authentication structure. Used by the
171 Boot Manager to make authenticated calls to the API based on a
172 unique node key or boot nonce value.
174 The original parameter serialization code did not define the byte
175 encoding of strings, or the string encoding of all other types. We
176 define the byte encoding to be UTF-8, and the string encoding of
177 all other types to be however Python version 2.3 unicode() encodes
182 Auth.__init__(self, {
183 'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional = False),
184 'node_id': Parameter(int, "Node identifier", optional = False),
185 'value': Parameter(str, "HMAC of node key and method call", optional = False)
188 def canonicalize(self, args):
192 if isinstance(arg, list) or isinstance(arg, tuple):
193 # The old implementation did not recursively handle
194 # lists of lists. But neither did the old API itself.
195 values += self.canonicalize(arg)
196 elif isinstance(arg, dict):
197 # Yes, the comments in the old implementation are
198 # misleading. Keys of dicts are not included in the
200 values += self.canonicalize(arg.values())
202 # We use unicode() instead of str().
203 values.append(unicode(arg))
207 def check(self, method, auth, *args):
208 # Method.type_check() should have checked that all of the
209 # mandatory fields were present.
210 assert auth.has_key('node_id')
212 if 'node' not in method.roles:
213 raise PLCAuthenticationFailure, "Not allowed to call method"
216 nodes = Nodes(method.api, {'node_id': auth['node_id'], 'peer_id': None})
218 raise PLCAuthenticationFailure, "No such node"
223 elif node['boot_nonce']:
224 # Allow very old nodes that do not have a node key in
225 # their configuration files to use their "boot nonce"
226 # instead. The boot nonce is a random value generated
227 # by the node itself and POSTed by the Boot CD when it
228 # requests the Boot Manager. This is obviously not
229 # very secure, so we only allow it to be used if the
230 # requestor IP is the same as the IP address we have
231 # on record for the node.
232 key = node['boot_nonce']
235 if node['interface_ids']:
236 interfaces = Interfaces(method.api, node['interface_ids'])
237 for interface in interfaces:
238 if interface['is_primary']:
241 if not interface or not interface['is_primary']:
242 raise PLCAuthenticationFailure, "No primary network interface on record"
244 if method.source is None:
245 raise PLCAuthenticationFailure, "Cannot determine IP address of requestor"
247 if interface['ip'] != method.source[0]:
248 raise PLCAuthenticationFailure, "Requestor IP %s does not match node IP %s" % \
249 (method.source[0], interface['ip'])
251 raise PLCAuthenticationFailure, "No node key or boot nonce"
253 # Yes, this is the "canonicalization" method used.
254 args = self.canonicalize(args)
256 msg = "[" + "".join(args) + "]"
258 # We encode in UTF-8 before calculating the HMAC, which is
259 # an 8-bit algorithm.
260 # python 2.6 insists on receiving a 'str' as opposed to a 'unicode'
261 digest = hmac.new(str(key), msg.encode('utf-8'), sha).hexdigest()
263 if digest != auth['value']:
264 raise PLCAuthenticationFailure, "Call could not be authenticated"
268 except PLCAuthenticationFailure, fault:
270 notify_owners(method, node, 'authfail', include_pis = True, include_techs = True, fault = fault)
273 class AnonymousAuth(Auth):
275 PlanetLab version 3.x anonymous authentication structure.
279 Auth.__init__(self, {
280 'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
283 def check(self, method, auth, *args):
284 if 'anonymous' not in method.roles:
285 raise PLCAuthenticationFailure, "Not allowed to call method anonymously"
289 class PasswordAuth(Auth):
291 PlanetLab version 3.x password authentication structure.
295 Auth.__init__(self, {
296 'AuthMethod': Parameter(str, "Authentication method to use, always 'password' or 'capability'", optional = False),
297 'Username': Parameter(str, "PlanetLab username, typically an e-mail address", optional = False),
298 'AuthString': Parameter(str, "Authentication string, typically a password", optional = False),
301 def check(self, method, auth, *args):
302 # Method.type_check() should have checked that all of the
303 # mandatory fields were present.
304 assert auth.has_key('Username')
306 # Get record (must be enabled)
307 persons = Persons(method.api, {'email': auth['Username'].lower(), 'enabled': True, 'peer_id': None})
308 if len(persons) != 1:
309 raise PLCAuthenticationFailure, "No such account"
313 if auth['Username'] == method.api.config.PLC_API_MAINTENANCE_USER:
314 # "Capability" authentication, whatever the hell that was
315 # supposed to mean. It really means, login as the special
316 # "maintenance user" using password authentication. Can
317 # only be used on particular machines (those in a list).
318 sources = method.api.config.PLC_API_MAINTENANCE_SOURCES.split()
319 if method.source is not None and method.source[0] not in sources:
320 raise PLCAuthenticationFailure, "Not allowed to login to maintenance account"
322 # Not sure why this is not stored in the DB
323 password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
325 if auth['AuthString'] != password:
326 raise PLCAuthenticationFailure, "Maintenance account password verification failed"
328 # Compare encrypted plaintext against encrypted password stored in the DB
329 plaintext = auth['AuthString'].encode(method.api.encoding)
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, "Password verification failed"
337 if not set(person['roles']).intersection(method.roles):
338 raise PLCAuthenticationFailure, "Not allowed to call method"
340 method.caller = person
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))