source: fedd/fedd_proj.py @ 2d58549

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

use dictionaries internally to facilitate XMLRPC

  • Property mode set to 100644
File size: 18.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.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 dict
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(
248                pack_soap(msg, "RequestAccessRequestBody", req))
249        try:
250            resp = port.RequestAccess(msg)
251        except ZSI.FaultException, e:
252            raise e.fault
253
254        r = unpack_soap(resp)
255
256        if r.has_key('RequestAccessResponseBody'):
257            return r['RequestAccessResponseBody']
258        else:
259            raise Fault(Fault.Server, "Bad proxy response")
260
261    def permute_wildcards(self, a, p):
262        """Return a copy of a with various fields wildcarded.
263
264        The bits of p control the wildcards.  A set bit is a wildcard
265        replacement with the lowest bit being user then project then testbed.
266        """
267        if p & 1: user = ["<any>"]
268        else: user = a[2]
269        if p & 2: proj = "<any>"
270        else: proj = a[1]
271        if p & 4: tb = "<any>"
272        else: tb = a[0]
273
274        return (tb, proj, user)
275
276    def find_access(self, search):
277        """
278        Search the access DB for a match on this tuple.  Return the matching
279        access tuple and the user that matched.
280       
281        NB, if the initial tuple fails to match we start inserting wildcards in
282        an order determined by self.project_priority.  Try the list of users in
283        order (when wildcarded, there's only one user in the list).
284        """
285        if self.project_priority: perm = (0, 1, 2, 3, 4, 5, 6, 7)
286        else: perm = (0, 2, 1, 3, 4, 6, 5, 7)
287
288        for p in perm: 
289            s = self.permute_wildcards(search, p)
290            # s[2] is None on an anonymous, unwildcarded request
291            if s[2] != None:
292                for u in s[2]:
293                    if self.access.has_key((s[0], s[1], u)):
294                        return (self.access[(s[0], s[1], u)], u)
295            else:
296                if self.access.has_key(s):
297                    return (self.access[s], None)
298        return None
299
300    def lookup_access(self, req, fid):
301        """
302        Determine the allowed access for this request.  Return the access and
303        which fields are dynamic.
304
305        The fedid is needed to construct the request
306        """
307        tb = None
308        project = None
309        user = None
310
311        principal_type = self.fedid_category.get(fid, self.fedid_default)
312
313        if principal_type == "testbed": tb = fid
314
315        if req.has_key('project'):
316            p = req['project']
317            if p.has_key('name'):
318                project = unpack_id(p['name'])
319            user = self.get_users(p)
320        else:
321            user = self.get_users(req)
322
323        # Now filter by prinicpal type
324        if principal_type == "user":
325            if user != None:
326                fedids = [ u for u in user if isinstance(u, type(fid))]
327                if len(fedids) > 1:
328                    raise Fault(Fault.Client,
329                            "User asserting multiple fedids")
330                elif len(fedids) == 1 and fedids[0] != fid:
331                    raise Fault(Fault.Client, 
332                            "User asserting different fedid")
333            project = None
334            tb = None
335        elif principal_type == "project":
336            if isinstance(project, type(fid)) and fid != project:
337                raise Fault(Fault.Client,
338                        "Project asserting different fedid")
339            tb = None
340
341        # Ready to look up access
342        print "Lookup %s %s %s: " % (tb, project, user)
343        found, user_match = self.find_access((tb, project, user))
344        print "Found: ", found
345       
346        if found == None: raise Fault(Fault.Server, "Access denied")
347
348        # resolve <dynamic> and <same> in found
349        dyn_proj = False
350        dyn_user = False
351
352        if found[0].name == "<same>":
353            if project != None:
354                found[0].name = project
355            else : 
356                raise Fault(Fault.Server, 
357                        "Project matched <same> when no project given")
358        elif found[0].name == "<dynamic>":
359            found[0].name = self.random_string("project", 3)
360            dyn_proj = True
361
362        if found[1] == "<same>":
363            if user_match == "<any>":
364                if user != None: found = (found[0], user[0])
365                else: raise Fault(Fault.Server,
366                        "Matched <same> on anonymous request")
367            else:
368                found = (found[0], user_match)
369        elif found[1] == "<dynamic>":
370            found = (found[0], self.random_string("user", 4))
371            dyn_user = True
372       
373        return found, (dyn_user, dyn_proj)
374
375    def build_response(self, alloc_id, ap, ssh):
376        """
377        Create the SOAP response.
378
379        Build the dictionary description of the response and use
380        fedd_utils.pack_soap to create the soap message.  NB that alloc_id is
381        a fedd_services_types.IDType_Holder pulled from the incoming message
382        """
383        # Because alloc_id is already a fedd_services_types.IDType_Holder,
384        # there's no need to repack it
385        msg = { 
386                'allocID': alloc_id,
387                'emulab': { 
388                    'domain': self.domain,
389                    'boss': self.boss,
390                    'ops': self.ops,
391                    'fileServer': self.fileserver,
392                    'eventServer': self.eventserver,
393                    'project': {
394                        'name': pack_id(ap[0].name),
395                        'user': [ {
396                            'userID': pack_id(ap[1]),
397                            'access' : [ { 'sshPubkey': x } for x in ssh ],
398                            }
399                        ]
400                    }
401                },
402            }
403        if len(self.attrs) > 0:
404            msg['emulab']['fedAttr'] = \
405                [ { 'attribute': x, 'value' : y } \
406                        for x,y in self.attrs.iteritems()]
407        return msg
408
409
410    def soap_RequestAccess(self, ps, fid):
411        req = ps.Parse(RequestAccessRequestMessage.typecode)
412
413
414        #if req == None: raise Fault(Fault.Client, "No request??")
415        #req = req.get_element_RequestAccessRequestBody()
416        # if req == None: raise Fault(Fault.Client, "No request body??")
417
418        msg = self.RequestAccess(unpack_soap(req), fedid)
419        resp = RequestAccessResponseMessage()
420
421        resp.set_element_RequestAccessResponseBody(
422                pack_soap(resp, "RequestAccessResponseBody", msg))
423        return resp
424
425    def RequestAccess(self, req, fid):
426
427        if req.has_key('RequestAccessRequestBody'):
428            req = req['RequestAccessRequestBody']
429        else:
430            raise Fault(Fault.Client, "No request!?")
431
432        if req.has_key('destinationTestbed'):
433            dt = unpack_id(req['destinationTestbed'])
434       
435        print dt
436
437        if dt == None or dt == self.testbed:
438            # Request for this fedd
439            found, dyn = self.lookup_access(req, fid)
440
441            # Check for access to restricted nodes
442            if req.has_key('resources') and req['resources'].has_key('node'):
443                resources = req['resources']
444                inaccessible = [ t for n in resources['node'] \
445                                if n.has_key('hardware') != None \
446                                    for t in n['hardware'] \
447                                        if t in self.restricted and \
448                                                t not in found[0].node_types]
449                if len(inaccessible) > 0:
450                    raise Fault(Fault.Server, 
451                            "Access denied (nodetypes %s)" % \
452                            str(', ').join(inaccessible))
453
454            ssh = [ x['sshPubkey'] \
455                    for x in req['access'] if x.has_key('sshPubkey')]
456
457            if len(ssh) > 0: 
458                if dyn[1]: self.dynamic_project(found, ssh)
459                else: pass    # SSH key additions
460            else:
461                raise Fault(Fault.Client, "SSH access parameters required")
462
463            resp = self.build_response(req['allocID'], found, ssh)
464            return resp
465        else:
466            # Proxy the request
467            return self.proxy_request(dt, req)
468
469    def read_trust(self, trust):
470        """
471        Read a trust file that splits fedids into testbeds, users or projects
472
473        Format is:
474
475        [type]
476        fedid
477        fedid
478        default: type
479        """
480        lineno = 0;
481        cat = None
482        cat_re = re.compile("\[(user|testbed|project)\]$", re.IGNORECASE)
483        fedid_re = re.compile("[" + string.hexdigits + "]+$")
484        default_re = re.compile("default:\s*(user|testbed|project)$", 
485                re.IGNORECASE)
486
487        f = open(trust, "r")
488        for line in f:
489            lineno += 1
490            line = line.strip()
491            if len(line) == 0 or line.startswith("#"):
492                continue
493            # Category line
494            m = cat_re.match(line)
495            if m != None:
496                cat = m.group(1).lower()
497                continue
498            # Fedid line
499            m = fedid_re.match(line)
500            if m != None:
501                if cat != None:
502                    self.fedid_category[fedid(hexstr=m.string)] = cat
503                else:
504                    raise fedd_proj.parse_error(\
505                            "Bad fedid in trust file (%s) line: %d" % \
506                            (trust, lineno))
507                continue
508            # default line
509            m = default_re.match(line)
510            if m != None:
511                self.fedid_default = m.group(1).lower()
512                continue
513            # Nothing matched - bad line, raise exception
514            f.close()
515            raise fedd_proj.parse_error(\
516                    "Unparsable line in trustfile %s line %d" % (trust, lineno))
517        f.close()
518
519    def read_config(self, config):
520        """
521        Read a configuration file and set internal parameters.
522
523        The format is more complex than one might hope.  The basic format is
524        attribute value pairs separated by colons(:) on a signle line.  The
525        attributes in bool_attrs, emulab_attrs and id_attrs can all be set
526        directly using the name: value syntax.  E.g.
527        boss: hostname
528        sets self.boss to hostname.  In addition, there are access lines of the
529        form (tb, proj, user) -> (aproj, auser) that map the first tuple of
530        names to the second for access purposes.  Names in the key (left side)
531        can include "<NONE> or <ANY>" to act as wildcards or to require the
532        fields to be empty.  Similarly aproj or auser can be <SAME> or
533        <DYNAMIC> indicating that either the matching key is to be used or a
534        dynamic user or project will be created.  These names can also be
535        federated IDs (fedid's) if prefixed with fedid:.  Finally, the aproj
536        can be followed with a colon-separated list of node types to which that
537        project has access (or will have access if dynamic).
538        Testbed attributes outside the forms above can be given using the
539        format attribute: name value: value.  The name is a single word and the
540        value continues to the end of the line.  Empty lines and lines startin
541        with a # are ignored.
542
543        Parsing errors result in a parse_error exception being raised.
544        """
545        lineno=0
546        name_expr = "["+string.ascii_letters + string.digits + "\.\-_]+"
547        fedid_expr = "fedid:[" + string.hexdigits + "]+"
548        key_name = "(<ANY>|<NONE>|"+fedid_expr + "|"+ name_expr + ")"
549        access_proj = "(<DYNAMIC>(?::" + name_expr +")*|"+ \
550                "<SAME>" + "(?::" + name_expr + ")*|" + \
551                fedid_expr + "(?::" + name_expr + ")*|" + \
552                name_expr + "(?::" + name_expr + ")*)"
553        access_name = "(<DYNAMIC>|<SAME>|" + fedid_expr + "|"+ name_expr + ")"
554
555        bool_re = re.compile('(' + '|'.join(fedd_proj.bool_attrs) + 
556                '):\s+(true|false)', re.IGNORECASE)
557        string_re = re.compile( "(" + \
558                '|'.join(fedd_proj.emulab_attrs + fedd_proj.id_attrs) + \
559                '):\s*(.*)', re.IGNORECASE)
560        attr_re = re.compile('attribute:\s*([\._\-a-z0-9]+)\s+value:\s*(.*)',
561                re.IGNORECASE)
562        access_re = re.compile('\('+key_name+'\s*,\s*'+key_name+'\s*,\s*'+
563                key_name+'\s*\)\s*->\s*\('+access_proj + '\s*,\s*' + 
564                access_name + '\s*\)', re.IGNORECASE)
565        trustfile_re = re.compile("trustfile:\s*(.*)", re.IGNORECASE)
566        restricted_re = re.compile("restricted:\s*(.*)", re.IGNORECASE)
567
568        def parse_name(n):
569            if n.startswith('fedid:'): return fedid(n[len('fedid:'):])
570            else: return n
571
572        f = open(config, "r");
573        for line in f:
574            lineno += 1
575            line = line.strip();
576            if len(line) == 0 or line.startswith('#'):
577                continue
578
579            # Boolean attribute line
580            m = bool_re.match(line);
581            if m != None:
582                attr, val = m.group(1,2)
583                setattr(self, attr.lower(), bool(val.lower() == "true"))
584                continue
585
586            # String attribute line
587            m = string_re.match(line)
588            if m != None:
589                attr, val = m.group(1,2)
590                setattr(self, attr.lower(), val)
591                continue
592
593            # Extended (attribute: x value: y) attribute line
594            m = attr_re.match(line)
595            if m != None:
596                attr, val = m.group(1,2)
597                self.attrs[attr] = val
598                continue
599
600            # Access line (t, p, u) -> (ap, au) line
601            m = access_re.match(line)
602            if m != None:
603                access_key = tuple([ parse_name(x) for x in m.group(1,2,3)])
604                aps = m.group(4).split(":");
605                if aps[0] == 'fedid:':
606                    del aps[0]
607                    aps[0] = fedid(hexstr=aps[0])
608
609                au = m.group(5)
610                if au.startswith("fedid:"):
611                    au = fedid(hexstr=aus[len("fedid:"):])
612
613                access_val = (fedd_proj.access_project(aps[0], aps[1:]), au)
614
615                self.access[access_key] = access_val
616                continue
617
618            # Trustfile inclusion
619            m = trustfile_re.match(line)
620            if m != None:
621                self.read_trust(m.group(1))
622                continue
623            # Restricted node types
624
625            m = restricted_re.match(line)
626            if m != None:
627                self.restricted.append(m.group(1))
628                continue
629
630            # Nothing matched to here: unknown line - raise exception
631            f.close()
632            raise fedd_proj.parse_error("Unknown statement at line %d of %s" % \
633                    (lineno, config))
634        f.close()
635
636def new_feddservice(configfile):
637    return fedd_proj(configfile)
Note: See TracBrowser for help on using the repository browser.