minimal hacks so that the PHP bindings can be built under FC2
[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.9 2006/12/15 18:35:38 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 from PLC.Peers import Peer, Peers
21
22 class Auth(Parameter):
23     """
24     Base class for all API authentication methods, as well as a class
25     that can be used to represent Mixed(SessionAuth(), PasswordAuth(),
26     GPGAuth()), i.e. the three principal API authentication methods.
27     """
28
29     def __init__(self, auth = {}):
30         Parameter.__init__(self, auth, "API authentication structure")
31
32     def check(self, method, auth, *args):
33         method.type_check("auth", auth,
34                           Mixed(SessionAuth(), PasswordAuth(), GPGAuth()),
35                           (auth,) + args)
36
37 class GPGAuth(Auth):
38     """
39     Proposed PlanetLab federation authentication structure.
40     """
41
42     def __init__(self):
43         Auth.__init__(self, {
44             'AuthMethod': Parameter(str, "Authentication method to use, always 'gpg'", optional = False),
45             'name': Parameter(str, "Peer or user name", optional = False),
46             'signature': Parameter(str, "Message signature", optional = False)
47             })
48
49     def check(self, method, auth, *args):
50         try:
51             peers = Peers(method.api, [auth['name']])
52             if peers:
53                 if 'peer' not in method.roles:
54                     raise PLCAuthenticationFailure, "Not allowed to call method"
55
56                 method.caller = peer = peers[0]
57                 keys = [peer['key']]
58             else:
59                 persons = Persons(method.api, {'email': auth['name'], 'enabled': True})
60                 if not persons:
61                     raise PLCAuthenticationFailure, "No such peer or user '%s'" % auth['name']
62
63                 if not set(person['roles']).intersection(method.roles):
64                     raise PLCAuthenticationFailure, "Not allowed to call method"
65
66                 method.caller = person = persons[0]
67                 keys = Keys(method.api, {'key_id': person['key_ids'], 'key_type': "gpg"})
68
69             if not keys:
70                 raise PLCAuthenticationFailure, "No GPG key on record for peer or user '%s'"
71
72             for key in keys:
73                 try:
74                     from PLC.GPG import gpg_verify
75                     gpg_verify(method.name, args, auth['signature'], key)
76                     return
77                 except PLCAuthenticationFailure, fault:
78                     pass
79
80             raise fault
81
82         except PLCAuthenticationFailure, fault:
83             # XXX Send e-mail
84             raise fault
85
86 class SessionAuth(Auth):
87     """
88     Secondary authentication method. After authenticating with a
89     primary authentication method, call GetSession() to generate a
90     session key that may be used for subsequent calls.
91     """
92
93     def __init__(self):
94         Auth.__init__(self, {
95             'AuthMethod': Parameter(str, "Authentication method to use, always 'session'", optional = False),
96             'session': Parameter(str, "Session key", optional = False)
97             })
98
99     def check(self, method, auth, *args):
100         # Method.type_check() should have checked that all of the
101         # mandatory fields were present.
102         assert auth.has_key('session')
103
104         # Get session record
105         sessions = Sessions(method.api, [auth['session']], expires = None)
106         if not sessions:
107             raise PLCAuthenticationFailure, "No such session"
108         session = sessions[0]
109
110         try:
111             if session['node_id'] is not None:
112                 nodes = Nodes(method.api, [session['node_id']])
113                 if not nodes:
114                     raise PLCAuthenticationFailure, "No such node"
115                 node = nodes[0]
116
117                 if 'node' not in method.roles:
118                     raise PLCAuthenticationFailure, "Not allowed to call method"
119
120                 method.caller = node
121
122             elif session['person_id'] is not None and session['expires'] > time.time():
123                 persons = Persons(method.api, {'person_id': session['person_id'], 'enabled': True})
124                 if not persons:
125                     raise PLCAuthenticationFailure, "No such account"
126                 person = persons[0]
127
128                 if not set(person['roles']).intersection(method.roles):
129                     raise PLCAuthenticationFailure, "Not allowed to call method"
130
131                 method.caller = persons[0]
132
133             else:
134                 raise PLCAuthenticationFailure, "Invalid session"
135
136         except PLCAuthenticationFailure, fault:
137             session.delete()
138             raise fault
139
140 class BootAuth(Auth):
141     """
142     PlanetLab version 3.x node authentication structure. Used by the
143     Boot Manager to make authenticated calls to the API based on a
144     unique node key or boot nonce value.
145
146     The original parameter serialization code did not define the byte
147     encoding of strings, or the string encoding of all other types. We
148     define the byte encoding to be UTF-8, and the string encoding of
149     all other types to be however Python version 2.3 unicode() encodes
150     them.
151     """
152
153     def __init__(self):
154         Auth.__init__(self, {
155             'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional = False),
156             'node_id': Parameter(int, "Node identifier", optional = False),
157             'value': Parameter(str, "HMAC of node key and method call", optional = False)
158             })
159
160     def canonicalize(self, args):
161         values = []
162
163         for arg in args:
164             if isinstance(arg, list) or isinstance(arg, tuple):
165                 # The old implementation did not recursively handle
166                 # lists of lists. But neither did the old API itself.
167                 values += self.canonicalize(arg)
168             elif isinstance(arg, dict):
169                 # Yes, the comments in the old implementation are
170                 # misleading. Keys of dicts are not included in the
171                 # hash.
172                 values += self.canonicalize(arg.values())
173             else:
174                 # We use unicode() instead of str().
175                 values.append(unicode(arg))
176
177         return values
178
179     def check(self, method, auth, *args):
180         # Method.type_check() should have checked that all of the
181         # mandatory fields were present.
182         assert auth.has_key('node_id')
183
184         try:
185             nodes = Nodes(method.api, [auth['node_id']])
186             if not nodes:
187                 raise PLCAuthenticationFailure, "No such node"
188             node = nodes[0]
189
190             if node['key']:
191                 key = node['key']
192             elif node['boot_nonce']:
193                 # Allow very old nodes that do not have a node key in
194                 # their configuration files to use their "boot nonce"
195                 # instead. The boot nonce is a random value generated
196                 # by the node itself and POSTed by the Boot CD when it
197                 # requests the Boot Manager. This is obviously not
198                 # very secure, so we only allow it to be used if the
199                 # requestor IP is the same as the IP address we have
200                 # on record for the node.
201                 key = node['boot_nonce']
202
203                 nodenetwork = None
204                 if node['nodenetwork_ids']:
205                     nodenetworks = NodeNetworks(method.api, node['nodenetwork_ids'])
206                     for nodenetwork in nodenetworks:
207                         if nodenetwork['is_primary']:
208                             break
209             
210                 if not nodenetwork or not nodenetwork['is_primary']:
211                     raise PLCAuthenticationFailure, "No primary network interface on record"
212             
213                 if method.source is None:
214                     raise PLCAuthenticationFailure, "Cannot determine IP address of requestor"
215
216                 if nodenetwork['ip'] != method.source[0]:
217                     raise PLCAuthenticationFailure, "Requestor IP %s does not mach node IP %s" % \
218                           (method.source[0], nodenetwork['ip'])
219             else:
220                 raise PLCAuthenticationFailure, "No node key or boot nonce"
221
222             # Yes, this is the "canonicalization" method used.
223             args = self.canonicalize(args)
224             args.sort()
225             msg = "[" + "".join(args) + "]"
226
227             # We encode in UTF-8 before calculating the HMAC, which is
228             # an 8-bit algorithm.
229             digest = hmac.new(key, msg.encode('utf-8'), sha).hexdigest()
230
231             if digest != auth['value']:
232                 raise PLCAuthenticationFailure, "Call could not be authenticated"
233
234             method.caller = node
235
236         except PLCAuthenticationFailure, fault:
237             # XXX Send e-mail
238             raise fault
239
240 class AnonymousAuth(Auth):
241     """
242     PlanetLab version 3.x anonymous authentication structure.
243     """
244
245     def __init__(self):
246         Auth.__init__(self, {
247             'AuthMethod': Parameter(str, "Authentication method to use, always 'anonymous'", False),
248             })
249
250     def check(self, method, auth, *args):
251         # Sure, dude, whatever
252         return True
253
254 class PasswordAuth(Auth):
255     """
256     PlanetLab version 3.x password authentication structure.
257     """
258
259     def __init__(self):
260         Auth.__init__(self, {
261             'AuthMethod': Parameter(str, "Authentication method to use, typically 'password'", optional = False),
262             'Username': Parameter(str, "PlanetLab username, typically an e-mail address", optional = False),
263             'AuthString': Parameter(str, "Authentication string, typically a password", optional = False),
264             })
265
266     def check(self, method, auth, *args):
267         # Method.type_check() should have checked that all of the
268         # mandatory fields were present.
269         assert auth.has_key('Username')
270
271         # Get record (must be enabled)
272         persons = Persons(method.api, {'email': auth['Username'], 'enabled': True})
273         if len(persons) != 1:
274             raise PLCAuthenticationFailure, "No such account"
275
276         person = persons[0]
277
278         if auth['Username'] == method.api.config.PLC_API_MAINTENANCE_USER:
279             # "Capability" authentication, whatever the hell that was
280             # supposed to mean. It really means, login as the special
281             # "maintenance user" using password authentication. Can
282             # only be used on particular machines (those in a list).
283             sources = method.api.config.PLC_API_MAINTENANCE_SOURCES.split()
284             if method.source is not None and method.source[0] not in sources:
285                 raise PLCAuthenticationFailure, "Not allowed to login to maintenance account"
286
287             # Not sure why this is not stored in the DB
288             password = method.api.config.PLC_API_MAINTENANCE_PASSWORD
289
290             if auth['AuthString'] != password:
291                 raise PLCAuthenticationFailure, "Maintenance account password verification failed"
292         else:
293             # Compare encrypted plaintext against encrypted password stored in the DB
294             plaintext = auth['AuthString'].encode(method.api.encoding)
295             password = person['password']
296
297             # Protect against blank passwords in the DB
298             if password is None or password[:12] == "" or \
299                crypt.crypt(plaintext, password[:12]) != password:
300                 raise PLCAuthenticationFailure, "Password verification failed"
301
302         if not set(person['roles']).intersection(method.roles):
303             raise PLCAuthenticationFailure, "Not allowed to call method"
304
305         method.caller = person