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.12 2007/01/30 23:08:58 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
21 from PLC.Boot import notify_owners
23 class Auth(Parameter):
25 Base class for all API authentication methods, as well as a class
26 that can be used to represent Mixed(SessionAuth(), PasswordAuth(),
27 GPGAuth()), i.e. the three principal API authentication methods.
30 def __init__(self, auth = {}):
31 Parameter.__init__(self, auth, "API authentication structure")
33 def check(self, method, auth, *args):
34 method.type_check("auth", auth,
35 Mixed(SessionAuth(), PasswordAuth(), GPGAuth()),
40 Proposed PlanetLab federation authentication structure.
45 'AuthMethod': Parameter(str, "Authentication method to use, always 'gpg'", optional = False),
46 'name': Parameter(str, "Peer or user name", optional = False),
47 'signature': Parameter(str, "Message signature", optional = False)
50 def check(self, method, auth, *args):
52 peers = Peers(method.api, [auth['name']])
54 if 'peer' not in method.roles:
55 raise PLCAuthenticationFailure, "Not allowed to call method"
57 method.caller = peer = peers[0]
60 persons = Persons(method.api, {'email': auth['name'], 'enabled': True, 'peer_id': None})
62 raise PLCAuthenticationFailure, "No such user '%s'" % auth['name']
64 if not set(person['roles']).intersection(method.roles):
65 raise PLCAuthenticationFailure, "Not allowed to call method"
67 method.caller = person = persons[0]
68 keys = Keys(method.api, {'key_id': person['key_ids'], 'key_type': "gpg", 'peer_id': None})
71 raise PLCAuthenticationFailure, "No GPG key on record for peer or user '%s'"
75 from PLC.GPG import gpg_verify
76 gpg_verify(method.name, args, auth['signature'], key)
78 except PLCAuthenticationFailure, fault:
83 except PLCAuthenticationFailure, fault:
87 class SessionAuth(Auth):
89 Secondary authentication method. After authenticating with a
90 primary authentication method, call GetSession() to generate a
91 session key that may be used for subsequent calls.
96 'AuthMethod': Parameter(str, "Authentication method to use, always 'session'", optional = False),
97 'session': Parameter(str, "Session key", optional = False)
100 def check(self, method, auth, *args):
101 # Method.type_check() should have checked that all of the
102 # mandatory fields were present.
103 assert auth.has_key('session')
106 sessions = Sessions(method.api, [auth['session']], expires = None)
108 raise PLCAuthenticationFailure, "No such session"
109 session = sessions[0]
112 if session['node_id'] is not None:
113 nodes = Nodes(method.api, {'node_id': session['node_id'], 'peer_id': None})
115 raise PLCAuthenticationFailure, "No such node"
118 if 'node' not in method.roles:
119 raise PLCAuthenticationFailure, "Not allowed to call method"
123 elif session['person_id'] is not None and session['expires'] > time.time():
124 persons = Persons(method.api, {'person_id': session['person_id'], 'enabled': True, 'peer_id': None})
126 raise PLCAuthenticationFailure, "No such account"
129 if not set(person['roles']).intersection(method.roles):
130 raise PLCAuthenticationFailure, "Not allowed to call method"
132 method.caller = persons[0]
135 raise PLCAuthenticationFailure, "Invalid session"
137 except PLCAuthenticationFailure, fault:
141 class BootAuth(Auth):
143 PlanetLab version 3.x node authentication structure. Used by the
144 Boot Manager to make authenticated calls to the API based on a
145 unique node key or boot nonce value.
147 The original parameter serialization code did not define the byte
148 encoding of strings, or the string encoding of all other types. We
149 define the byte encoding to be UTF-8, and the string encoding of
150 all other types to be however Python version 2.3 unicode() encodes
155 Auth.__init__(self, {
156 'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional = False),
157 'node_id': Parameter(int, "Node identifier", optional = False),
158 'value': Parameter(str, "HMAC of node key and method call", optional = False)
161 def canonicalize(self, args):
165 if isinstance(arg, list) or isinstance(arg, tuple):
166 # The old implementation did not recursively handle
167 # lists of lists. But neither did the old API itself.
168 values += self.canonicalize(arg)
169 elif isinstance(arg, dict):
170 # Yes, the comments in the old implementation are
171 # misleading. Keys of dicts are not included in the
173 values += self.canonicalize(arg.values())
175 # We use unicode() instead of str().
176 values.append(unicode(arg))
180 def check(self, method, auth, *args):
181 # Method.type_check() should have checked that all of the
182 # mandatory fields were present.
183 assert auth.has_key('node_id')
186 nodes = Nodes(method.api, {'node_id': auth['node_id'], 'peer_id': None})
188 raise PLCAuthenticationFailure, "No such node"
193 elif node['boot_nonce']:
194 # Allow very old nodes that do not have a node key in
195 # their configuration files to use their "boot nonce"
196 # instead. The boot nonce is a random value generated
197 # by the node itself and POSTed by the Boot CD when it
198 # requests the Boot Manager. This is obviously not
199 # very secure, so we only allow it to be used if the
200 # requestor IP is the same as the IP address we have
201 # on record for the node.
202 key = node['boot_nonce']
205 if node['nodenetwork_ids']:
206 nodenetworks = NodeNetworks(method.api, node['nodenetwork_ids'])
207 for nodenetwork in nodenetworks:
208 if nodenetwork['is_primary']:
211 if not nodenetwork or not nodenetwork['is_primary']:
212 raise PLCAuthenticationFailure, "No primary network interface on record"
214 if method.source is None:
215 raise PLCAuthenticationFailure, "Cannot determine IP address of requestor"
217 if nodenetwork['ip'] != method.source[0]:
218 raise PLCAuthenticationFailure, "Requestor IP %s does not match node IP %s" % \
219 (method.source[0], nodenetwork['ip'])
221 raise PLCAuthenticationFailure, "No node key or boot nonce"
223 # Yes, this is the "canonicalization" method used.
224 args = self.canonicalize(args)
226 msg = "[" + "".join(args) + "]"
228 # We encode in UTF-8 before calculating the HMAC, which is
229 # an 8-bit algorithm.
230 digest = hmac.new(key, msg.encode('utf-8'), sha).hexdigest()
232 if digest != auth['value']:
233 raise PLCAuthenticationFailure, "Call could not be authenticated"
237 except PLCAuthenticationFailure, fault:
239 notify_owners(method, node, 'authfail', include_pis = True, include_techs = True, fault = fault)
242 class AnonymousAuth(Auth):
244 PlanetLab version 3.x anonymous authentication structure.
248 Auth.__init__(self, {
249 'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
252 def check(self, method, auth, *args):
253 # Sure, dude, whatever
256 class PasswordAuth(Auth):
258 PlanetLab version 3.x password authentication structure.
262 Auth.__init__(self, {
263 'AuthMethod': Parameter(str, "Authentication method to use, typically 'password'", optional = False),
264 'Username': Parameter(str, "PlanetLab username, typically an e-mail address", optional = False),
265 'AuthString': Parameter(str, "Authentication string, typically a password", optional = False),
268 def check(self, method, auth, *args):
269 # Method.type_check() should have checked that all of the
270 # mandatory fields were present.
271 assert auth.has_key('Username')
273 # Get record (must be enabled)
274 persons = Persons(method.api, {'email': auth['Username'], 'enabled': True, 'peer_id': None})
275 if len(persons) != 1:
276 raise PLCAuthenticationFailure, "No such account"
280 if auth['Username'] == method.api.config.PLC_API_MAINTENANCE_USER:
281 # "Capability" authentication, whatever the hell that was
282 # supposed to mean. It really means, login as the special
283 # "maintenance user" using password authentication. Can
284 # only be used on particular machines (those in a list).
285 sources = method.api.config.PLC_API_MAINTENANCE_SOURCES.split()
286 if method.source is not None and method.source[0] not in sources:
287 raise PLCAuthenticationFailure, "Not allowed to login to maintenance account"
289 # Not sure why this is not stored in the DB
290 password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
292 if auth['AuthString'] != password:
293 raise PLCAuthenticationFailure, "Maintenance account password verification failed"
295 # Compare encrypted plaintext against encrypted password stored in the DB
296 plaintext = auth['AuthString'].encode(method.api.encoding)
297 password = person['password']
299 # Protect against blank passwords in the DB
300 if password is None or password[:12] == "" or \
301 crypt.crypt(plaintext, password[:12]) != password:
302 raise PLCAuthenticationFailure, "Password verification failed"
304 if not set(person['roles']).intersection(method.roles):
305 raise PLCAuthenticationFailure, "Not allowed to call method"
307 method.caller = person