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.5 2006/10/27 15:32:29 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
21 class Auth(Parameter, dict):
23 Base class for all API authentication methods, as well as a class
24 that can be used to represent Mixed(SessionAuth(), PasswordAuth()),
25 i.e. the two principal API authentication methods.
28 def __init__(self, auth = {}):
29 Parameter.__init__(self, auth, "API authentication structure")
32 def check(self, method, auth, *args):
33 method.type_check("auth", auth,
34 Mixed(SessionAuth(), PasswordAuth()),
37 class SessionAuth(Auth):
39 Proposed PlanetLab federation authentication structure.
44 'session': Parameter(str, "Session key", optional = False)
47 def check(self, method, auth, *args):
48 # Method.type_check() should have checked that all of the
49 # mandatory fields were present.
50 assert auth.has_key('session')
53 sessions = Sessions(method.api, [auth['session']], expires = None).values()
55 raise PLCAuthenticationFailure, "No such session"
59 if session['node_id'] is not None:
60 nodes = Nodes(method.api, [session['node_id']]).values()
62 raise PLCAuthenticationFailure, "No such node"
65 if 'node' not in method.roles:
66 raise PLCAuthenticationFailure, "Not allowed to call method"
70 elif session['person_id'] is not None and session['expires'] > time.time():
71 persons = Persons(method.api, [session['person_id']], enabled = True).values()
73 raise PLCAuthenticationFailure, "No such account"
76 if not set(person['roles']).intersection(method.roles):
77 raise PLCAuthenticationFailure, "Not allowed to call method"
79 method.caller = persons[0]
82 raise PLCAuthenticationFailure, "Invalid session"
84 except PLCAuthenticationFailure, fault:
90 PlanetLab version 3.x node authentication structure. Used by the
91 Boot Manager to make authenticated calls to the API based on a
92 unique node key or boot nonce value.
94 The original parameter serialization code did not define the byte
95 encoding of strings, or the string encoding of all other types. We
96 define the byte encoding to be UTF-8, and the string encoding of
97 all other types to be however Python version 2.3 unicode() encodes
102 Auth.__init__(self, {
103 'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional = False),
104 'node_id': Parameter(int, "Node identifier", optional = False),
105 'value': Parameter(str, "HMAC of node key and method call", optional = False)
108 def canonicalize(self, args):
112 if isinstance(arg, list) or isinstance(arg, tuple):
113 # The old implementation did not recursively handle
114 # lists of lists. But neither did the old API itself.
115 values += self.canonicalize(arg)
116 elif isinstance(arg, dict):
117 # Yes, the comments in the old implementation are
118 # misleading. Keys of dicts are not included in the
120 values += self.canonicalize(arg.values())
122 # We use unicode() instead of str().
123 values.append(unicode(arg))
127 def check(self, method, auth, *args):
128 # Method.type_check() should have checked that all of the
129 # mandatory fields were present.
130 assert auth.has_key('node_id')
133 nodes = Nodes(method.api, [auth['node_id']]).values()
135 raise PLCAuthenticationFailure, "No such node"
140 elif node['boot_nonce']:
141 # Allow very old nodes that do not have a node key in
142 # their configuration files to use their "boot nonce"
143 # instead. The boot nonce is a random value generated
144 # by the node itself and POSTed by the Boot CD when it
145 # requests the Boot Manager. This is obviously not
146 # very secure, so we only allow it to be used if the
147 # requestor IP is the same as the IP address we have
148 # on record for the node.
149 key = node['boot_nonce']
152 if node['nodenetwork_ids']:
153 nodenetworks = NodeNetworks(method.api, node['nodenetwork_ids']).values()
154 for nodenetwork in nodenetworks:
155 if nodenetwork['is_primary']:
158 if not nodenetwork or not nodenetwork['is_primary']:
159 raise PLCAuthenticationFailure, "No primary network interface on record"
161 if method.source is None:
162 raise PLCAuthenticationFailure, "Cannot determine IP address of requestor"
164 if nodenetwork['ip'] != method.source[0]:
165 raise PLCAuthenticationFailure, "Requestor IP %s does not mach node IP %s" % \
166 (method.source[0], nodenetwork['ip'])
168 raise PLCAuthenticationFailure, "No node key or boot nonce"
170 # Yes, this is the "canonicalization" method used.
171 args = self.canonicalize(args)
173 msg = "[" + "".join(args) + "]"
175 # We encode in UTF-8 before calculating the HMAC, which is
176 # an 8-bit algorithm.
177 digest = hmac.new(key, msg.encode('utf-8'), sha).hexdigest()
179 if digest != auth['value']:
180 raise PLCAuthenticationFailure, "Call could not be authenticated"
184 except PLCAuthenticationFailure, fault:
188 class AnonymousAuth(Auth):
190 PlanetLab version 3.x anonymous authentication structure.
194 Auth.__init__(self, {
195 'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
198 def check(self, method, auth, *args):
199 # Sure, dude, whatever
202 class PasswordAuth(Auth):
204 PlanetLab version 3.x password authentication structure.
208 Auth.__init__(self, {
209 'AuthMethod': Parameter(str, "Authentication method to use, typically 'password'", optional = False),
210 'Username': Parameter(str, "PlanetLab username, typically an e-mail address", optional = False),
211 'AuthString': Parameter(str, "Authentication string, typically a password", optional = False),
214 def check(self, method, auth, *args):
215 # Method.type_check() should have checked that all of the
216 # mandatory fields were present.
217 assert auth.has_key('Username')
219 # Get record (must be enabled)
220 persons = Persons(method.api, [auth['Username']], enabled = True)
221 if len(persons) != 1:
222 raise PLCAuthenticationFailure, "No such account"
224 person = persons.values()[0]
226 if auth['Username'] == method.api.config.PLC_API_MAINTENANCE_USER:
227 # "Capability" authentication, whatever the hell that was
228 # supposed to mean. It really means, login as the special
229 # "maintenance user" using password authentication. Can
230 # only be used on particular machines (those in a list).
231 sources = method.api.config.PLC_API_MAINTENANCE_SOURCES.split()
232 if method.source is not None and method.source[0] not in sources:
233 raise PLCAuthenticationFailure, "Not allowed to login to maintenance account"
235 # Not sure why this is not stored in the DB
236 password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
238 if auth['AuthString'] != password:
239 raise PLCAuthenticationFailure, "Maintenance account password verification failed"
241 # Compare encrypted plaintext against encrypted password stored in the DB
242 plaintext = auth['AuthString'].encode(method.api.encoding)
243 password = person['password']
245 # Protect against blank passwords in the DB
246 if password is None or password[:12] == "" or \
247 crypt.crypt(plaintext, password[:12]) != password:
248 raise PLCAuthenticationFailure, "Password verification failed"
250 if not set(person['roles']).intersection(method.roles):
251 raise PLCAuthenticationFailure, "Not allowed to call method"
253 method.caller = person