source: fedd/fedd_proj.py @ 329f61d

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

basic XMLRPC

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