source: fedd/fedd_access.py @ 11a08b0

axis_examplecompt_changesinfo-opsversion-1.30version-2.00version-3.01version-3.02
Last change on this file since 11a08b0 was 11a08b0, checked in by Ted Faber <faber@…>, 16 years ago

decent logging

  • Property mode set to 100644
File size: 14.3 KB
Line 
1#!/usr/local/bin/python
2
3import os,sys
4
5from BaseHTTPServer import BaseHTTPRequestHandler
6from ZSI import *
7from M2Crypto import SSL
8from M2Crypto.m2xmlrpclib import SSL_Transport
9from M2Crypto.SSL.SSLServer import SSLServer
10import M2Crypto.httpslib
11import xmlrpclib
12
13import re
14import string
15import copy
16
17from fedd_config_file import config_file
18from fedd_access_project import access_project
19from fedd_services import *
20from fedd_util import *
21from fedd_allocate_project import *
22import parse_detail
23from service_error import *
24import logging
25
26class nullHandler(logging.Handler):
27    def emit(self, record): pass
28
29fl = logging.getLogger("fedd.access")
30fl.addHandler(nullHandler())
31
32class fedd_access:
33    """
34    The implementation of access control based on mapping users to projects.
35
36    Users can be mapped to existing projects or have projects created
37    dynamically.  This implements both direct requests and proxies.
38    """
39
40    def __init__(self, config=None):
41        """
42        Initializer.  Parses a configuration if one is given.
43        """
44
45        # Read the configuration
46        if not config: raise RunTimeError("No config to fedd_allocate")
47
48        # Create instance attributes from the static lists
49        for a in config_file.bool_attrs:
50            setattr(self, a, getattr(config, a, False))
51
52        for a in config_file.emulab_attrs + config_file.id_attrs:
53            setattr(self, a, getattr(config, a, None))
54
55
56        self.attrs = copy.deepcopy(config.attrs);
57        self.access = copy.deepcopy(config.access);
58        self.fedid_category = copy.deepcopy(config.fedid_category)
59        self.fedid_default = config.fedid_default
60        self.restricted = copy.copy(config.restricted)
61
62        self.log = logging.getLogger("fedd.access")
63
64        # Certs are promoted from the generic to the specific, so without a
65        # specific proxy certificate, the main certificates are used for
66        # proxy interactions. If no dynamic project certificates, then
67        # proxy certs are used, and if none of those the main certs.
68
69        if config.proxy_cert_file:
70            if not self.dynamic_projects_cert_file:
71                self.dynamic_projects_cert_file = config.proxy_cert_file
72                self.dynamic_projects_cert_pwd = config.proxy_cert_pwd
73
74        if config.proxy_trusted_certs:
75            if not self.dynamic_projects_trusted_certs:
76                self.dynamic_projects_trusted_certs =\
77                        config.proxy_trusted_certs
78
79        if config.cert_file:
80            if not self.dynamic_projects_cert_file:
81                self.dynamic_projects_cert_file = config.cert_file
82                self.dynamic_projects_cert_pwd = config.cert_pwd
83            if not self.proxy_cert_file:
84                self.proxy_cert_file = config.cert_file
85                self.proxy_cert_pwd = config.cert_pwd
86
87        if config.trusted_certs:
88            if not self.proxy_trusted_certs:
89                self.proxy_trusted_certs = config.trusted_certs
90            if not self.dynamic_projects_trusted_certs:
91                self.dynamic_projects_trusted_certs = config.trusted_certs
92
93        proj_certs = (self.dynamic_projects_cert_file, 
94                self.dynamic_projects_trusted_certs,
95                self.dynamic_projects_cert_pwd)
96
97
98        if config.dynamic_projects_url == None:
99            self.allocate_project = \
100                fedd_allocate_project_local(config.dynamic_projects, 
101                        config.dynamic_projects_url, proj_certs)
102        else:
103            self.allocate_project = \
104                fedd_allocate_project_remote(config.dynamic_projects, 
105                        config.dynamic_projects_url, proj_certs)
106
107        self.soap_handlers = {\
108            'RequestAccess': make_soap_handler(\
109                RequestAccessRequestMessage.typecode,\
110                self.RequestAccess, RequestAccessResponseMessage,\
111                "RequestAccessResponseBody")\
112            }
113        self.xmlrpc_handlers =  {\
114            'RequestAccess': make_xmlrpc_handler(\
115                self.RequestAccess, "RequestAccessResponseBody")\
116            }
117
118    def dump_state(self):
119        """
120        Dump the state read from a configuration file.  Mostly for debugging.
121        """
122        for a in fedd_proj.bool_attrs:
123            print "%s: %s" % (a, getattr(self, a ))
124        for a in fedd_proj.emulab_attrs + fedd_proj.id_attrs:
125            print "%s: %s" % (a, getattr(self, a))
126        for k, v in self.attrs.iteritems():
127            print "%s %s" % (k, v)
128        print "Access DB:"
129        for k, v in self.access.iteritems():
130            print "%s %s" % (k, v)
131        print "Trust DB:"
132        for k, v in self.fedid_category.iteritems():
133            print "%s %s" % (k, v)
134        print "Restricted: %s" % str(',').join(sorted(self.restricted))
135
136    def get_users(self, obj):
137        """
138        Return a list of the IDs of the users in dict
139        """
140        if obj.has_key('user'):
141            return [ unpack_id(u['userID']) \
142                    for u in obj['user'] if u.has_key('userID') ]
143        else:
144            return None
145
146    def strip_unicode(self, obj):
147        """Loosly de-unicode an object"""
148        if isinstance(obj, dict):
149            for k in obj.keys():
150                obj[k] = self.strip_unicode(obj[k])
151            return obj
152        elif isinstance(obj, basestring):
153            return str(obj)
154        elif getattr(obj, "__iter__", None):
155            return [ self.strip_unicode(x) for x in obj]
156        else:
157            return obj
158
159    def proxy_xmlrpc_request(self, dt, req):
160        """Send an XMLRPC proxy request.  Called if the SOAP RPC fails"""
161
162        # No retry loop here.  Proxy servers must correctly authenticate
163        # themselves without help
164        try:
165            ctx = fedd_ssl_context(self.proxy_cert_file, 
166                    self.proxy_trusted_certs, password=self.proxy_cert_pwd)
167        except SSL.SSLError:
168            raise service_error(service_error.server_config,
169                    "Server certificates misconfigured")
170
171        # Of all the dumbass things.  The XMLRPC library in use here won't
172        # properly encode unicode strings, so we make a copy of req with the
173        # unicode objects converted.  We also convert the destination testbed
174        # to a basic string if it isn't one already.
175        if isinstance(dt, str): url = dt
176        else: url = str(dt)
177
178        r = copy.deepcopy(req)
179        self.strip_unicode(r)
180       
181        transport = SSL_Transport(ctx)
182        port = xmlrpclib.ServerProxy(url, transport=transport)
183
184        # Reconstruct the full request message
185        try:
186            resp = port.RequestAccess(
187                    { "RequestAccessRequestBody": r})
188            resp, method = xmlrpclib.loads(resp)
189        except xmlrpclib.Fault, f:
190            se = service_error(None, f.faultString, f.faultCode)
191            raise se
192        except xmlrpclib.Error, e:
193            raise service_error(service_error.proxy, 
194                    "Remote XMLRPC Fault: %s" % e)
195       
196        if resp[0].has_key('RequestAccessResponseBody'):
197            return resp[0]['RequestAccessResponseBody']
198        else:
199            raise service_error(service_error.proxy, 
200                    "Bad proxy response")
201
202    def proxy_request(self, dt, req):
203        """
204        Send req on to the real destination in dt and return the response
205
206        Req is just the requestType object.  This function re-wraps it.  It
207        also rethrows any faults.
208        """
209        # No retry loop here.  Proxy servers must correctly authenticate
210        # themselves without help
211        try:
212            ctx = fedd_ssl_context(self.proxy_cert_file, 
213                    self.proxy_trusted_certs, password=self.proxy_cert_pwd)
214        except SSL.SSLError:
215            raise service_error(service_error.server_config, 
216                    "Server certificates misconfigured")
217
218        loc = feddServiceLocator();
219        port = loc.getfeddPortType(dt,
220                transport=M2Crypto.httpslib.HTTPSConnection, 
221                transdict={ 'ssl_context' : ctx })
222
223        # Reconstruct the full request message
224        msg = RequestAccessRequestMessage()
225        msg.set_element_RequestAccessRequestBody(
226                pack_soap(msg, "RequestAccessRequestBody", req))
227        try:
228            resp = port.RequestAccess(msg)
229        except ZSI.ParseException, e:
230            raise service_error(service_error.proxy,
231                    "Bad format message (XMLRPC??): %s" %
232                    str(e))
233        r = unpack_soap(resp)
234
235        if r.has_key('RequestAccessResponseBody'):
236            return r['RequestAccessResponseBody']
237        else:
238            raise service_error(service_error.proxy,
239                    "Bad proxy response")
240
241    def permute_wildcards(self, a, p):
242        """Return a copy of a with various fields wildcarded.
243
244        The bits of p control the wildcards.  A set bit is a wildcard
245        replacement with the lowest bit being user then project then testbed.
246        """
247        if p & 1: user = ["<any>"]
248        else: user = a[2]
249        if p & 2: proj = "<any>"
250        else: proj = a[1]
251        if p & 4: tb = "<any>"
252        else: tb = a[0]
253
254        return (tb, proj, user)
255
256    def find_access(self, search):
257        """
258        Search the access DB for a match on this tuple.  Return the matching
259        access tuple and the user that matched.
260       
261        NB, if the initial tuple fails to match we start inserting wildcards in
262        an order determined by self.project_priority.  Try the list of users in
263        order (when wildcarded, there's only one user in the list).
264        """
265        if self.project_priority: perm = (0, 1, 2, 3, 4, 5, 6, 7)
266        else: perm = (0, 2, 1, 3, 4, 6, 5, 7)
267
268        for p in perm: 
269            s = self.permute_wildcards(search, p)
270            # s[2] is None on an anonymous, unwildcarded request
271            if s[2] != None:
272                for u in s[2]:
273                    if self.access.has_key((s[0], s[1], u)):
274                        return (self.access[(s[0], s[1], u)], u)
275            else:
276                if self.access.has_key(s):
277                    return (self.access[s], None)
278        return None, None
279
280    def lookup_access(self, req, fid):
281        """
282        Determine the allowed access for this request.  Return the access and
283        which fields are dynamic.
284
285        The fedid is needed to construct the request
286        """
287        # Search keys
288        tb = None
289        project = None
290        user = None
291        # Return values
292        rp = access_project(None, ())
293        ru = None
294
295
296        principal_type = self.fedid_category.get(fid, self.fedid_default)
297
298        if principal_type == "testbed": tb = fid
299
300        if req.has_key('project'):
301            p = req['project']
302            if p.has_key('name'):
303                project = unpack_id(p['name'])
304            user = self.get_users(p)
305        else:
306            user = self.get_users(req)
307
308        # Now filter by prinicpal type
309        if principal_type == "user":
310            if user != None:
311                fedids = [ u for u in user if isinstance(u, type(fid))]
312                if len(fedids) > 1:
313                    raise service_error(service_error.req,
314                            "User asserting multiple fedids")
315                elif len(fedids) == 1 and fedids[0] != fid:
316                    raise service_error(service_error.req,
317                            "User asserting different fedid")
318            project = None
319            tb = None
320        elif principal_type == "project":
321            if isinstance(project, type(fid)) and fid != project:
322                raise service_error(service_error.req,
323                        "Project asserting different fedid")
324            tb = None
325
326        # Ready to look up access
327        found, user_match = self.find_access((tb, project, user))
328       
329        if found == None:
330            raise service_error(service_error.access,
331                    "Access denied")
332
333        # resolve <dynamic> and <same> in found
334        dyn_proj = False
335        dyn_user = False
336
337        if found[0].name == "<same>":
338            if project != None:
339                rp.name = project
340            else : 
341                raise service_error(\
342                        service_error.server_config,
343                        "Project matched <same> when no project given")
344        elif found[0].name == "<dynamic>":
345            rp.name = None
346            dyn_proj = True
347        else:
348            rp.name = found[0].name
349        rp.node_types = found[0].node_types;
350
351        if found[1] == "<same>":
352            if user_match == "<any>":
353                if user != None: ru = user[0]
354                else: raise service_error(\
355                        service_error.server_config,
356                        "Matched <same> on anonymous request")
357            else:
358                ru = user_match
359        elif found[1] == "<dynamic>":
360            ru = None
361            dyn_user = True
362       
363        return (rp, ru), (dyn_user, dyn_proj)
364
365    def build_response(self, alloc_id, ap):
366        """
367        Create the SOAP response.
368
369        Build the dictionary description of the response and use
370        fedd_utils.pack_soap to create the soap message.  NB that alloc_id is
371        a fedd_services_types.IDType_Holder pulled from the incoming message.
372        ap is the allocate project message returned from a remote project
373        allocation (even if that allocation was done locally).
374        """
375        # Because alloc_id is already a fedd_services_types.IDType_Holder,
376        # there's no need to repack it
377        msg = { 
378                'allocID': alloc_id,
379                'emulab': { 
380                    'domain': self.domain,
381                    'boss': self.boss,
382                    'ops': self.ops,
383                    'fileServer': self.fileserver,
384                    'eventServer': self.eventserver,
385                    'project': ap['project']
386                },
387            }
388        if len(self.attrs) > 0:
389            msg['emulab']['fedAttr'] = \
390                [ { 'attribute': x, 'value' : y } \
391                        for x,y in self.attrs.iteritems()]
392        return msg
393
394    def RequestAccess(self, req, fid):
395
396        if req.has_key('RequestAccessRequestBody'):
397            req = req['RequestAccessRequestBody']
398        else:
399            raise service_error(service_error.req, "No request!?")
400
401        if req.has_key('destinationTestbed'):
402            dt = unpack_id(req['destinationTestbed'])
403
404        if dt == None or dt == self.testbed:
405            # Request for this fedd
406            found, dyn = self.lookup_access(req, fid)
407            restricted = None
408            ap = None
409
410            # Check for access to restricted nodes
411            if req.has_key('resources') and req['resources'].has_key('node'):
412                resources = req['resources']
413                restricted = [ t for n in resources['node'] \
414                                if n.has_key('hardware') \
415                                    for t in n['hardware'] \
416                                        if t in self.restricted ]
417                inaccessible = [ t for t in restricted \
418                                    if t not in found[0].node_types]
419                if len(inaccessible) > 0:
420                    raise service_error(service_error.access,
421                            "Access denied (nodetypes %s)" % \
422                            str(', ').join(inaccessible))
423
424            ssh = [ x['sshPubkey'] \
425                    for x in req['access'] if x.has_key('sshPubkey')]
426
427            if len(ssh) > 0: 
428                if dyn[1]: 
429                    # Compose the dynamic project request
430                    # (only dynamic, dynamic currently allowed)
431                    preq = { 'AllocateProjectRequestBody': \
432                                { 'project' : {\
433                                    'user': [ \
434                                    { 'access': [ { 'sshPubkey': s } ] } \
435                                        for s in ssh ] \
436                                    }\
437                                }\
438                            }
439                    if restricted != None and len(restricted) > 0:
440                        preq['AllocateProjectRequestBody']['resources'] = \
441                            [ {'node': { 'hardware' :  [ h ] } } \
442                                    for h in restricted ]
443                               
444                    ap = self.allocate_project.dynamic_project(preq)
445                else:
446                    # XXX ssh key additions
447                    ap = { 'project': \
448                            { 'name' : { 'localname' : found[0].name },\
449                              'user' : [ {\
450                                'userID': { 'localname' : found[1] }, \
451                                'access': [ { 'sshPubkey': s } for s in ssh]}\
452                                ]\
453                            }\
454                    }
455            else:
456                raise service_error(service_error.req, 
457                        "SSH access parameters required")
458
459            resp = self.build_response(req['allocID'], ap)
460            return resp
461        else:
462            p_fault = None      # Any SOAP failure (sent unless XMLRPC works)
463            try:
464                # Proxy the request using SOAP
465                return self.proxy_request(dt, req)
466            except service_error, e:
467                if e.code == service_error.proxy: p_fault = None
468                else: raise
469            except ZSI.FaultException, f:
470                p_fault = f.fault.detail[0]
471                   
472
473            # If we could not get a valid SOAP response to the request above,
474            # try the same address using XMLRPC and let any faults flow back
475            # out.
476            if p_fault == None:
477                return self.proxy_xmlrpc_request(dt, req)
478            else:
479                # Build the fault
480                body = p_fault.get_element_FeddFaultBody()
481                if body != None:
482                    raise service_error(body.get_element_code(),
483                                body.get_element_desc());
484                else:
485                    raise service_error(\
486                            service_error.proxy,
487                            "Undefined fault from proxy??");
488   
489    def get_soap_services(self):
490        return self.soap_handlers
491
492    def get_xmlrpc_services(self):
493        return self.xmlrpc_handlers
494
Note: See TracBrowser for help on using the repository browser.