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.10 2007/01/04 16:01:28 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.Sessions import Session, Sessions
20 from PLC.Peers import Peer, Peers
22 class Auth(Parameter):
24 Base class for all API authentication methods, as well as a class
25 that can be used to represent Mixed(SessionAuth(), PasswordAuth(),
26 GPGAuth()), i.e. the three principal API authentication methods.
29 def __init__(self, auth = {}):
30 Parameter.__init__(self, auth, "API authentication structure")
32 def check(self, method, auth, *args):
33 method.type_check("auth", auth,
34 Mixed(SessionAuth(), PasswordAuth(), GPGAuth()),
39 Proposed PlanetLab federation authentication structure.
44 'AuthMethod': Parameter(str, "Authentication method to use, always 'gpg'", optional = False),
45 'name': Parameter(str, "Peer or user name", optional = False),
46 'signature': Parameter(str, "Message signature", optional = False)
49 def check(self, method, auth, *args):
51 peers = Peers(method.api, [auth['name']])
53 if 'peer' not in method.roles:
54 raise PLCAuthenticationFailure, "Not allowed to call method"
56 method.caller = peer = peers[0]
59 persons = Persons(method.api, {'email': auth['name'], 'enabled': True, 'peer_id': None})
61 raise PLCAuthenticationFailure, "No such user '%s'" % auth['name']
63 if not set(person['roles']).intersection(method.roles):
64 raise PLCAuthenticationFailure, "Not allowed to call method"
66 method.caller = person = persons[0]
67 keys = Keys(method.api, {'key_id': person['key_ids'], 'key_type': "gpg", 'peer_id': None})
70 raise PLCAuthenticationFailure, "No GPG key on record for peer or user '%s'"
74 from PLC.GPG import gpg_verify
75 gpg_verify(method.name, args, auth['signature'], key)
77 except PLCAuthenticationFailure, fault:
82 except PLCAuthenticationFailure, fault:
86 class SessionAuth(Auth):
88 Secondary authentication method. After authenticating with a
89 primary authentication method, call GetSession() to generate a
90 session key that may be used for subsequent calls.
95 'AuthMethod': Parameter(str, "Authentication method to use, always 'session'", optional = False),
96 'session': Parameter(str, "Session key", optional = False)
99 def check(self, method, auth, *args):
100 # Method.type_check() should have checked that all of the
101 # mandatory fields were present.
102 assert auth.has_key('session')
105 sessions = Sessions(method.api, [auth['session']], expires = None)
107 raise PLCAuthenticationFailure, "No such session"
108 session = sessions[0]
111 if session['node_id'] is not None:
112 nodes = Nodes(method.api, {'node_id': session['node_id'], 'peer_id': None})
114 raise PLCAuthenticationFailure, "No such node"
117 if 'node' not in method.roles:
118 raise PLCAuthenticationFailure, "Not allowed to call method"
122 elif session['person_id'] is not None and session['expires'] > time.time():
123 persons = Persons(method.api, {'person_id': session['person_id'], 'enabled': True, 'peer_id': None})
125 raise PLCAuthenticationFailure, "No such account"
128 if not set(person['roles']).intersection(method.roles):
129 raise PLCAuthenticationFailure, "Not allowed to call method"
131 method.caller = persons[0]
134 raise PLCAuthenticationFailure, "Invalid session"
136 except PLCAuthenticationFailure, fault:
140 class BootAuth(Auth):
142 PlanetLab version 3.x node authentication structure. Used by the
143 Boot Manager to make authenticated calls to the API based on a
144 unique node key or boot nonce value.
146 The original parameter serialization code did not define the byte
147 encoding of strings, or the string encoding of all other types. We
148 define the byte encoding to be UTF-8, and the string encoding of
149 all other types to be however Python version 2.3 unicode() encodes
154 Auth.__init__(self, {
155 'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional = False),
156 'node_id': Parameter(int, "Node identifier", optional = False),
157 'value': Parameter(str, "HMAC of node key and method call", optional = False)
160 def canonicalize(self, args):
164 if isinstance(arg, list) or isinstance(arg, tuple):
165 # The old implementation did not recursively handle
166 # lists of lists. But neither did the old API itself.
167 values += self.canonicalize(arg)
168 elif isinstance(arg, dict):
169 # Yes, the comments in the old implementation are
170 # misleading. Keys of dicts are not included in the
172 values += self.canonicalize(arg.values())
174 # We use unicode() instead of str().
175 values.append(unicode(arg))
179 def check(self, method, auth, *args):
180 # Method.type_check() should have checked that all of the
181 # mandatory fields were present.
182 assert auth.has_key('node_id')
185 nodes = Nodes(method.api, {'node_id': auth['node_id'], 'peer_id': None})
187 raise PLCAuthenticationFailure, "No such node"
192 elif node['boot_nonce']:
193 # Allow very old nodes that do not have a node key in
194 # their configuration files to use their "boot nonce"
195 # instead. The boot nonce is a random value generated
196 # by the node itself and POSTed by the Boot CD when it
197 # requests the Boot Manager. This is obviously not
198 # very secure, so we only allow it to be used if the
199 # requestor IP is the same as the IP address we have
200 # on record for the node.
201 key = node['boot_nonce']
204 if node['nodenetwork_ids']:
205 nodenetworks = NodeNetworks(method.api, node['nodenetwork_ids'])
206 for nodenetwork in nodenetworks:
207 if nodenetwork['is_primary']:
210 if not nodenetwork or not nodenetwork['is_primary']:
211 raise PLCAuthenticationFailure, "No primary network interface on record"
213 if method.source is None:
214 raise PLCAuthenticationFailure, "Cannot determine IP address of requestor"
216 if nodenetwork['ip'] != method.source[0]:
217 raise PLCAuthenticationFailure, "Requestor IP %s does not mach node IP %s" % \
218 (method.source[0], nodenetwork['ip'])
220 raise PLCAuthenticationFailure, "No node key or boot nonce"
222 # Yes, this is the "canonicalization" method used.
223 args = self.canonicalize(args)
225 msg = "[" + "".join(args) + "]"
227 # We encode in UTF-8 before calculating the HMAC, which is
228 # an 8-bit algorithm.
229 digest = hmac.new(key, msg.encode('utf-8'), sha).hexdigest()
231 if digest != auth['value']:
232 raise PLCAuthenticationFailure, "Call could not be authenticated"
236 except PLCAuthenticationFailure, fault:
240 class AnonymousAuth(Auth):
242 PlanetLab version 3.x anonymous authentication structure.
246 Auth.__init__(self, {
247 'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
250 def check(self, method, auth, *args):
251 # Sure, dude, whatever
254 class PasswordAuth(Auth):
256 PlanetLab version 3.x password authentication structure.
260 Auth.__init__(self, {
261 'AuthMethod': Parameter(str, "Authentication method to use, typically 'password'", optional = False),
262 'Username': Parameter(str, "PlanetLab username, typically an e-mail address", optional = False),
263 'AuthString': Parameter(str, "Authentication string, typically a password", optional = False),
266 def check(self, method, auth, *args):
267 # Method.type_check() should have checked that all of the
268 # mandatory fields were present.
269 assert auth.has_key('Username')
271 # Get record (must be enabled)
272 persons = Persons(method.api, {'email': auth['Username'], 'enabled': True, 'peer_id': None})
273 if len(persons) != 1:
274 raise PLCAuthenticationFailure, "No such account"
278 if auth['Username'] == method.api.config.PLC_API_MAINTENANCE_USER:
279 # "Capability" authentication, whatever the hell that was
280 # supposed to mean. It really means, login as the special
281 # "maintenance user" using password authentication. Can
282 # only be used on particular machines (those in a list).
283 sources = method.api.config.PLC_API_MAINTENANCE_SOURCES.split()
284 if method.source is not None and method.source[0] not in sources:
285 raise PLCAuthenticationFailure, "Not allowed to login to maintenance account"
287 # Not sure why this is not stored in the DB
288 password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
290 if auth['AuthString'] != password:
291 raise PLCAuthenticationFailure, "Maintenance account password verification failed"
293 # Compare encrypted plaintext against encrypted password stored in the DB
294 plaintext = auth['AuthString'].encode(method.api.encoding)
295 password = person['password']
297 # Protect against blank passwords in the DB
298 if password is None or password[:12] == "" or \
299 crypt.crypt(plaintext, password[:12]) != password:
300 raise PLCAuthenticationFailure, "Password verification failed"
302 if not set(person['roles']).intersection(method.roles):
303 raise PLCAuthenticationFailure, "Not allowed to call method"
305 method.caller = person