source: fedd/fedd_proj.py @ 5b8b886

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

back out some of the extra conversions to dictionary. Not useful for XMLRPC (left the ones that simplified code, though)

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