- re-enable return_fields specification
[plcapi.git] / PLC / Auth.py
1 #
2 # PLCAPI authentication parameters
3 #
4 # Mark Huang <mlhuang@cs.princeton.edu>
5 # Copyright (C) 2006 The Trustees of Princeton University
6 #
7 # $Id: Auth.py,v 1.7 2006/11/08 22:53:30 mlhuang Exp $
8 #
9
10 import crypt
11 import sha
12 import hmac
13 import time
14
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
21 class Auth(Parameter):
22     """
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.
26     """
27
28     def __init__(self, auth = {}):
29         Parameter.__init__(self, auth, "API authentication structure")
30
31     def check(self, method, auth, *args):
32         method.type_check("auth", auth,
33                           Mixed(SessionAuth(), PasswordAuth()),
34                           (auth,) + args)
35
36 class SessionAuth(Auth):
37     """
38     Proposed PlanetLab federation authentication structure.
39     """
40
41     def __init__(self):
42         Auth.__init__(self, {
43             'session': Parameter(str, "Session key", optional = False)
44             })
45
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')
50
51         # Get session record
52         sessions = Sessions(method.api, [auth['session']], expires = None)
53         if not sessions:
54             raise PLCAuthenticationFailure, "No such session"
55         session = sessions[0]
56
57         try:
58             if session['node_id'] is not None:
59                 nodes = Nodes(method.api, [session['node_id']])
60                 if not nodes:
61                     raise PLCAuthenticationFailure, "No such node"
62                 node = nodes[0]
63
64                 if 'node' not in method.roles:
65                     raise PLCAuthenticationFailure, "Not allowed to call method"
66
67                 method.caller = node
68
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})
71                 if not persons:
72                     raise PLCAuthenticationFailure, "No such account"
73                 person = persons[0]
74
75                 if not set(person['roles']).intersection(method.roles):
76                     raise PLCAuthenticationFailure, "Not allowed to call method"
77
78                 method.caller = persons[0]
79
80             else:
81                 raise PLCAuthenticationFailure, "Invalid session"
82
83         except PLCAuthenticationFailure, fault:
84             session.delete()
85             raise fault
86
87 class BootAuth(Auth):
88     """
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.
92
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
97     them.
98     """
99
100     def __init__(self):
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)
105             })
106
107     def canonicalize(self, args):
108         values = []
109
110         for arg in 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
118                 # hash.
119                 values += self.canonicalize(arg.values())
120             else:
121                 # We use unicode() instead of str().
122                 values.append(unicode(arg))
123
124         return values
125
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')
130
131         try:
132             nodes = Nodes(method.api, [auth['node_id']])
133             if not nodes:
134                 raise PLCAuthenticationFailure, "No such node"
135             node = nodes[0]
136
137             if node['key']:
138                 key = node['key']
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']
149
150                 nodenetwork = None
151                 if node['nodenetwork_ids']:
152                     nodenetworks = NodeNetworks(method.api, node['nodenetwork_ids'])
153                     for nodenetwork in nodenetworks:
154                         if nodenetwork['is_primary']:
155                             break
156             
157                 if not nodenetwork or not nodenetwork['is_primary']:
158                     raise PLCAuthenticationFailure, "No primary network interface on record"
159             
160                 if method.source is None:
161                     raise PLCAuthenticationFailure, "Cannot determine IP address of requestor"
162
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'])
166             else:
167                 raise PLCAuthenticationFailure, "No node key or boot nonce"
168
169             # Yes, this is the "canonicalization" method used.
170             args = self.canonicalize(args)
171             args.sort()
172             msg = "[" + "".join(args) + "]"
173
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()
177
178             if digest != auth['value']:
179                 raise PLCAuthenticationFailure, "Call could not be authenticated"
180
181             method.caller = node
182
183         except PLCAuthenticationFailure, fault:
184             # XXX Send e-mail
185             raise fault
186
187 class AnonymousAuth(Auth):
188     """
189     PlanetLab version 3.x anonymous authentication structure.
190     """
191
192     def __init__(self):
193         Auth.__init__(self, {
194             'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
195             })
196
197     def check(self, method, auth, *args):
198         # Sure, dude, whatever
199         return True
200
201 class PasswordAuth(Auth):
202     """
203     PlanetLab version 3.x password authentication structure.
204     """
205
206     def __init__(self):
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),
211             })
212
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')
217
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"
222
223         person = persons[0]
224
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"
233
234             # Not sure why this is not stored in the DB
235             password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
236
237             if auth['AuthString'] != password:
238                 raise PLCAuthenticationFailure, "Maintenance account password verification failed"
239         else:
240             # Compare encrypted plaintext against encrypted password stored in the DB
241             plaintext = auth['AuthString'].encode(method.api.encoding)
242             password = person['password']
243
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"
248
249         if not set(person['roles']).intersection(method.roles):
250             raise PLCAuthenticationFailure, "Not allowed to call method"
251
252         method.caller = person