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 raise PLCAuthenticationFailure, "SessionAuth: Not allowed to call method, missing 'node' role"
139 elif session['person_id'] is not None and session['expires'] > time.time():
140 persons = Persons(method.api, {'person_id': session['person_id'], 'enabled': True, 'peer_id': None})
142 raise PLCAuthenticationFailure, "SessionAuth: No such account"
145 if not set(person['roles']).intersection(method.roles):
146 raise PLCPermissionDenied, "Not allowed to call method, missing role"
148 method.caller = persons[0]
151 raise PLCAuthenticationFailure, "SessionAuth: Invalid session"
153 except PLCAuthenticationFailure, fault:
157 class BootAuth(Auth):
159 PlanetLab version 3.x node authentication structure. Used by the
160 Boot Manager to make authenticated calls to the API based on a
161 unique node key or boot nonce value.
163 The original parameter serialization code did not define the byte
164 encoding of strings, or the string encoding of all other types. We
165 define the byte encoding to be UTF-8, and the string encoding of
166 all other types to be however Python version 2.3 unicode() encodes
171 Auth.__init__(self, {
172 'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional = False),
173 'node_id': Parameter(int, "Node identifier", optional = False),
174 'value': Parameter(str, "HMAC of node key and method call", optional = False)
177 def canonicalize(self, args):
181 if isinstance(arg, list) or isinstance(arg, tuple):
182 # The old implementation did not recursively handle
183 # lists of lists. But neither did the old API itself.
184 values += self.canonicalize(arg)
185 elif isinstance(arg, dict):
186 # Yes, the comments in the old implementation are
187 # misleading. Keys of dicts are not included in the
189 values += self.canonicalize(arg.values())
191 # We use unicode() instead of str().
192 values.append(unicode(arg))
196 def check(self, method, auth, *args):
197 # Method.type_check() should have checked that all of the
198 # mandatory fields were present.
199 assert auth.has_key('node_id')
201 if 'node' not in method.roles:
202 raise PLCAuthenticationFailure, "BootAuth: Not allowed to call method, missing 'node' role"
205 nodes = Nodes(method.api, {'node_id': auth['node_id'], 'peer_id': None})
207 raise PLCAuthenticationFailure, "BootAuth: No such node"
212 elif node['boot_nonce']:
213 # Allow very old nodes that do not have a node key in
214 # their configuration files to use their "boot nonce"
215 # instead. The boot nonce is a random value generated
216 # by the node itself and POSTed by the Boot CD when it
217 # requests the Boot Manager. This is obviously not
218 # very secure, so we only allow it to be used if the
219 # requestor IP is the same as the IP address we have
220 # on record for the node.
221 key = node['boot_nonce']
224 if node['interface_ids']:
225 interfaces = Interfaces(method.api, node['interface_ids'])
226 for interface in interfaces:
227 if interface['is_primary']:
230 if not interface or not interface['is_primary']:
231 raise PLCAuthenticationFailure, "BootAuth: No primary network interface on record"
233 if method.source is None:
234 raise PLCAuthenticationFailure, "BootAuth: Cannot determine IP address of requestor"
236 if interface['ip'] != method.source[0]:
237 raise PLCAuthenticationFailure, "BootAuth: Requestor IP %s does not match node IP %s" % \
238 (method.source[0], interface['ip'])
240 raise PLCAuthenticationFailure, "BootAuth: No node key or boot nonce"
242 # Yes, this is the "canonicalization" method used.
243 args = self.canonicalize(args)
245 msg = "[" + "".join(args) + "]"
247 # We encode in UTF-8 before calculating the HMAC, which is
248 # an 8-bit algorithm.
249 # python 2.6 insists on receiving a 'str' as opposed to a 'unicode'
250 digest = hmac.new(str(key), msg.encode('utf-8'), sha).hexdigest()
252 if digest != auth['value']:
253 raise PLCAuthenticationFailure, "BootAuth: Call could not be authenticated"
257 except PLCAuthenticationFailure, fault:
259 notify_owners(method, node, 'authfail', include_pis = True, include_techs = True, fault = fault)
262 class AnonymousAuth(Auth):
264 PlanetLab version 3.x anonymous authentication structure.
268 Auth.__init__(self, {
269 'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
272 def check(self, method, auth, *args):
273 if 'anonymous' not in method.roles:
274 raise PLCAuthenticationFailure, "AnonymousAuth: method cannot be called anonymously"
278 class PasswordAuth(Auth):
280 PlanetLab version 3.x password authentication structure.
284 Auth.__init__(self, {
285 'AuthMethod': Parameter(str, "Authentication method to use, always 'password' or 'capability'", optional = False),
286 'Username': Parameter(str, "PlanetLab username, typically an e-mail address", optional = False),
287 'AuthString': Parameter(str, "Authentication string, typically a password", optional = False),
290 def check(self, method, auth, *args):
291 # Method.type_check() should have checked that all of the
292 # mandatory fields were present.
293 assert auth.has_key('Username')
295 # Get record (must be enabled)
296 persons = Persons(method.api, {'email': auth['Username'].lower(), 'enabled': True, 'peer_id': None})
297 if len(persons) != 1:
298 raise PLCAuthenticationFailure, "PasswordAuth: No such account"
302 if auth['Username'] == method.api.config.PLC_API_MAINTENANCE_USER:
303 # "Capability" authentication, whatever the hell that was
304 # supposed to mean. It really means, login as the special
305 # "maintenance user" using password authentication. Can
306 # only be used on particular machines (those in a list).
307 sources = method.api.config.PLC_API_MAINTENANCE_SOURCES.split()
308 if method.source is not None and method.source[0] not in sources:
309 raise PLCAuthenticationFailure, "PasswordAuth: Not allowed to login to maintenance account"
311 # Not sure why this is not stored in the DB
312 password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
314 if auth['AuthString'] != password:
315 raise PLCAuthenticationFailure, "PasswordAuth: Maintenance account password verification failed"
317 # Compare encrypted plaintext against encrypted password stored in the DB
318 plaintext = auth['AuthString'].encode(method.api.encoding)
319 password = person['password']
321 # Protect against blank passwords in the DB
322 if password is None or password[:12] == "" or \
323 crypt.crypt(plaintext, password[:12]) != password:
324 raise PLCAuthenticationFailure, "PasswordAuth: Password verification failed"
326 if not set(person['roles']).intersection(method.roles):
327 raise PLCAuthenticationFailure, "PasswordAuth: Not allowed to call method, missing role"
329 method.caller = person
331 auth_methods = {'session': SessionAuth,
332 'password': PasswordAuth,
333 'capability': PasswordAuth,
336 'hmac_dummybox': BootAuth,
337 'anonymous': AnonymousAuth}
339 path = os.path.dirname(__file__) + "/Auth.d"
341 extensions = os.listdir(path)
344 for extension in extensions:
345 if extension.startswith("."):
347 if not extension.endswith(".py"):
349 execfile("%s/%s" % (path, extension))