source: fedd/fedd_access.py @ abb87eb

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

split acces to service and experiment access

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