source: fedd/fedd_access.py @ ea0a821

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

refactoring

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