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