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.3 2006/09/25 14:47:32 mlhuang Exp $
14 from PLC.Faults import *
15 from PLC.Parameter import Parameter, Mixed
16 from PLC.Persons import Persons
17 from PLC.Nodes import Node, Nodes
19 class Auth(Parameter, dict):
21 Base class for all API authentication methods.
24 def __init__(self, auth):
25 Parameter.__init__(self, auth, "API authentication structure")
30 PlanetLab version 3.x node authentication structure. Used by the
31 Boot Manager to make authenticated calls to the API based on a
32 unique node key or boot nonce value.
34 The original parameter serialization code did not define the byte
35 encoding of strings, or the string encoding of all other types. We
36 define the byte encoding to be UTF-8, and the string encoding of
37 all other types to be however Python version 2.3 unicode() encodes
43 'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional = False),
44 'node_id': Parameter(int, "Node identifier", optional = False),
45 'value': Parameter(str, "HMAC of node key and method call", optional = False)
48 def canonicalize(self, args):
52 if isinstance(arg, list) or isinstance(arg, tuple):
53 # The old implementation did not recursively handle
54 # lists of lists. But neither did the old API itself.
55 values += self.canonicalize(arg)
56 elif isinstance(arg, dict):
57 # Yes, the comments in the old implementation are
58 # misleading. Keys of dicts are not included in the
60 values += self.canonicalize(arg.values())
62 # We use unicode() instead of str().
63 values.append(unicode(arg))
67 def check(self, method, auth, *args):
68 # Method.type_check() should have checked that all of the
69 # mandatory fields were present.
70 assert auth.has_key('node_id')
73 nodes = Nodes(method.api, [auth['node_id']]).values()
75 raise PLCAuthenticationFailure, "No such node"
80 elif node['boot_nonce']:
81 # Allow very old nodes that do not have a node key in
82 # their configuration files to use their "boot nonce"
83 # instead. The boot nonce is a random value generated
84 # by the node itself and POSTed by the Boot CD when it
85 # requests the Boot Manager. This is obviously not
86 # very secure, so we only allow it to be used if the
87 # requestor IP is the same as the IP address we have
88 # on record for the node.
89 key = node['boot_nonce']
92 if node['nodenetwork_ids']:
93 nodenetworks = NodeNetworks(method.api, node['nodenetwork_ids']).values()
94 for nodenetwork in nodenetworks:
95 if nodenetwork['is_primary']:
98 if not nodenetwork or not nodenetwork['is_primary']:
99 raise PLCAuthenticationFailure, "No primary network interface on record"
101 if method.source is None:
102 raise PLCAuthenticationFailure, "Cannot determine IP address of requestor"
104 if nodenetwork['ip'] != method.source[0]:
105 raise PLCAuthenticationFailure, "Requestor IP %s does not mach node IP %s" % \
106 (method.source[0], nodenetwork['ip'])
108 raise PLCAuthenticationFailure, "No node key or boot nonce"
110 # Yes, this is the "canonicalization" method used.
111 args = self.canonicalize(args)
113 msg = "[" + "".join(args) + "]"
115 # We encode in UTF-8 before calculating the HMAC, which is
116 # an 8-bit algorithm.
117 digest = hmac.new(key, msg.encode('utf-8'), sha).hexdigest()
119 if digest != auth['value']:
120 raise PLCAuthenticationFailure, "Call could not be authenticated"
124 except PLCAuthenticationFailure, fault:
128 class AnonymousAuth(Auth):
130 PlanetLab version 3.x anonymous authentication structure.
134 Auth.__init__(self, {
135 'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
138 def check(self, method, auth, *args):
139 # Sure, dude, whatever
142 class PasswordAuth(Auth):
144 PlanetLab version 3.x password authentication structure.
148 Auth.__init__(self, {
149 'AuthMethod': Parameter(str, "Authentication method to use, typically 'password'", optional = False),
150 'Username': Parameter(str, "PlanetLab username, typically an e-mail address", optional = False),
151 'AuthString': Parameter(str, "Authentication string, typically a password", optional = False),
154 def check(self, method, auth, *args):
155 # Method.type_check() should have checked that all of the
156 # mandatory fields were present.
157 assert auth.has_key('Username')
159 # Get record (must be enabled)
160 persons = Persons(method.api, [auth['Username']], enabled = True)
161 if len(persons) != 1:
162 raise PLCAuthenticationFailure, "No such account"
164 person = persons.values()[0]
166 if auth['Username'] == method.api.config.PLC_API_MAINTENANCE_USER:
167 # "Capability" authentication, whatever the hell that was
168 # supposed to mean. It really means, login as the special
169 # "maintenance user" using password authentication. Can
170 # only be used on particular machines (those in a list).
171 sources = method.api.config.PLC_API_MAINTENANCE_SOURCES.split()
172 if method.source is not None and method.source[0] not in sources:
173 raise PLCAuthenticationFailure, "Not allowed to login to maintenance account"
175 # Not sure why this is not stored in the DB
176 password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
178 if auth['AuthString'] != password:
179 raise PLCAuthenticationFailure, "Maintenance account password verification failed"
181 # Compare encrypted plaintext against encrypted password stored in the DB
182 plaintext = auth['AuthString'].encode(method.api.encoding)
183 password = person['password']
185 # Protect against blank passwords in the DB
186 if password is None or password[:12] == "" or \
187 crypt.crypt(plaintext, password[:12]) != password:
188 raise PLCAuthenticationFailure, "Password verification failed"
190 if not set(person['roles']).intersection(method.roles):
191 raise PLCAuthenticationFailure, "Not allowed to call method"
193 method.caller = person