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