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")
40 class Auth(Parameter):
42 Base class for all API authentication methods, as well as a class
43 that can be used to represent all supported API authentication
47 def __init__(self, auth = None):
49 auth = {'AuthMethod': Parameter(str, "Authentication method to use", optional = False)}
50 Parameter.__init__(self, auth, "API authentication structure")
52 def check(self, method, auth, *args):
53 # Method.type_check() should have checked that all of the
54 # mandatory fields were present.
55 assert 'AuthMethod' in auth
57 expected = map_auth(auth)
59 # Re-check using the specified authentication method
60 method.type_check("auth", auth, expected, (auth,) + args)
64 Proposed PlanetLab federation authentication structure.
69 'AuthMethod': Parameter(str, "Authentication method to use, always 'gpg'", optional = False),
70 'name': Parameter(str, "Peer or user name", optional = False),
71 'signature': Parameter(str, "Message signature", optional = False)
74 def check(self, method, auth, *args):
76 peers = Peers(method.api, [auth['name']])
78 if 'peer' not in method.roles:
79 raise PLCAuthenticationFailure, "Not allowed to call method"
81 method.caller = peer = peers[0]
84 persons = Persons(method.api, {'email': auth['name'], 'enabled': True, 'peer_id': None})
86 raise PLCAuthenticationFailure, "No such user '%s'" % auth['name']
88 if not set(person['roles']).intersection(method.roles):
89 raise PLCAuthenticationFailure, "Not allowed to call method"
91 method.caller = person = persons[0]
92 keys = Keys(method.api, {'key_id': person['key_ids'], 'key_type': "gpg", 'peer_id': None})
95 raise PLCAuthenticationFailure, "No GPG key on record for peer or user '%s'"
99 from PLC.GPG import gpg_verify
100 gpg_verify(args, key, auth['signature'], method.name)
102 except PLCAuthenticationFailure, fault:
107 except PLCAuthenticationFailure, fault:
111 class SessionAuth(Auth):
113 Secondary authentication method. After authenticating with a
114 primary authentication method, call GetSession() to generate a
115 session key that may be used for subsequent calls.
119 Auth.__init__(self, {
120 'AuthMethod': Parameter(str, "Authentication method to use, always 'session'", optional = False),
121 'session': Parameter(str, "Session key", optional = False)
124 def check(self, method, auth, *args):
125 # Method.type_check() should have checked that all of the
126 # mandatory fields were present.
127 assert auth.has_key('session')
130 sessions = Sessions(method.api, [auth['session']], expires = None)
132 raise PLCAuthenticationFailure, "No such session"
133 session = sessions[0]
136 if session['node_id'] is not None:
137 nodes = Nodes(method.api, {'node_id': session['node_id'], 'peer_id': None})
139 raise PLCAuthenticationFailure, "No such node"
142 if 'node' not in method.roles:
143 raise PLCAuthenticationFailure, "Not allowed to call method"
147 elif session['person_id'] is not None and session['expires'] > time.time():
148 persons = Persons(method.api, {'person_id': session['person_id'], 'enabled': True, 'peer_id': None})
150 raise PLCAuthenticationFailure, "No such account"
153 if not set(person['roles']).intersection(method.roles):
154 raise PLCPermissionDenied, "Not allowed to call method"
156 method.caller = persons[0]
159 raise PLCAuthenticationFailure, "Invalid session"
161 except PLCAuthenticationFailure, fault:
165 class BootAuth(Auth):
167 PlanetLab version 3.x node authentication structure. Used by the
168 Boot Manager to make authenticated calls to the API based on a
169 unique node key or boot nonce value.
171 The original parameter serialization code did not define the byte
172 encoding of strings, or the string encoding of all other types. We
173 define the byte encoding to be UTF-8, and the string encoding of
174 all other types to be however Python version 2.3 unicode() encodes
179 Auth.__init__(self, {
180 'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional = False),
181 'node_id': Parameter(int, "Node identifier", optional = False),
182 'value': Parameter(str, "HMAC of node key and method call", optional = False)
185 def canonicalize(self, args):
189 if isinstance(arg, list) or isinstance(arg, tuple):
190 # The old implementation did not recursively handle
191 # lists of lists. But neither did the old API itself.
192 values += self.canonicalize(arg)
193 elif isinstance(arg, dict):
194 # Yes, the comments in the old implementation are
195 # misleading. Keys of dicts are not included in the
197 values += self.canonicalize(arg.values())
199 # We use unicode() instead of str().
200 values.append(unicode(arg))
204 def check(self, method, auth, *args):
205 # Method.type_check() should have checked that all of the
206 # mandatory fields were present.
207 assert auth.has_key('node_id')
209 if 'node' not in method.roles:
210 raise PLCAuthenticationFailure, "Not allowed to call method"
213 nodes = Nodes(method.api, {'node_id': auth['node_id'], 'peer_id': None})
215 raise PLCAuthenticationFailure, "No such node"
220 elif node['boot_nonce']:
221 # Allow very old nodes that do not have a node key in
222 # their configuration files to use their "boot nonce"
223 # instead. The boot nonce is a random value generated
224 # by the node itself and POSTed by the Boot CD when it
225 # requests the Boot Manager. This is obviously not
226 # very secure, so we only allow it to be used if the
227 # requestor IP is the same as the IP address we have
228 # on record for the node.
229 key = node['boot_nonce']
232 if node['interface_ids']:
233 interfaces = Interfaces(method.api, node['interface_ids'])
234 for interface in interfaces:
235 if interface['is_primary']:
238 if not interface or not interface['is_primary']:
239 raise PLCAuthenticationFailure, "No primary network interface on record"
241 if method.source is None:
242 raise PLCAuthenticationFailure, "Cannot determine IP address of requestor"
244 if interface['ip'] != method.source[0]:
245 raise PLCAuthenticationFailure, "Requestor IP %s does not match node IP %s" % \
246 (method.source[0], interface['ip'])
248 raise PLCAuthenticationFailure, "No node key or boot nonce"
250 # Yes, this is the "canonicalization" method used.
251 args = self.canonicalize(args)
253 msg = "[" + "".join(args) + "]"
255 # We encode in UTF-8 before calculating the HMAC, which is
256 # an 8-bit algorithm.
257 digest = hmac.new(key, msg.encode('utf-8'), sha).hexdigest()
259 if digest != auth['value']:
260 raise PLCAuthenticationFailure, "Call could not be authenticated"
264 except PLCAuthenticationFailure, fault:
266 notify_owners(method, node, 'authfail', include_pis = True, include_techs = True, fault = fault)
269 class AnonymousAuth(Auth):
271 PlanetLab version 3.x anonymous authentication structure.
275 Auth.__init__(self, {
276 'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
279 def check(self, method, auth, *args):
280 if 'anonymous' not in method.roles:
281 raise PLCAuthenticationFailure, "Not allowed to call method anonymously"
285 class PasswordAuth(Auth):
287 PlanetLab version 3.x password authentication structure.
291 Auth.__init__(self, {
292 'AuthMethod': Parameter(str, "Authentication method to use, always 'password' or 'capability'", optional = False),
293 'Username': Parameter(str, "PlanetLab username, typically an e-mail address", optional = False),
294 'AuthString': Parameter(str, "Authentication string, typically a password", optional = False),
297 def check(self, method, auth, *args):
298 # Method.type_check() should have checked that all of the
299 # mandatory fields were present.
300 assert auth.has_key('Username')
302 # Get record (must be enabled)
303 persons = Persons(method.api, {'email': auth['Username'].lower(), 'enabled': True, 'peer_id': None})
304 if len(persons) != 1:
305 raise PLCAuthenticationFailure, "No such account"
309 if auth['Username'] == method.api.config.PLC_API_MAINTENANCE_USER:
310 # "Capability" authentication, whatever the hell that was
311 # supposed to mean. It really means, login as the special
312 # "maintenance user" using password authentication. Can
313 # only be used on particular machines (those in a list).
314 sources = method.api.config.PLC_API_MAINTENANCE_SOURCES.split()
315 if method.source is not None and method.source[0] not in sources:
316 raise PLCAuthenticationFailure, "Not allowed to login to maintenance account"
318 # Not sure why this is not stored in the DB
319 password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
321 if auth['AuthString'] != password:
322 raise PLCAuthenticationFailure, "Maintenance account password verification failed"
324 # Compare encrypted plaintext against encrypted password stored in the DB
325 plaintext = auth['AuthString'].encode(method.api.encoding)
326 password = person['password']
328 # Protect against blank passwords in the DB
329 if password is None or password[:12] == "" or \
330 crypt.crypt(plaintext, password[:12]) != password:
331 raise PLCAuthenticationFailure, "Password verification failed"
333 if not set(person['roles']).intersection(method.roles):
334 raise PLCAuthenticationFailure, "Not allowed to call method"
336 method.caller = person