2 # PLCAPI authentication parameters
4 # Mark Huang <mlhuang@cs.princeton.edu>
5 # Copyright (C) 2006 The Trustees of Princeton University
16 from PLC.Faults import *
17 from PLC.Parameter import Parameter, Mixed
18 from PLC.Persons import Persons
19 from PLC.Nodes import Node, Nodes
20 from PLC.Interfaces import Interface, Interfaces
21 from PLC.Sessions import Session, Sessions
22 from PLC.Peers import Peer, Peers
23 from PLC.Boot import notify_owners
26 if auth['AuthMethod'] == "session":
27 expected = SessionAuth()
28 elif auth['AuthMethod'] == "password" or \
29 auth['AuthMethod'] == "capability":
30 expected = PasswordAuth()
31 elif auth['AuthMethod'] == "gpg":
33 elif auth['AuthMethod'] == "hmac" or \
34 auth['AuthMethod'] == "hmac_dummybox":
36 elif auth['AuthMethod'] == "anonymous":
37 expected = AnonymousAuth()
39 raise PLCInvalidArgument("must be 'session', 'password', 'gpg', 'hmac', 'hmac_dummybox', or 'anonymous'", "AuthMethod")
42 class Auth(Parameter):
44 Base class for all API authentication methods, as well as a class
45 that can be used to represent all supported API authentication
49 def __init__(self, auth = None):
51 auth = {'AuthMethod': Parameter(str, "Authentication method to use", optional = False)}
52 Parameter.__init__(self, auth, "API authentication structure")
54 def check(self, method, auth, *args):
55 # Method.type_check() should have checked that all of the
56 # mandatory fields were present.
57 assert 'AuthMethod' in auth
59 expected = map_auth(auth)
61 # Re-check using the specified authentication method
62 method.type_check("auth", auth, expected, (auth,) + args)
66 Proposed PlanetLab federation authentication structure.
71 'AuthMethod': Parameter(str, "Authentication method to use, always 'gpg'", optional = False),
72 'name': Parameter(str, "Peer or user name", optional = False),
73 'signature': Parameter(str, "Message signature", optional = False)
76 def check(self, method, auth, *args):
78 peers = Peers(method.api, [auth['name']])
80 if 'peer' not in method.roles:
81 raise PLCAuthenticationFailure, "Not allowed to call method"
83 method.caller = peer = peers[0]
86 persons = Persons(method.api, {'email': auth['name'], 'enabled': True, 'peer_id': None})
88 raise PLCAuthenticationFailure, "No such user '%s'" % auth['name']
90 if not set(person['roles']).intersection(method.roles):
91 raise PLCAuthenticationFailure, "Not allowed to call method"
93 method.caller = person = persons[0]
94 keys = Keys(method.api, {'key_id': person['key_ids'], 'key_type': "gpg", 'peer_id': None})
97 raise PLCAuthenticationFailure, "No GPG key on record for peer or user '%s'"
101 from PLC.GPG import gpg_verify
102 gpg_verify(args, key, auth['signature'], method.name)
104 except PLCAuthenticationFailure, fault:
109 except PLCAuthenticationFailure, fault:
113 class SessionAuth(Auth):
115 Secondary authentication method. After authenticating with a
116 primary authentication method, call GetSession() to generate a
117 session key that may be used for subsequent calls.
121 Auth.__init__(self, {
122 'AuthMethod': Parameter(str, "Authentication method to use, always 'session'", optional = False),
123 'session': Parameter(str, "Session key", optional = False)
126 def check(self, method, auth, *args):
127 # Method.type_check() should have checked that all of the
128 # mandatory fields were present.
129 assert auth.has_key('session')
132 sessions = Sessions(method.api, [auth['session']], expires = None)
134 raise PLCAuthenticationFailure, "No such session"
135 session = sessions[0]
138 if session['node_id'] is not None:
139 nodes = Nodes(method.api, {'node_id': session['node_id'], 'peer_id': None})
141 raise PLCAuthenticationFailure, "No such node"
144 if 'node' not in method.roles:
145 raise PLCAuthenticationFailure, "Not allowed to call method"
149 elif session['person_id'] is not None and session['expires'] > time.time():
150 persons = Persons(method.api, {'person_id': session['person_id'], 'enabled': True, 'peer_id': None})
152 raise PLCAuthenticationFailure, "No such account"
155 if not set(person['roles']).intersection(method.roles):
156 raise PLCPermissionDenied, "Not allowed to call method"
158 method.caller = persons[0]
161 raise PLCAuthenticationFailure, "Invalid session"
163 except PLCAuthenticationFailure, fault:
167 class BootAuth(Auth):
169 PlanetLab version 3.x node authentication structure. Used by the
170 Boot Manager to make authenticated calls to the API based on a
171 unique node key or boot nonce value.
173 The original parameter serialization code did not define the byte
174 encoding of strings, or the string encoding of all other types. We
175 define the byte encoding to be UTF-8, and the string encoding of
176 all other types to be however Python version 2.3 unicode() encodes
181 Auth.__init__(self, {
182 'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional = False),
183 'node_id': Parameter(int, "Node identifier", optional = False),
184 'value': Parameter(str, "HMAC of node key and method call", optional = False)
187 def canonicalize(self, args):
191 if isinstance(arg, list) or isinstance(arg, tuple):
192 # The old implementation did not recursively handle
193 # lists of lists. But neither did the old API itself.
194 values += self.canonicalize(arg)
195 elif isinstance(arg, dict):
196 # Yes, the comments in the old implementation are
197 # misleading. Keys of dicts are not included in the
199 values += self.canonicalize(arg.values())
201 # We use unicode() instead of str().
202 values.append(unicode(arg))
206 def check(self, method, auth, *args):
207 # Method.type_check() should have checked that all of the
208 # mandatory fields were present.
209 assert auth.has_key('node_id')
211 if 'node' not in method.roles:
212 raise PLCAuthenticationFailure, "Not allowed to call method"
215 nodes = Nodes(method.api, {'node_id': auth['node_id'], 'peer_id': None})
217 raise PLCAuthenticationFailure, "No such node"
222 elif node['boot_nonce']:
223 # Allow very old nodes that do not have a node key in
224 # their configuration files to use their "boot nonce"
225 # instead. The boot nonce is a random value generated
226 # by the node itself and POSTed by the Boot CD when it
227 # requests the Boot Manager. This is obviously not
228 # very secure, so we only allow it to be used if the
229 # requestor IP is the same as the IP address we have
230 # on record for the node.
231 key = node['boot_nonce']
234 if node['interface_ids']:
235 interfaces = Interfaces(method.api, node['interface_ids'])
236 for interface in interfaces:
237 if interface['is_primary']:
240 if not interface or not interface['is_primary']:
241 raise PLCAuthenticationFailure, "No primary network interface on record"
243 if method.source is None:
244 raise PLCAuthenticationFailure, "Cannot determine IP address of requestor"
246 if interface['ip'] != method.source[0]:
247 raise PLCAuthenticationFailure, "Requestor IP %s does not match node IP %s" % \
248 (method.source[0], interface['ip'])
250 raise PLCAuthenticationFailure, "No node key or boot nonce"
252 # Yes, this is the "canonicalization" method used.
253 args = self.canonicalize(args)
255 msg = "[" + "".join(args) + "]"
257 # We encode in UTF-8 before calculating the HMAC, which is
258 # an 8-bit algorithm.
259 # python 2.6 insists on receiving a 'str' as opposed to a 'unicode'
260 digest = hmac.new(str(key), msg.encode('utf-8'), sha).hexdigest()
262 if digest != auth['value']:
263 raise PLCAuthenticationFailure, "Call could not be authenticated"
267 except PLCAuthenticationFailure, fault:
269 notify_owners(method, node, 'authfail', include_pis = True, include_techs = True, fault = fault)
272 class AnonymousAuth(Auth):
274 PlanetLab version 3.x anonymous authentication structure.
278 Auth.__init__(self, {
279 'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
282 def check(self, method, auth, *args):
283 if 'anonymous' not in method.roles:
284 raise PLCAuthenticationFailure, "Not allowed to call method anonymously"
288 class PasswordAuth(Auth):
290 PlanetLab version 3.x password authentication structure.
294 Auth.__init__(self, {
295 'AuthMethod': Parameter(str, "Authentication method to use, always 'password' or 'capability'", optional = False),
296 'Username': Parameter(str, "PlanetLab username, typically an e-mail address", optional = False),
297 'AuthString': Parameter(str, "Authentication string, typically a password", optional = False),
300 def check(self, method, auth, *args):
301 # Method.type_check() should have checked that all of the
302 # mandatory fields were present.
303 assert auth.has_key('Username')
305 # Get record (must be enabled)
306 persons = Persons(method.api, {'email': auth['Username'].lower(), 'enabled': True, 'peer_id': None})
307 if len(persons) != 1:
308 raise PLCAuthenticationFailure, "No such account"
312 if auth['Username'] == method.api.config.PLC_API_MAINTENANCE_USER:
313 # "Capability" authentication, whatever the hell that was
314 # supposed to mean. It really means, login as the special
315 # "maintenance user" using password authentication. Can
316 # only be used on particular machines (those in a list).
317 sources = method.api.config.PLC_API_MAINTENANCE_SOURCES.split()
318 if method.source is not None and method.source[0] not in sources:
319 raise PLCAuthenticationFailure, "Not allowed to login to maintenance account"
321 # Not sure why this is not stored in the DB
322 password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
324 if auth['AuthString'] != password:
325 raise PLCAuthenticationFailure, "Maintenance account password verification failed"
327 # Compare encrypted plaintext against encrypted password stored in the DB
328 plaintext = auth['AuthString'].encode(method.api.encoding)
329 password = person['password']
331 # Protect against blank passwords in the DB
332 if password is None or password[:12] == "" or \
333 crypt.crypt(plaintext, password[:12]) != password:
334 raise PLCAuthenticationFailure, "Password verification failed"
336 if not set(person['roles']).intersection(method.roles):
337 raise PLCAuthenticationFailure, "Not allowed to call method"
339 method.caller = person