2 # PLCAPI authentication parameters
4 # Mark Huang <mlhuang@cs.princeton.edu>
5 # Copyright (C) 2006 The Trustees of Princeton University
7 # $Id: Auth.py,v 1.17 2007/02/10 18:39:09 mlhuang Exp $
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.NodeNetworks import NodeNetwork, NodeNetworks
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":
50 elif auth['AuthMethod'] == "anonymous":
51 expected = AnonymousAuth()
53 raise PLCInvalidArgument("must be 'session', 'password', 'gpg', 'hmac', or 'anonymous'", "AuthMethod")
55 # Re-check using the specified authentication method
56 method.type_check("auth", auth, expected, (auth,) + args)
60 Proposed PlanetLab federation authentication structure.
65 'AuthMethod': Parameter(str, "Authentication method to use, always 'gpg'", optional = False),
66 'name': Parameter(str, "Peer or user name", optional = False),
67 'signature': Parameter(str, "Message signature", optional = False)
70 def check(self, method, auth, *args):
72 peers = Peers(method.api, [auth['name']])
74 if 'peer' not in method.roles:
75 raise PLCAuthenticationFailure, "Not allowed to call method"
77 method.caller = peer = peers[0]
80 persons = Persons(method.api, {'email': auth['name'], 'enabled': True, 'peer_id': None})
82 raise PLCAuthenticationFailure, "No such user '%s'" % auth['name']
84 if not set(person['roles']).intersection(method.roles):
85 raise PLCAuthenticationFailure, "Not allowed to call method"
87 method.caller = person = persons[0]
88 keys = Keys(method.api, {'key_id': person['key_ids'], 'key_type': "gpg", 'peer_id': None})
91 raise PLCAuthenticationFailure, "No GPG key on record for peer or user '%s'"
95 from PLC.GPG import gpg_verify
96 gpg_verify(args, key, auth['signature'], method.name)
98 except PLCAuthenticationFailure, fault:
103 except PLCAuthenticationFailure, fault:
107 class SessionAuth(Auth):
109 Secondary authentication method. After authenticating with a
110 primary authentication method, call GetSession() to generate a
111 session key that may be used for subsequent calls.
115 Auth.__init__(self, {
116 'AuthMethod': Parameter(str, "Authentication method to use, always 'session'", optional = False),
117 'session': Parameter(str, "Session key", optional = False)
120 def check(self, method, auth, *args):
121 # Method.type_check() should have checked that all of the
122 # mandatory fields were present.
123 assert auth.has_key('session')
126 sessions = Sessions(method.api, [auth['session']], expires = None)
128 raise PLCAuthenticationFailure, "No such session"
129 session = sessions[0]
132 if session['node_id'] is not None:
133 nodes = Nodes(method.api, {'node_id': session['node_id'], 'peer_id': None})
135 raise PLCAuthenticationFailure, "No such node"
138 if 'node' not in method.roles:
139 raise PLCAuthenticationFailure, "Not allowed to call method"
143 elif session['person_id'] is not None and session['expires'] > time.time():
144 persons = Persons(method.api, {'person_id': session['person_id'], 'enabled': True, 'peer_id': None})
146 raise PLCAuthenticationFailure, "No such account"
149 if not set(person['roles']).intersection(method.roles):
150 raise PLCPermissionDenied, "Not allowed to call method"
152 method.caller = persons[0]
155 raise PLCAuthenticationFailure, "Invalid session"
157 except PLCAuthenticationFailure, fault:
161 class BootAuth(Auth):
163 PlanetLab version 3.x node authentication structure. Used by the
164 Boot Manager to make authenticated calls to the API based on a
165 unique node key or boot nonce value.
167 The original parameter serialization code did not define the byte
168 encoding of strings, or the string encoding of all other types. We
169 define the byte encoding to be UTF-8, and the string encoding of
170 all other types to be however Python version 2.3 unicode() encodes
175 Auth.__init__(self, {
176 'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional = False),
177 'node_id': Parameter(int, "Node identifier", optional = False),
178 'value': Parameter(str, "HMAC of node key and method call", optional = False)
181 def canonicalize(self, args):
185 if isinstance(arg, list) or isinstance(arg, tuple):
186 # The old implementation did not recursively handle
187 # lists of lists. But neither did the old API itself.
188 values += self.canonicalize(arg)
189 elif isinstance(arg, dict):
190 # Yes, the comments in the old implementation are
191 # misleading. Keys of dicts are not included in the
193 values += self.canonicalize(arg.values())
195 # We use unicode() instead of str().
196 values.append(unicode(arg))
200 def check(self, method, auth, *args):
201 # Method.type_check() should have checked that all of the
202 # mandatory fields were present.
203 assert auth.has_key('node_id')
205 if 'node' not in method.roles:
206 raise PLCAuthenticationFailure, "Not allowed to call method"
209 nodes = Nodes(method.api, {'node_id': auth['node_id'], 'peer_id': None})
211 raise PLCAuthenticationFailure, "No such node"
216 elif node['boot_nonce']:
217 # Allow very old nodes that do not have a node key in
218 # their configuration files to use their "boot nonce"
219 # instead. The boot nonce is a random value generated
220 # by the node itself and POSTed by the Boot CD when it
221 # requests the Boot Manager. This is obviously not
222 # very secure, so we only allow it to be used if the
223 # requestor IP is the same as the IP address we have
224 # on record for the node.
225 key = node['boot_nonce']
228 if node['nodenetwork_ids']:
229 nodenetworks = NodeNetworks(method.api, node['nodenetwork_ids'])
230 for nodenetwork in nodenetworks:
231 if nodenetwork['is_primary']:
234 if not nodenetwork or not nodenetwork['is_primary']:
235 raise PLCAuthenticationFailure, "No primary network interface on record"
237 if method.source is None:
238 raise PLCAuthenticationFailure, "Cannot determine IP address of requestor"
240 if nodenetwork['ip'] != method.source[0]:
241 raise PLCAuthenticationFailure, "Requestor IP %s does not match node IP %s" % \
242 (method.source[0], nodenetwork['ip'])
244 raise PLCAuthenticationFailure, "No node key or boot nonce"
246 # Yes, this is the "canonicalization" method used.
247 args = self.canonicalize(args)
249 msg = "[" + "".join(args) + "]"
251 # We encode in UTF-8 before calculating the HMAC, which is
252 # an 8-bit algorithm.
253 digest = hmac.new(key, msg.encode('utf-8'), sha).hexdigest()
255 if digest != auth['value']:
256 raise PLCAuthenticationFailure, "Call could not be authenticated"
260 except PLCAuthenticationFailure, fault:
262 notify_owners(method, node, 'authfail', include_pis = True, include_techs = True, fault = fault)
265 class AnonymousAuth(Auth):
267 PlanetLab version 3.x anonymous authentication structure.
271 Auth.__init__(self, {
272 'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
275 def check(self, method, auth, *args):
276 if 'anonymous' not in method.roles:
277 raise PLCAuthenticationFailure, "Not allowed to call method anonymously"
281 class PasswordAuth(Auth):
283 PlanetLab version 3.x password authentication structure.
287 Auth.__init__(self, {
288 'AuthMethod': Parameter(str, "Authentication method to use, always 'password' or 'capability'", optional = False),
289 'Username': Parameter(str, "PlanetLab username, typically an e-mail address", optional = False),
290 'AuthString': Parameter(str, "Authentication string, typically a password", optional = False),
293 def check(self, method, auth, *args):
294 # Method.type_check() should have checked that all of the
295 # mandatory fields were present.
296 assert auth.has_key('Username')
298 # Get record (must be enabled)
299 persons = Persons(method.api, {'email': auth['Username'].lower(), 'enabled': True, 'peer_id': None})
300 if len(persons) != 1:
301 raise PLCAuthenticationFailure, "No such account"
305 if auth['Username'] == method.api.config.PLC_API_MAINTENANCE_USER:
306 # "Capability" authentication, whatever the hell that was
307 # supposed to mean. It really means, login as the special
308 # "maintenance user" using password authentication. Can
309 # only be used on particular machines (those in a list).
310 sources = method.api.config.PLC_API_MAINTENANCE_SOURCES.split()
311 if method.source is not None and method.source[0] not in sources:
312 raise PLCAuthenticationFailure, "Not allowed to login to maintenance account"
314 # Not sure why this is not stored in the DB
315 password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
317 if auth['AuthString'] != password:
318 raise PLCAuthenticationFailure, "Maintenance account password verification failed"
320 # Compare encrypted plaintext against encrypted password stored in the DB
321 plaintext = auth['AuthString'].encode(method.api.encoding)
322 password = person['password']
324 # Protect against blank passwords in the DB
325 if password is None or password[:12] == "" or \
326 crypt.crypt(plaintext, password[:12]) != password:
327 raise PLCAuthenticationFailure, "Password verification failed"
329 if not set(person['roles']).intersection(method.roles):
330 raise PLCAuthenticationFailure, "Not allowed to call method"
332 method.caller = person