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
19 from PLC.Faults import *
20 from PLC.Parameter import Parameter, Mixed
21 from PLC.Persons import Persons
22 from PLC.Nodes import Node, Nodes
23 from PLC.Interfaces import Interface, Interfaces
24 from PLC.Sessions import Session, Sessions
25 from PLC.Peers import Peer, Peers
26 from PLC.Boot import notify_owners
29 if auth['AuthMethod'] == "session":
30 expected = SessionAuth()
31 elif auth['AuthMethod'] == "password" or \
32 auth['AuthMethod'] == "capability":
33 expected = PasswordAuth()
34 elif auth['AuthMethod'] == "gpg":
36 elif auth['AuthMethod'] == "hmac" or \
37 auth['AuthMethod'] == "hmac_dummybox":
39 elif auth['AuthMethod'] == "anonymous":
40 expected = AnonymousAuth()
42 raise PLCInvalidArgument("must be 'session', 'password', 'gpg', 'hmac', 'hmac_dummybox', or 'anonymous'", "AuthMethod")
45 class Auth(Parameter):
47 Base class for all API authentication methods, as well as a class
48 that can be used to represent all supported API authentication
52 def __init__(self, auth = None):
54 auth = {'AuthMethod': Parameter(str, "Authentication method to use", optional = False)}
55 Parameter.__init__(self, auth, "API authentication structure")
57 def check(self, method, auth, *args):
58 # Method.type_check() should have checked that all of the
59 # mandatory fields were present.
60 assert 'AuthMethod' in auth
62 expected = map_auth(auth)
64 # Re-check using the specified authentication method
65 method.type_check("auth", auth, expected, (auth,) + args)
69 Proposed PlanetLab federation authentication structure.
74 'AuthMethod': Parameter(str, "Authentication method to use, always 'gpg'", optional = False),
75 'name': Parameter(str, "Peer or user name", optional = False),
76 'signature': Parameter(str, "Message signature", optional = False)
79 def check(self, method, auth, *args):
81 peers = Peers(method.api, [auth['name']])
83 if 'peer' not in method.roles:
84 raise PLCAuthenticationFailure, "Not allowed to call method"
86 method.caller = peer = peers[0]
89 persons = Persons(method.api, {'email': auth['name'], 'enabled': True, 'peer_id': None})
91 raise PLCAuthenticationFailure, "No such user '%s'" % auth['name']
93 if not set(person['roles']).intersection(method.roles):
94 raise PLCAuthenticationFailure, "Not allowed to call method"
96 method.caller = person = persons[0]
97 keys = Keys(method.api, {'key_id': person['key_ids'], 'key_type': "gpg", 'peer_id': None})
100 raise PLCAuthenticationFailure, "No GPG key on record for peer or user '%s'"
104 from PLC.GPG import gpg_verify
105 gpg_verify(args, key, auth['signature'], method.name)
107 except PLCAuthenticationFailure, fault:
112 except PLCAuthenticationFailure, fault:
116 class SessionAuth(Auth):
118 Secondary authentication method. After authenticating with a
119 primary authentication method, call GetSession() to generate a
120 session key that may be used for subsequent calls.
124 Auth.__init__(self, {
125 'AuthMethod': Parameter(str, "Authentication method to use, always 'session'", optional = False),
126 'session': Parameter(str, "Session key", optional = False)
129 def check(self, method, auth, *args):
130 # Method.type_check() should have checked that all of the
131 # mandatory fields were present.
132 assert auth.has_key('session')
135 sessions = Sessions(method.api, [auth['session']], expires = None)
137 raise PLCAuthenticationFailure, "No such session"
138 session = sessions[0]
141 if session['node_id'] is not None:
142 nodes = Nodes(method.api, {'node_id': session['node_id'], 'peer_id': None})
144 raise PLCAuthenticationFailure, "No such node"
147 if 'node' not in method.roles:
148 raise PLCAuthenticationFailure, "Not allowed to call method"
152 elif session['person_id'] is not None and session['expires'] > time.time():
153 persons = Persons(method.api, {'person_id': session['person_id'], 'enabled': True, 'peer_id': None})
155 raise PLCAuthenticationFailure, "No such account"
158 if not set(person['roles']).intersection(method.roles):
159 raise PLCPermissionDenied, "Not allowed to call method"
161 method.caller = persons[0]
164 raise PLCAuthenticationFailure, "Invalid session"
166 except PLCAuthenticationFailure, fault:
170 class BootAuth(Auth):
172 PlanetLab version 3.x node authentication structure. Used by the
173 Boot Manager to make authenticated calls to the API based on a
174 unique node key or boot nonce value.
176 The original parameter serialization code did not define the byte
177 encoding of strings, or the string encoding of all other types. We
178 define the byte encoding to be UTF-8, and the string encoding of
179 all other types to be however Python version 2.3 unicode() encodes
184 Auth.__init__(self, {
185 'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional = False),
186 'node_id': Parameter(int, "Node identifier", optional = False),
187 'value': Parameter(str, "HMAC of node key and method call", optional = False)
190 def canonicalize(self, args):
194 if isinstance(arg, list) or isinstance(arg, tuple):
195 # The old implementation did not recursively handle
196 # lists of lists. But neither did the old API itself.
197 values += self.canonicalize(arg)
198 elif isinstance(arg, dict):
199 # Yes, the comments in the old implementation are
200 # misleading. Keys of dicts are not included in the
202 values += self.canonicalize(arg.values())
204 # We use unicode() instead of str().
205 values.append(unicode(arg))
209 def check(self, method, auth, *args):
210 # Method.type_check() should have checked that all of the
211 # mandatory fields were present.
212 assert auth.has_key('node_id')
214 if 'node' not in method.roles:
215 raise PLCAuthenticationFailure, "Not allowed to call method"
218 nodes = Nodes(method.api, {'node_id': auth['node_id'], 'peer_id': None})
220 raise PLCAuthenticationFailure, "No such node"
225 elif node['boot_nonce']:
226 # Allow very old nodes that do not have a node key in
227 # their configuration files to use their "boot nonce"
228 # instead. The boot nonce is a random value generated
229 # by the node itself and POSTed by the Boot CD when it
230 # requests the Boot Manager. This is obviously not
231 # very secure, so we only allow it to be used if the
232 # requestor IP is the same as the IP address we have
233 # on record for the node.
234 key = node['boot_nonce']
237 if node['interface_ids']:
238 interfaces = Interfaces(method.api, node['interface_ids'])
239 for interface in interfaces:
240 if interface['is_primary']:
243 if not interface or not interface['is_primary']:
244 raise PLCAuthenticationFailure, "No primary network interface on record"
246 if method.source is None:
247 raise PLCAuthenticationFailure, "Cannot determine IP address of requestor"
249 if interface['ip'] != method.source[0]:
250 raise PLCAuthenticationFailure, "Requestor IP %s does not match node IP %s" % \
251 (method.source[0], interface['ip'])
253 raise PLCAuthenticationFailure, "No node key or boot nonce"
255 # Yes, this is the "canonicalization" method used.
256 args = self.canonicalize(args)
258 msg = "[" + "".join(args) + "]"
260 # We encode in UTF-8 before calculating the HMAC, which is
261 # an 8-bit algorithm.
262 # python 2.6 insists on receiving a 'str' as opposed to a 'unicode'
263 digest = hmac.new(str(key), msg.encode('utf-8'), sha).hexdigest()
265 if digest != auth['value']:
266 raise PLCAuthenticationFailure, "Call could not be authenticated"
270 except PLCAuthenticationFailure, fault:
272 notify_owners(method, node, 'authfail', include_pis = True, include_techs = True, fault = fault)
275 class AnonymousAuth(Auth):
277 PlanetLab version 3.x anonymous authentication structure.
281 Auth.__init__(self, {
282 'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
285 def check(self, method, auth, *args):
286 if 'anonymous' not in method.roles:
287 raise PLCAuthenticationFailure, "Not allowed to call method anonymously"
291 class PasswordAuth(Auth):
293 PlanetLab version 3.x password authentication structure.
297 Auth.__init__(self, {
298 'AuthMethod': Parameter(str, "Authentication method to use, always 'password' or 'capability'", optional = False),
299 'Username': Parameter(str, "PlanetLab username, typically an e-mail address", optional = False),
300 'AuthString': Parameter(str, "Authentication string, typically a password", optional = False),
303 def check(self, method, auth, *args):
304 # Method.type_check() should have checked that all of the
305 # mandatory fields were present.
306 assert auth.has_key('Username')
308 # Get record (must be enabled)
309 persons = Persons(method.api, {'email': auth['Username'].lower(), 'enabled': True, 'peer_id': None})
310 if len(persons) != 1:
311 raise PLCAuthenticationFailure, "No such account"
315 if auth['Username'] == method.api.config.PLC_API_MAINTENANCE_USER:
316 # "Capability" authentication, whatever the hell that was
317 # supposed to mean. It really means, login as the special
318 # "maintenance user" using password authentication. Can
319 # only be used on particular machines (those in a list).
320 sources = method.api.config.PLC_API_MAINTENANCE_SOURCES.split()
321 if method.source is not None and method.source[0] not in sources:
322 raise PLCAuthenticationFailure, "Not allowed to login to maintenance account"
324 # Not sure why this is not stored in the DB
325 password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
327 if auth['AuthString'] != password:
328 raise PLCAuthenticationFailure, "Maintenance account password verification failed"
330 # Compare encrypted plaintext against encrypted password stored in the DB
331 plaintext = auth['AuthString'].encode(method.api.encoding)
332 password = person['password']
334 # Protect against blank passwords in the DB
335 if password is None or password[:12] == "" or \
336 crypt.crypt(plaintext, password[:12]) != password:
337 raise PLCAuthenticationFailure, "Password verification failed"
339 if not set(person['roles']).intersection(method.roles):
340 raise PLCAuthenticationFailure, "Not allowed to call method"
342 method.caller = person