source: fedd/fedd_proj.py @ 7da9da6

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

split out project creation. Local project creation works

  • Property mode set to 100644
File size: 19.7 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 subprocess
16import tempfile
17import copy
18
19from fedd_services import *
20from fedd_util import *
21from fedd_allocate_project import *
22import parse_detail
23from service_error import *
24
25class fedd_proj:
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    # Attributes that can be parsed from the configuration file
33    bool_attrs = ("dynamic_projects", "project_priority")
34    emulab_attrs = ("boss", "ops", "domain", "fileserver", "eventserver")
35    id_attrs = ("testbed", "cert_file", "trusted_certs", "proxy",
36            "proxy_trusted_certs", "cert_pwd")
37
38    # Used by the SOAP caller
39    soap_namespace = 'http://www.isi.edu/faber/fedd.wsdl'
40    soap_methods = { 'RequestAccess': 'soap_RequestAccess' }
41    xmlrpc_methods = { 'RequestAccess': 'xmlrpc_RequestAccess' }
42
43    class access_project:
44        """
45        A project description used to grant access to this testbed.
46
47        The description includes a name and a list of node types to which the
48        project will be granted access.
49        """
50        def __init__(self, name, nt):
51            self.name = name
52            self.node_types = list(nt)
53
54        def __repr__(self):
55            if len(self.node_types) > 0:
56                return "access_proj('%s', ['%s'])" % \
57                        (self.name, str("','").join(self.node_types))
58            else:
59                return "access_proj('%s', [])" % self.name
60
61    # Used to report errors parsing the configuration files, not in providing
62    # service
63    class parse_error(RuntimeError): pass
64
65
66    def __init__(self, config=None):
67        """
68        Initializer.  Parses a configuration if one is given.
69        """
70
71        # Create instance attributes from the static lists
72        for a in fedd_proj.bool_attrs:
73            setattr(self, a, False)
74
75        for a in fedd_proj.emulab_attrs + fedd_proj.id_attrs:
76            setattr(self, a, None)
77
78        # Other attributes
79        self.attrs = {}
80        self.access = {}
81        self.fedid_category = {}
82        self.fedid_default = "user"
83        self.restricted = []
84
85        # Delete these
86        self.wap = '/usr/testbed/sbin/wap'
87        self.newproj = '/usr/testbed/sbin/newproj'
88        self.mkproj = '/usr/testbed/sbin/mkproj'
89        self.grantnodetype = '/usr/testbed/sbin/grantnodetype'
90
91        # Read the configuration
92        if config != None: 
93            self.read_config(config)
94        self.allocate_project = \
95            fedd_allocate_project_local(self.dynamic_projects)
96
97    def dump_state(self):
98        """
99        Dump the state read from a configuration file.  Mostly for debugging.
100        """
101        for a in fedd_proj.bool_attrs:
102            print "%s: %s" % (a, getattr(self, a ))
103        for a in fedd_proj.emulab_attrs + fedd_proj.id_attrs:
104            print "%s: %s" % (a, getattr(self, a))
105        for k, v in self.attrs.iteritems():
106            print "%s %s" % (k, v)
107        print "Access DB:"
108        for k, v in self.access.iteritems():
109            print "%s %s" % (k, v)
110        print "Trust DB:"
111        for k, v in self.fedid_category.iteritems():
112            print "%s %s" % (k, v)
113        print "Restricted: %s" % str(',').join(sorted(self.restricted))
114
115    def get_users(self, obj):
116        """
117        Return a list of the IDs of the users in dict
118        """
119        if obj.has_key('user'):
120            return [ unpack_id(u['userID']) \
121                    for u in obj['user'] if u.has_key('userID') ]
122        else:
123            return None
124
125    def strip_unicode(self, obj):
126        """Loosly de-unicode an object"""
127        if isinstance(obj, dict):
128            for k in obj.keys():
129                obj[k] = self.strip_unicode(obj[k])
130            return obj
131        elif isinstance(obj, basestring):
132            return str(obj)
133        elif getattr(obj, "__iter__", None):
134            return [ self.strip_unicode(x) for x in obj]
135        else:
136            return obj
137
138    def proxy_xmlrpc_request(self, dt, req):
139        """Send an XMLRPC proxy request.  Called if the SOAP RPC fails"""
140        tc = self.proxy_trusted_certs or self.trusted_certs
141
142        # No retry loop here.  Proxy servers must correctly authenticate
143        # themselves without help
144        try:
145            ctx = fedd_ssl_context(self.cert_file, tc, password=self.cert_pwd)
146        except SSL.SSLError:
147            raise service_error(service_error.server_config,
148                    "Server certificates misconfigured")
149
150        # Of all the dumbass things.  The XMLRPC library in use here won't
151        # properly encode unicode strings, so we make a copy of req with the
152        # unicode objects converted.  We also convert the destination testbed
153        # to a basic string if it isn't one already.
154        if isinstance(dt, str): url = dt
155        else: url = str(dt)
156
157        r = copy.deepcopy(req)
158        self.strip_unicode(r)
159       
160        transport = SSL_Transport(ctx)
161        port = xmlrpclib.ServerProxy(url, transport=transport)
162
163        # Reconstruct the full request message
164        try:
165            resp = port.RequestAccess(
166                    { "RequestAccessRequestBody": r})
167            resp, method = xmlrpclib.loads(resp)
168        except xmlrpclib.Fault, f:
169            se = service_error(None, f.faultString, f.faultCode)
170            raise se
171        except xmlrpclib.Error, e:
172            raise service_error(service_error.proxy, 
173                    "Remote XMLRPC Fault: %s" % e)
174       
175        if resp[0].has_key('RequestAccessResponseBody'):
176            return resp[0]['RequestAccessResponseBody']
177        else:
178            raise service_error(service_error.proxy, 
179                    "Bad proxy response")
180
181    def proxy_request(self, dt, req):
182        """
183        Send req on to the real destination in dt and return the response
184
185        Req is just the requestType object.  This function re-wraps it.  It
186        also rethrows any faults.
187        """
188        tc = self.proxy_trusted_certs or self.trusted_certs
189
190        # No retry loop here.  Proxy servers must correctly authenticate
191        # themselves without help
192        try:
193            ctx = fedd_ssl_context(self.cert_file, tc, password=self.cert_pwd)
194        except SSL.SSLError:
195            raise service_error(service_error.server_config, 
196                    "Server certificates misconfigured")
197
198        loc = feddServiceLocator();
199        port = loc.getfeddPortType(dt,
200                transport=M2Crypto.httpslib.HTTPSConnection, 
201                transdict={ 'ssl_context' : ctx })
202
203        # Reconstruct the full request message
204        msg = RequestAccessRequestMessage()
205        msg.set_element_RequestAccessRequestBody(
206                pack_soap(msg, "RequestAccessRequestBody", req))
207        try:
208            resp = port.RequestAccess(msg)
209        except ZSI.ParseException, e:
210            raise service_error(service_error.proxy,
211                    "Bad format message (XMLRPC??): %s" %
212                    str(e))
213        r = unpack_soap(resp)
214
215        if r.has_key('RequestAccessResponseBody'):
216            return r['RequestAccessResponseBody']
217        else:
218            raise service_error(service_error.proxy,
219                    "Bad proxy response")
220
221    def permute_wildcards(self, a, p):
222        """Return a copy of a with various fields wildcarded.
223
224        The bits of p control the wildcards.  A set bit is a wildcard
225        replacement with the lowest bit being user then project then testbed.
226        """
227        if p & 1: user = ["<any>"]
228        else: user = a[2]
229        if p & 2: proj = "<any>"
230        else: proj = a[1]
231        if p & 4: tb = "<any>"
232        else: tb = a[0]
233
234        return (tb, proj, user)
235
236    def find_access(self, search):
237        """
238        Search the access DB for a match on this tuple.  Return the matching
239        access tuple and the user that matched.
240       
241        NB, if the initial tuple fails to match we start inserting wildcards in
242        an order determined by self.project_priority.  Try the list of users in
243        order (when wildcarded, there's only one user in the list).
244        """
245        if self.project_priority: perm = (0, 1, 2, 3, 4, 5, 6, 7)
246        else: perm = (0, 2, 1, 3, 4, 6, 5, 7)
247
248        for p in perm: 
249            s = self.permute_wildcards(search, p)
250            # s[2] is None on an anonymous, unwildcarded request
251            if s[2] != None:
252                for u in s[2]:
253                    if self.access.has_key((s[0], s[1], u)):
254                        return (self.access[(s[0], s[1], u)], u)
255            else:
256                if self.access.has_key(s):
257                    return (self.access[s], None)
258        return None
259
260    def lookup_access(self, req, fid):
261        """
262        Determine the allowed access for this request.  Return the access and
263        which fields are dynamic.
264
265        The fedid is needed to construct the request
266        """
267        tb = None
268        project = None
269        user = None
270
271        principal_type = self.fedid_category.get(fid, self.fedid_default)
272
273        if principal_type == "testbed": tb = fid
274
275        if req.has_key('project'):
276            p = req['project']
277            if p.has_key('name'):
278                project = unpack_id(p['name'])
279            user = self.get_users(p)
280        else:
281            user = self.get_users(req)
282
283        # Now filter by prinicpal type
284        if principal_type == "user":
285            if user != None:
286                fedids = [ u for u in user if isinstance(u, type(fid))]
287                if len(fedids) > 1:
288                    raise service_error(service_error.req,
289                            "User asserting multiple fedids")
290                elif len(fedids) == 1 and fedids[0] != fid:
291                    raise service_error(service_error.req,
292                            "User asserting different fedid")
293            project = None
294            tb = None
295        elif principal_type == "project":
296            if isinstance(project, type(fid)) and fid != project:
297                raise service_error(service_error.req,
298                        "Project asserting different fedid")
299            tb = None
300
301        # Ready to look up access
302        found, user_match = self.find_access((tb, project, user))
303       
304        if found == None:
305            raise service_error(service_error.access,
306                    "Access denied")
307
308        # resolve <dynamic> and <same> in found
309        dyn_proj = False
310        dyn_user = False
311
312        if found[0].name == "<same>":
313            if project != None:
314                found[0].name = project
315            else : 
316                raise service_error(\
317                        service_error.server_config,
318                        "Project matched <same> when no project given")
319        elif found[0].name == "<dynamic>":
320            found[0].name = None
321            dyn_proj = True
322
323        if found[1] == "<same>":
324            if user_match == "<any>":
325                if user != None: found = (found[0], user[0])
326                else: raise service_error(\
327                        service_error.server_config,
328                        "Matched <same> on anonymous request")
329            else:
330                found = (found[0], user_match)
331        elif found[1] == "<dynamic>":
332            found = (found[0], None)
333            dyn_user = True
334       
335        return found, (dyn_user, dyn_proj)
336
337    def build_response(self, alloc_id, ap):
338        """
339        Create the SOAP response.
340
341        Build the dictionary description of the response and use
342        fedd_utils.pack_soap to create the soap message.  NB that alloc_id is
343        a fedd_services_types.IDType_Holder pulled from the incoming message.
344        ap is the allocate project message returned from a remote project
345        allocation (even if that allocation was done locally).
346        """
347        # Because alloc_id is already a fedd_services_types.IDType_Holder,
348        # there's no need to repack it
349        msg = { 
350                'allocID': alloc_id,
351                'emulab': { 
352                    'domain': self.domain,
353                    'boss': self.boss,
354                    'ops': self.ops,
355                    'fileServer': self.fileserver,
356                    'eventServer': self.eventserver,
357                    'project': ap['project']
358                },
359            }
360        if len(self.attrs) > 0:
361            msg['emulab']['fedAttr'] = \
362                [ { 'attribute': x, 'value' : y } \
363                        for x,y in self.attrs.iteritems()]
364        return msg
365
366    def RequestAccess(self, req, fid):
367
368        if req.has_key('RequestAccessRequestBody'):
369            req = req['RequestAccessRequestBody']
370        else:
371            raise service_error(service_error.req, "No request!?")
372
373        if req.has_key('destinationTestbed'):
374            dt = unpack_id(req['destinationTestbed'])
375       
376        if dt == None or dt == self.testbed:
377            # Request for this fedd
378            found, dyn = self.lookup_access(req, fid)
379            restricted = None
380            ap = None
381
382            # Check for access to restricted nodes
383            if req.has_key('resources') and req['resources'].has_key('node'):
384                resources = req['resources']
385                restricted = [ t for n in resources['node'] \
386                                if n.has_key('hardware') \
387                                    for t in n['hardware'] \
388                                        if t in self.restricted ]
389                inaccessible = [ t for t in restricted \
390                                    if t not in found[0].node_types]
391                if len(inaccessible) > 0:
392                    raise service_error(service_error.access,
393                            "Access denied (nodetypes %s)" % \
394                            str(', ').join(inaccessible))
395
396            ssh = [ x['sshPubkey'] \
397                    for x in req['access'] if x.has_key('sshPubkey')]
398
399            if len(ssh) > 0: 
400                if dyn[1]: 
401                    # Compose the dynamic project request
402                    # (only dynamic, dynamic currently allowed)
403                    preq = { 'project' : {\
404                                    'user': [ \
405                                    { 'access': { 'sshPubkey': s } } \
406                                        for s in ssh ] \
407                                }\
408                            }
409                    if restricted != None and len(restricted) > 0:
410                        preq['resources'] =  [ {'node': { 'hardware' :  [ h ]\
411                            } } for h in restricted ]
412                               
413
414                    #self.dynamic_project(found, ssh)
415                    ap = self.allocate_project.dynamic_project(preq)
416                    # XXX: fill in response values into the real response
417                else: pass    # SSH key additions
418            else:
419                raise service_error(service_error.req, 
420                        "SSH access parameters required")
421
422            resp = self.build_response(req['allocID'], ap)
423            return resp
424        else:
425            p_fault = None      # Any SOAP failure (sent unless XMLRPC works)
426            try:
427                # Proxy the request using SOAP
428                return self.proxy_request(dt, req)
429            except service_error, e:
430                if e.code == service_error.proxy: p_fault = None
431                else: raise
432            except ZSI.FaultException, f:
433                p_fault = f.fault.detail[0]
434                   
435
436            # If we could not get a valid SOAP response to the request above,
437            # try the same address using XMLRPC and let any faults flow back
438            # out.
439            if p_fault == None:
440                return self.proxy_xmlrpc_request(dt, req)
441            else:
442                # Build the fault
443                body = p_fault.get_element_RequestAccessFaultBody()
444                if body != None:
445                    raise service_error(body.get_element_code(),
446                                body.get_element_desc());
447                else:
448                    raise service_error(\
449                            service_error.proxy,
450                            "Undefined fault from proxy??");
451
452    def soap_RequestAccess(self, ps, fid):
453        req = ps.Parse(RequestAccessRequestMessage.typecode)
454
455        msg = self.RequestAccess(unpack_soap(req), fedid)
456
457        resp = RequestAccessResponseMessage()
458        resp.set_element_RequestAccessResponseBody(
459                pack_soap(resp, "RequestAccessResponseBody", msg))
460
461        return resp
462
463    def xmlrpc_RequestAccess(self, params, fid):
464        msg = self.RequestAccess(params[0], fedid)
465
466        if msg != None:
467            return xmlrpclib.dumps(({ "RequestAccessResponseBody": msg },))
468        else:
469            raise service_error(service_error.internal,
470                    "No response generated?!");
471
472    def read_trust(self, trust):
473        """
474        Read a trust file that splits fedids into testbeds, users or projects
475
476        Format is:
477
478        [type]
479        fedid
480        fedid
481        default: type
482        """
483        lineno = 0;
484        cat = None
485        cat_re = re.compile("\[(user|testbed|project)\]$", re.IGNORECASE)
486        fedid_re = re.compile("[" + string.hexdigits + "]+$")
487        default_re = re.compile("default:\s*(user|testbed|project)$", 
488                re.IGNORECASE)
489
490        f = open(trust, "r")
491        for line in f:
492            lineno += 1
493            line = line.strip()
494            if len(line) == 0 or line.startswith("#"):
495                continue
496            # Category line
497            m = cat_re.match(line)
498            if m != None:
499                cat = m.group(1).lower()
500                continue
501            # Fedid line
502            m = fedid_re.match(line)
503            if m != None:
504                if cat != None:
505                    self.fedid_category[fedid(hexstr=m.string)] = cat
506                else:
507                    raise fedd_proj.parse_error(\
508                            "Bad fedid in trust file (%s) line: %d" % \
509                            (trust, lineno))
510                continue
511            # default line
512            m = default_re.match(line)
513            if m != None:
514                self.fedid_default = m.group(1).lower()
515                continue
516            # Nothing matched - bad line, raise exception
517            f.close()
518            raise fedd_proj.parse_error(\
519                    "Unparsable line in trustfile %s line %d" % (trust, lineno))
520        f.close()
521
522    def read_config(self, config):
523        """
524        Read a configuration file and set internal parameters.
525
526        The format is more complex than one might hope.  The basic format is
527        attribute value pairs separated by colons(:) on a signle line.  The
528        attributes in bool_attrs, emulab_attrs and id_attrs can all be set
529        directly using the name: value syntax.  E.g.
530        boss: hostname
531        sets self.boss to hostname.  In addition, there are access lines of the
532        form (tb, proj, user) -> (aproj, auser) that map the first tuple of
533        names to the second for access purposes.  Names in the key (left side)
534        can include "<NONE> or <ANY>" to act as wildcards or to require the
535        fields to be empty.  Similarly aproj or auser can be <SAME> or
536        <DYNAMIC> indicating that either the matching key is to be used or a
537        dynamic user or project will be created.  These names can also be
538        federated IDs (fedid's) if prefixed with fedid:.  Finally, the aproj
539        can be followed with a colon-separated list of node types to which that
540        project has access (or will have access if dynamic).
541        Testbed attributes outside the forms above can be given using the
542        format attribute: name value: value.  The name is a single word and the
543        value continues to the end of the line.  Empty lines and lines startin
544        with a # are ignored.
545
546        Parsing errors result in a parse_error exception being raised.
547        """
548        lineno=0
549        name_expr = "["+string.ascii_letters + string.digits + "\.\-_]+"
550        fedid_expr = "fedid:[" + string.hexdigits + "]+"
551        key_name = "(<ANY>|<NONE>|"+fedid_expr + "|"+ name_expr + ")"
552        access_proj = "(<DYNAMIC>(?::" + name_expr +")*|"+ \
553                "<SAME>" + "(?::" + name_expr + ")*|" + \
554                fedid_expr + "(?::" + name_expr + ")*|" + \
555                name_expr + "(?::" + name_expr + ")*)"
556        access_name = "(<DYNAMIC>|<SAME>|" + fedid_expr + "|"+ name_expr + ")"
557
558        bool_re = re.compile('(' + '|'.join(fedd_proj.bool_attrs) + 
559                '):\s+(true|false)', re.IGNORECASE)
560        string_re = re.compile( "(" + \
561                '|'.join(fedd_proj.emulab_attrs + fedd_proj.id_attrs) + \
562                '):\s*(.*)', re.IGNORECASE)
563        attr_re = re.compile('attribute:\s*([\._\-a-z0-9]+)\s+value:\s*(.*)',
564                re.IGNORECASE)
565        access_re = re.compile('\('+key_name+'\s*,\s*'+key_name+'\s*,\s*'+
566                key_name+'\s*\)\s*->\s*\('+access_proj + '\s*,\s*' + 
567                access_name + '\s*\)', re.IGNORECASE)
568        trustfile_re = re.compile("trustfile:\s*(.*)", re.IGNORECASE)
569        restricted_re = re.compile("restricted:\s*(.*)", re.IGNORECASE)
570
571        def parse_name(n):
572            if n.startswith('fedid:'): return fedid(n[len('fedid:'):])
573            else: return n
574
575        f = open(config, "r");
576        for line in f:
577            lineno += 1
578            line = line.strip();
579            if len(line) == 0 or line.startswith('#'):
580                continue
581
582            # Boolean attribute line
583            m = bool_re.match(line);
584            if m != None:
585                attr, val = m.group(1,2)
586                setattr(self, attr.lower(), bool(val.lower() == "true"))
587                continue
588
589            # String attribute line
590            m = string_re.match(line)
591            if m != None:
592                attr, val = m.group(1,2)
593                setattr(self, attr.lower(), val)
594                continue
595
596            # Extended (attribute: x value: y) attribute line
597            m = attr_re.match(line)
598            if m != None:
599                attr, val = m.group(1,2)
600                self.attrs[attr] = val
601                continue
602
603            # Access line (t, p, u) -> (ap, au) line
604            m = access_re.match(line)
605            if m != None:
606                access_key = tuple([ parse_name(x) for x in m.group(1,2,3)])
607                aps = m.group(4).split(":");
608                if aps[0] == 'fedid:':
609                    del aps[0]
610                    aps[0] = fedid(hexstr=aps[0])
611
612                au = m.group(5)
613                if au.startswith("fedid:"):
614                    au = fedid(hexstr=aus[len("fedid:"):])
615
616                access_val = (fedd_proj.access_project(aps[0], aps[1:]), au)
617
618                self.access[access_key] = access_val
619                continue
620
621            # Trustfile inclusion
622            m = trustfile_re.match(line)
623            if m != None:
624                self.read_trust(m.group(1))
625                continue
626            # Restricted node types
627
628            m = restricted_re.match(line)
629            if m != None:
630                self.restricted.append(m.group(1))
631                continue
632
633            # Nothing matched to here: unknown line - raise exception
634            f.close()
635            raise fedd_proj.parse_error("Unknown statement at line %d of %s" % \
636                    (lineno, config))
637        f.close()
638
639    def soap_dispatch(self, method, req, fid):
640        if fedd_proj.soap_methods.has_key(method):
641            try:
642                return getattr(self, fedd_proj.soap_methods[method])(req, fid)
643            except service_error, e:
644                de = ns0.faultType_Def(
645                        (ns0.faultType_Def.schema,
646                            "RequestAccessFaultBody")).pyclass()
647                de._code=e.code
648                de._errstr=e.code_string()
649                de._desc=e.desc
650                if  e.is_server_error():
651                    raise Fault(Fault.Server, e.code_string(), detail=de)
652                else:
653                    raise Fault(Fault.Client, e.code_string(), detail=de)
654        else:
655            raise Fault(Fault.Client, "Unknown method: %s" % method)
656
657    def xmlrpc_dispatch(self, method, req, fid):
658        if fedd_proj.xmlrpc_methods.has_key(method):
659            try:
660                return getattr(self, fedd_proj.xmlrpc_methods[method])(req, fid)
661            except service_error, e:
662                raise xmlrpclib.Fault(e.code_string(), e.desc)
663        else:
664            raise xmlrpclib.Fault(100, "Unknown method: %s" % method)
665
666def new_feddservice(configfile):
667    return fedd_proj(configfile)
Note: See TracBrowser for help on using the repository browser.