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.7 2006/11/08 22:53:30 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):
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")
31 def check(self, method, auth, *args):
32 method.type_check("auth", auth,
33 Mixed(SessionAuth(), PasswordAuth()),
36 class SessionAuth(Auth):
38 Proposed PlanetLab federation authentication structure.
43 'session': Parameter(str, "Session key", optional = False)
46 def check(self, method, auth, *args):
47 # Method.type_check() should have checked that all of the
48 # mandatory fields were present.
49 assert auth.has_key('session')
52 sessions = Sessions(method.api, [auth['session']], expires = None)
54 raise PLCAuthenticationFailure, "No such session"
58 if session['node_id'] is not None:
59 nodes = Nodes(method.api, [session['node_id']])
61 raise PLCAuthenticationFailure, "No such node"
64 if 'node' not in method.roles:
65 raise PLCAuthenticationFailure, "Not allowed to call method"
69 elif session['person_id'] is not None and session['expires'] > time.time():
70 persons = Persons(method.api, {'person_id': session['person_id'], 'enabled': True})
72 raise PLCAuthenticationFailure, "No such account"
75 if not set(person['roles']).intersection(method.roles):
76 raise PLCAuthenticationFailure, "Not allowed to call method"
78 method.caller = persons[0]
81 raise PLCAuthenticationFailure, "Invalid session"
83 except PLCAuthenticationFailure, fault:
89 PlanetLab version 3.x node authentication structure. Used by the
90 Boot Manager to make authenticated calls to the API based on a
91 unique node key or boot nonce value.
93 The original parameter serialization code did not define the byte
94 encoding of strings, or the string encoding of all other types. We
95 define the byte encoding to be UTF-8, and the string encoding of
96 all other types to be however Python version 2.3 unicode() encodes
101 Auth.__init__(self, {
102 'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional = False),
103 'node_id': Parameter(int, "Node identifier", optional = False),
104 'value': Parameter(str, "HMAC of node key and method call", optional = False)
107 def canonicalize(self, args):
111 if isinstance(arg, list) or isinstance(arg, tuple):
112 # The old implementation did not recursively handle
113 # lists of lists. But neither did the old API itself.
114 values += self.canonicalize(arg)
115 elif isinstance(arg, dict):
116 # Yes, the comments in the old implementation are
117 # misleading. Keys of dicts are not included in the
119 values += self.canonicalize(arg.values())
121 # We use unicode() instead of str().
122 values.append(unicode(arg))
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('node_id')
132 nodes = Nodes(method.api, [auth['node_id']])
134 raise PLCAuthenticationFailure, "No such node"
139 elif node['boot_nonce']:
140 # Allow very old nodes that do not have a node key in
141 # their configuration files to use their "boot nonce"
142 # instead. The boot nonce is a random value generated
143 # by the node itself and POSTed by the Boot CD when it
144 # requests the Boot Manager. This is obviously not
145 # very secure, so we only allow it to be used if the
146 # requestor IP is the same as the IP address we have
147 # on record for the node.
148 key = node['boot_nonce']
151 if node['nodenetwork_ids']:
152 nodenetworks = NodeNetworks(method.api, node['nodenetwork_ids'])
153 for nodenetwork in nodenetworks:
154 if nodenetwork['is_primary']:
157 if not nodenetwork or not nodenetwork['is_primary']:
158 raise PLCAuthenticationFailure, "No primary network interface on record"
160 if method.source is None:
161 raise PLCAuthenticationFailure, "Cannot determine IP address of requestor"
163 if nodenetwork['ip'] != method.source[0]:
164 raise PLCAuthenticationFailure, "Requestor IP %s does not mach node IP %s" % \
165 (method.source[0], nodenetwork['ip'])
167 raise PLCAuthenticationFailure, "No node key or boot nonce"
169 # Yes, this is the "canonicalization" method used.
170 args = self.canonicalize(args)
172 msg = "[" + "".join(args) + "]"
174 # We encode in UTF-8 before calculating the HMAC, which is
175 # an 8-bit algorithm.
176 digest = hmac.new(key, msg.encode('utf-8'), sha).hexdigest()
178 if digest != auth['value']:
179 raise PLCAuthenticationFailure, "Call could not be authenticated"
183 except PLCAuthenticationFailure, fault:
187 class AnonymousAuth(Auth):
189 PlanetLab version 3.x anonymous authentication structure.
193 Auth.__init__(self, {
194 'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
197 def check(self, method, auth, *args):
198 # Sure, dude, whatever
201 class PasswordAuth(Auth):
203 PlanetLab version 3.x password authentication structure.
207 Auth.__init__(self, {
208 'AuthMethod': Parameter(str, "Authentication method to use, typically 'password'", optional = False),
209 'Username': Parameter(str, "PlanetLab username, typically an e-mail address", optional = False),
210 'AuthString': Parameter(str, "Authentication string, typically a password", optional = False),
213 def check(self, method, auth, *args):
214 # Method.type_check() should have checked that all of the
215 # mandatory fields were present.
216 assert auth.has_key('Username')
218 # Get record (must be enabled)
219 persons = Persons(method.api, {'email': auth['Username'], 'enabled': True})
220 if len(persons) != 1:
221 raise PLCAuthenticationFailure, "No such account"
225 if auth['Username'] == method.api.config.PLC_API_MAINTENANCE_USER:
226 # "Capability" authentication, whatever the hell that was
227 # supposed to mean. It really means, login as the special
228 # "maintenance user" using password authentication. Can
229 # only be used on particular machines (those in a list).
230 sources = method.api.config.PLC_API_MAINTENANCE_SOURCES.split()
231 if method.source is not None and method.source[0] not in sources:
232 raise PLCAuthenticationFailure, "Not allowed to login to maintenance account"
234 # Not sure why this is not stored in the DB
235 password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
237 if auth['AuthString'] != password:
238 raise PLCAuthenticationFailure, "Maintenance account password verification failed"
240 # Compare encrypted plaintext against encrypted password stored in the DB
241 plaintext = auth['AuthString'].encode(method.api.encoding)
242 password = person['password']
244 # Protect against blank passwords in the DB
245 if password is None or password[:12] == "" or \
246 crypt.crypt(plaintext, password[:12]) != password:
247 raise PLCAuthenticationFailure, "Password verification failed"
249 if not set(person['roles']).intersection(method.roles):
250 raise PLCAuthenticationFailure, "Not allowed to call method"
252 method.caller = person