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
25 if auth['AuthMethod'] == "session":
26 expected = SessionAuth()
27 elif auth['AuthMethod'] == "password" or \
28 auth['AuthMethod'] == "capability":
29 expected = PasswordAuth()
30 elif auth['AuthMethod'] == "gpg":
32 elif auth['AuthMethod'] == "hmac" or \
33 auth['AuthMethod'] == "hmac_dummybox":
35 elif auth['AuthMethod'] == "anonymous":
36 expected = AnonymousAuth()
38 raise PLCInvalidArgument("must be 'session', 'password', 'gpg', 'hmac', 'hmac_dummybox', or 'anonymous'", "AuthMethod")
41 class Auth(Parameter):
43 Base class for all API authentication methods, as well as a class
44 that can be used to represent all supported API authentication
48 def __init__(self, auth = None):
50 auth = {'AuthMethod': Parameter(str, "Authentication method to use", optional = False)}
51 Parameter.__init__(self, auth, "API authentication structure")
53 def check(self, method, auth, *args):
54 # Method.type_check() should have checked that all of the
55 # mandatory fields were present.
56 assert 'AuthMethod' in auth
58 expected = map_auth(auth)
60 # Re-check using the specified authentication method
61 method.type_check("auth", auth, expected, (auth,) + args)
65 Proposed PlanetLab federation authentication structure.
70 'AuthMethod': Parameter(str, "Authentication method to use, always 'gpg'", optional = False),
71 'name': Parameter(str, "Peer or user name", optional = False),
72 'signature': Parameter(str, "Message signature", optional = False)
75 def check(self, method, auth, *args):
77 peers = Peers(method.api, [auth['name']])
79 if 'peer' not in method.roles:
80 raise PLCAuthenticationFailure, "Not allowed to call method"
82 method.caller = peer = peers[0]
85 persons = Persons(method.api, {'email': auth['name'], 'enabled': True, 'peer_id': None})
87 raise PLCAuthenticationFailure, "No such user '%s'" % auth['name']
89 if not set(person['roles']).intersection(method.roles):
90 raise PLCAuthenticationFailure, "Not allowed to call method"
92 method.caller = person = persons[0]
93 keys = Keys(method.api, {'key_id': person['key_ids'], 'key_type': "gpg", 'peer_id': None})
96 raise PLCAuthenticationFailure, "No GPG key on record for peer or user '%s'"
100 from PLC.GPG import gpg_verify
101 gpg_verify(args, key, auth['signature'], method.name)
103 except PLCAuthenticationFailure, fault:
108 except PLCAuthenticationFailure, fault:
112 class SessionAuth(Auth):
114 Secondary authentication method. After authenticating with a
115 primary authentication method, call GetSession() to generate a
116 session key that may be used for subsequent calls.
120 Auth.__init__(self, {
121 'AuthMethod': Parameter(str, "Authentication method to use, always 'session'", optional = False),
122 'session': Parameter(str, "Session key", optional = False)
125 def check(self, method, auth, *args):
126 # Method.type_check() should have checked that all of the
127 # mandatory fields were present.
128 assert auth.has_key('session')
131 sessions = Sessions(method.api, [auth['session']], expires = None)
133 raise PLCAuthenticationFailure, "No such session"
134 session = sessions[0]
137 if session['node_id'] is not None:
138 nodes = Nodes(method.api, {'node_id': session['node_id'], 'peer_id': None})
140 raise PLCAuthenticationFailure, "No such node"
143 if 'node' not in method.roles:
144 raise PLCAuthenticationFailure, "Not allowed to call method"
148 elif session['person_id'] is not None and session['expires'] > time.time():
149 persons = Persons(method.api, {'person_id': session['person_id'], 'enabled': True, 'peer_id': None})
151 raise PLCAuthenticationFailure, "No such account"
154 if not set(person['roles']).intersection(method.roles):
155 raise PLCPermissionDenied, "Not allowed to call method"
157 method.caller = persons[0]
160 raise PLCAuthenticationFailure, "Invalid session"
162 except PLCAuthenticationFailure, fault:
166 class BootAuth(Auth):
168 PlanetLab version 3.x node authentication structure. Used by the
169 Boot Manager to make authenticated calls to the API based on a
170 unique node key or boot nonce value.
172 The original parameter serialization code did not define the byte
173 encoding of strings, or the string encoding of all other types. We
174 define the byte encoding to be UTF-8, and the string encoding of
175 all other types to be however Python version 2.3 unicode() encodes
180 Auth.__init__(self, {
181 'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional = False),
182 'node_id': Parameter(int, "Node identifier", optional = False),
183 'value': Parameter(str, "HMAC of node key and method call", optional = False)
186 def canonicalize(self, args):
190 if isinstance(arg, list) or isinstance(arg, tuple):
191 # The old implementation did not recursively handle
192 # lists of lists. But neither did the old API itself.
193 values += self.canonicalize(arg)
194 elif isinstance(arg, dict):
195 # Yes, the comments in the old implementation are
196 # misleading. Keys of dicts are not included in the
198 values += self.canonicalize(arg.values())
200 # We use unicode() instead of str().
201 values.append(unicode(arg))
205 def check(self, method, auth, *args):
206 # Method.type_check() should have checked that all of the
207 # mandatory fields were present.
208 assert auth.has_key('node_id')
210 if 'node' not in method.roles:
211 raise PLCAuthenticationFailure, "Not allowed to call method"
214 nodes = Nodes(method.api, {'node_id': auth['node_id'], 'peer_id': None})
216 raise PLCAuthenticationFailure, "No such node"
221 elif node['boot_nonce']:
222 # Allow very old nodes that do not have a node key in
223 # their configuration files to use their "boot nonce"
224 # instead. The boot nonce is a random value generated
225 # by the node itself and POSTed by the Boot CD when it
226 # requests the Boot Manager. This is obviously not
227 # very secure, so we only allow it to be used if the
228 # requestor IP is the same as the IP address we have
229 # on record for the node.
230 key = node['boot_nonce']
233 if node['interface_ids']:
234 interfaces = Interfaces(method.api, node['interface_ids'])
235 for interface in interfaces:
236 if interface['is_primary']:
239 if not interface or not interface['is_primary']:
240 raise PLCAuthenticationFailure, "No primary network interface on record"
242 if method.source is None:
243 raise PLCAuthenticationFailure, "Cannot determine IP address of requestor"
245 if interface['ip'] != method.source[0]:
246 raise PLCAuthenticationFailure, "Requestor IP %s does not match node IP %s" % \
247 (method.source[0], interface['ip'])
249 raise PLCAuthenticationFailure, "No node key or boot nonce"
251 # Yes, this is the "canonicalization" method used.
252 args = self.canonicalize(args)
254 msg = "[" + "".join(args) + "]"
256 # We encode in UTF-8 before calculating the HMAC, which is
257 # an 8-bit algorithm.
258 digest = hmac.new(key, msg.encode('utf-8'), sha).hexdigest()
260 if digest != auth['value']:
261 raise PLCAuthenticationFailure, "Call could not be authenticated"
265 except PLCAuthenticationFailure, fault:
267 notify_owners(method, node, 'authfail', include_pis = True, include_techs = True, fault = fault)
270 class AnonymousAuth(Auth):
272 PlanetLab version 3.x anonymous authentication structure.
276 Auth.__init__(self, {
277 'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
280 def check(self, method, auth, *args):
281 if 'anonymous' not in method.roles:
282 raise PLCAuthenticationFailure, "Not allowed to call method anonymously"
286 class PasswordAuth(Auth):
288 PlanetLab version 3.x password authentication structure.
292 Auth.__init__(self, {
293 'AuthMethod': Parameter(str, "Authentication method to use, always 'password' or 'capability'", optional = False),
294 'Username': Parameter(str, "PlanetLab username, typically an e-mail address", optional = False),
295 'AuthString': Parameter(str, "Authentication string, typically a password", optional = False),
298 def check(self, method, auth, *args):
299 # Method.type_check() should have checked that all of the
300 # mandatory fields were present.
301 assert auth.has_key('Username')
303 # Get record (must be enabled)
304 persons = Persons(method.api, {'email': auth['Username'].lower(), 'enabled': True, 'peer_id': None})
305 if len(persons) != 1:
306 raise PLCAuthenticationFailure, "No such account"
310 if auth['Username'] == method.api.config.PLC_API_MAINTENANCE_USER:
311 # "Capability" authentication, whatever the hell that was
312 # supposed to mean. It really means, login as the special
313 # "maintenance user" using password authentication. Can
314 # only be used on particular machines (those in a list).
315 sources = method.api.config.PLC_API_MAINTENANCE_SOURCES.split()
316 if method.source is not None and method.source[0] not in sources:
317 raise PLCAuthenticationFailure, "Not allowed to login to maintenance account"
319 # Not sure why this is not stored in the DB
320 password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
322 if auth['AuthString'] != password:
323 raise PLCAuthenticationFailure, "Maintenance account password verification failed"
325 # Compare encrypted plaintext against encrypted password stored in the DB
326 plaintext = auth['AuthString'].encode(method.api.encoding)
327 password = person['password']
329 # Protect against blank passwords in the DB
330 if password is None or password[:12] == "" or \
331 crypt.crypt(plaintext, password[:12]) != password:
332 raise PLCAuthenticationFailure, "Password verification failed"
334 if not set(person['roles']).intersection(method.roles):
335 raise PLCAuthenticationFailure, "Not allowed to call method"
337 method.caller = person