2 # PLCAPI authentication parameters
4 # Mark Huang <mlhuang@cs.princeton.edu>
5 # Copyright (C) 2006 The Trustees of Princeton University
15 from PLC.Faults import *
16 from PLC.Parameter import Parameter, Mixed
17 from PLC.Persons import Persons
18 from PLC.Nodes import Node, Nodes
19 from PLC.Interfaces import Interface, Interfaces
20 from PLC.Sessions import Session, Sessions
21 from PLC.Peers import Peer, Peers
22 from PLC.Boot import notify_owners
24 class Auth(Parameter):
26 Base class for all API authentication methods, as well as a class
27 that can be used to represent all supported API authentication
31 def __init__(self, auth = None):
33 auth = {'AuthMethod': Parameter(str, "Authentication method to use", optional = False)}
34 Parameter.__init__(self, auth, "API authentication structure")
36 def check(self, method, auth, *args):
37 # Method.type_check() should have checked that all of the
38 # mandatory fields were present.
39 assert 'AuthMethod' in auth
41 if auth['AuthMethod'] == "session":
42 expected = SessionAuth()
43 elif auth['AuthMethod'] == "password" or \
44 auth['AuthMethod'] == "capability":
45 expected = PasswordAuth()
46 elif auth['AuthMethod'] == "gpg":
48 elif auth['AuthMethod'] == "hmac" or \
49 auth['AuthMethod'] == "hmac_dummybox":
51 elif auth['AuthMethod'] == "anonymous":
52 expected = AnonymousAuth()
54 raise PLCInvalidArgument("must be 'session', 'password', 'gpg', 'hmac', 'hmac_dummybox', or 'anonymous'", "AuthMethod")
56 # Re-check using the specified authentication method
57 method.type_check("auth", auth, expected, (auth,) + args)
61 Proposed PlanetLab federation authentication structure.
66 'AuthMethod': Parameter(str, "Authentication method to use, always 'gpg'", optional = False),
67 'name': Parameter(str, "Peer or user name", optional = False),
68 'signature': Parameter(str, "Message signature", optional = False)
71 def check(self, method, auth, *args):
73 peers = Peers(method.api, [auth['name']])
75 if 'peer' not in method.roles:
76 raise PLCAuthenticationFailure, "Not allowed to call method"
78 method.caller = peer = peers[0]
81 persons = Persons(method.api, {'email': auth['name'], 'enabled': True, 'peer_id': None})
83 raise PLCAuthenticationFailure, "No such user '%s'" % auth['name']
85 if not set(person['roles']).intersection(method.roles):
86 raise PLCAuthenticationFailure, "Not allowed to call method"
88 method.caller = person = persons[0]
89 keys = Keys(method.api, {'key_id': person['key_ids'], 'key_type': "gpg", 'peer_id': None})
92 raise PLCAuthenticationFailure, "No GPG key on record for peer or user '%s'"
96 from PLC.GPG import gpg_verify
97 gpg_verify(args, key, auth['signature'], method.name)
99 except PLCAuthenticationFailure, fault:
104 except PLCAuthenticationFailure, fault:
108 class SessionAuth(Auth):
110 Secondary authentication method. After authenticating with a
111 primary authentication method, call GetSession() to generate a
112 session key that may be used for subsequent calls.
116 Auth.__init__(self, {
117 'AuthMethod': Parameter(str, "Authentication method to use, always 'session'", optional = False),
118 'session': Parameter(str, "Session key", optional = False)
121 def check(self, method, auth, *args):
122 # Method.type_check() should have checked that all of the
123 # mandatory fields were present.
124 assert auth.has_key('session')
127 sessions = Sessions(method.api, [auth['session']], expires = None)
129 raise PLCAuthenticationFailure, "No such session"
130 session = sessions[0]
133 if session['node_id'] is not None:
134 nodes = Nodes(method.api, {'node_id': session['node_id'], 'peer_id': None})
136 raise PLCAuthenticationFailure, "No such node"
139 if 'node' not in method.roles:
140 raise PLCAuthenticationFailure, "Not allowed to call method"
144 elif session['person_id'] is not None and session['expires'] > time.time():
145 persons = Persons(method.api, {'person_id': session['person_id'], 'enabled': True, 'peer_id': None})
147 raise PLCAuthenticationFailure, "No such account"
150 if not set(person['roles']).intersection(method.roles):
151 raise PLCPermissionDenied, "Not allowed to call method"
153 method.caller = persons[0]
156 raise PLCAuthenticationFailure, "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, "Not allowed to call method"
210 nodes = Nodes(method.api, {'node_id': auth['node_id'], 'peer_id': None})
212 raise PLCAuthenticationFailure, "No such node"
217 elif node['boot_nonce']:
218 # Allow very old nodes that do not have a node key in
219 # their configuration files to use their "boot nonce"
220 # instead. The boot nonce is a random value generated
221 # by the node itself and POSTed by the Boot CD when it
222 # requests the Boot Manager. This is obviously not
223 # very secure, so we only allow it to be used if the
224 # requestor IP is the same as the IP address we have
225 # on record for the node.
226 key = node['boot_nonce']
229 if node['interface_ids']:
230 interfaces = Interfaces(method.api, node['interface_ids'])
231 for interface in interfaces:
232 if interface['is_primary']:
235 if not interface or not interface['is_primary']:
236 raise PLCAuthenticationFailure, "No primary network interface on record"
238 if method.source is None:
239 raise PLCAuthenticationFailure, "Cannot determine IP address of requestor"
241 if interface['ip'] != method.source[0]:
242 raise PLCAuthenticationFailure, "Requestor IP %s does not match node IP %s" % \
243 (method.source[0], interface['ip'])
245 raise PLCAuthenticationFailure, "No node key or boot nonce"
247 # Yes, this is the "canonicalization" method used.
248 args = self.canonicalize(args)
250 msg = "[" + "".join(args) + "]"
252 # We encode in UTF-8 before calculating the HMAC, which is
253 # an 8-bit algorithm.
254 digest = hmac.new(key, msg.encode('utf-8'), sha).hexdigest()
256 if digest != auth['value']:
257 raise PLCAuthenticationFailure, "Call could not be authenticated"
261 except PLCAuthenticationFailure, fault:
263 notify_owners(method, node, 'authfail', include_pis = True, include_techs = True, fault = fault)
266 class AnonymousAuth(Auth):
268 PlanetLab version 3.x anonymous authentication structure.
272 Auth.__init__(self, {
273 'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
276 def check(self, method, auth, *args):
277 if 'anonymous' not in method.roles:
278 raise PLCAuthenticationFailure, "Not allowed to call method anonymously"
282 class PasswordAuth(Auth):
284 PlanetLab version 3.x password authentication structure.
288 Auth.__init__(self, {
289 'AuthMethod': Parameter(str, "Authentication method to use, always 'password' or 'capability'", optional = False),
290 'Username': Parameter(str, "PlanetLab username, typically an e-mail address", optional = False),
291 'AuthString': Parameter(str, "Authentication string, typically a password", optional = False),
294 def check(self, method, auth, *args):
295 # Method.type_check() should have checked that all of the
296 # mandatory fields were present.
297 assert auth.has_key('Username')
299 # Get record (must be enabled)
300 persons = Persons(method.api, {'email': auth['Username'].lower(), 'enabled': True, 'peer_id': None})
301 if len(persons) != 1:
302 raise PLCAuthenticationFailure, "No such account"
306 if auth['Username'] == method.api.config.PLC_API_MAINTENANCE_USER:
307 # "Capability" authentication, whatever the hell that was
308 # supposed to mean. It really means, login as the special
309 # "maintenance user" using password authentication. Can
310 # only be used on particular machines (those in a list).
311 sources = method.api.config.PLC_API_MAINTENANCE_SOURCES.split()
312 if method.source is not None and method.source[0] not in sources:
313 raise PLCAuthenticationFailure, "Not allowed to login to maintenance account"
315 # Not sure why this is not stored in the DB
316 password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
318 if auth['AuthString'] != password:
319 raise PLCAuthenticationFailure, "Maintenance account password verification failed"
321 # Compare encrypted plaintext against encrypted password stored in the DB
322 plaintext = auth['AuthString'].encode(method.api.encoding)
323 password = person['password']
325 # Protect against blank passwords in the DB
326 if password is None or password[:12] == "" or \
327 crypt.crypt(plaintext, password[:12]) != password:
328 raise PLCAuthenticationFailure, "Password verification failed"
330 if not set(person['roles']).intersection(method.roles):
331 raise PLCAuthenticationFailure, "Not allowed to call method"
333 method.caller = person