- node sessions do not expire
[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.5 2006/10/27 15:32:29 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, dict):
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         dict.__init__(auth)
31
32     def check(self, method, auth, *args):
33         method.type_check("auth", auth,
34                           Mixed(SessionAuth(), PasswordAuth()),
35                           (auth,) + args)
36
37 class SessionAuth(Auth):
38     """
39     Proposed PlanetLab federation authentication structure.
40     """
41
42     def __init__(self):
43         Auth.__init__(self, {
44             'session': Parameter(str, "Session key", optional = False)
45             })
46
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')
51
52         # Get session record
53         sessions = Sessions(method.api, [auth['session']], expires = None).values()
54         if not sessions:
55             raise PLCAuthenticationFailure, "No such session"
56         session = sessions[0]
57
58         try:
59             if session['node_id'] is not None:
60                 nodes = Nodes(method.api, [session['node_id']]).values()
61                 if not nodes:
62                     raise PLCAuthenticationFailure, "No such node"
63                 node = nodes[0]
64
65                 if 'node' not in method.roles:
66                     raise PLCAuthenticationFailure, "Not allowed to call method"
67
68                 method.caller = node
69
70             elif session['person_id'] is not None and session['expires'] > time.time():
71                 persons = Persons(method.api, [session['person_id']], enabled = True).values()
72                 if not persons:
73                     raise PLCAuthenticationFailure, "No such account"
74                 person = persons[0]
75
76                 if not set(person['roles']).intersection(method.roles):
77                     raise PLCAuthenticationFailure, "Not allowed to call method"
78
79                 method.caller = persons[0]
80
81             else:
82                 raise PLCAuthenticationFailure, "Invalid session"
83
84         except PLCAuthenticationFailure, fault:
85             session.delete()
86             raise fault
87
88 class BootAuth(Auth):
89     """
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.
93
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
98     them.
99     """
100
101     def __init__(self):
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)
106             })
107
108     def canonicalize(self, args):
109         values = []
110
111         for arg in 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
119                 # hash.
120                 values += self.canonicalize(arg.values())
121             else:
122                 # We use unicode() instead of str().
123                 values.append(unicode(arg))
124
125         return values
126
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')
131
132         try:
133             nodes = Nodes(method.api, [auth['node_id']]).values()
134             if not nodes:
135                 raise PLCAuthenticationFailure, "No such node"
136             node = nodes[0]
137
138             if node['key']:
139                 key = node['key']
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']
150
151                 nodenetwork = None
152                 if node['nodenetwork_ids']:
153                     nodenetworks = NodeNetworks(method.api, node['nodenetwork_ids']).values()
154                     for nodenetwork in nodenetworks:
155                         if nodenetwork['is_primary']:
156                             break
157             
158                 if not nodenetwork or not nodenetwork['is_primary']:
159                     raise PLCAuthenticationFailure, "No primary network interface on record"
160             
161                 if method.source is None:
162                     raise PLCAuthenticationFailure, "Cannot determine IP address of requestor"
163
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'])
167             else:
168                 raise PLCAuthenticationFailure, "No node key or boot nonce"
169
170             # Yes, this is the "canonicalization" method used.
171             args = self.canonicalize(args)
172             args.sort()
173             msg = "[" + "".join(args) + "]"
174
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()
178
179             if digest != auth['value']:
180                 raise PLCAuthenticationFailure, "Call could not be authenticated"
181
182             method.caller = node
183
184         except PLCAuthenticationFailure, fault:
185             # XXX Send e-mail
186             raise fault
187
188 class AnonymousAuth(Auth):
189     """
190     PlanetLab version 3.x anonymous authentication structure.
191     """
192
193     def __init__(self):
194         Auth.__init__(self, {
195             'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
196             })
197
198     def check(self, method, auth, *args):
199         # Sure, dude, whatever
200         return True
201
202 class PasswordAuth(Auth):
203     """
204     PlanetLab version 3.x password authentication structure.
205     """
206
207     def __init__(self):
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),
212             })
213
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')
218
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"
223
224         person = persons.values()[0]
225
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"
234
235             # Not sure why this is not stored in the DB
236             password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
237
238             if auth['AuthString'] != password:
239                 raise PLCAuthenticationFailure, "Maintenance account password verification failed"
240         else:
241             # Compare encrypted plaintext against encrypted password stored in the DB
242             plaintext = auth['AuthString'].encode(method.api.encoding)
243             password = person['password']
244
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"
249
250         if not set(person['roles']).intersection(method.roles):
251             raise PLCAuthenticationFailure, "Not allowed to call method"
252
253         method.caller = person