source: fedd/federation/access.py @ 1d73342

axis_examplecompt_changesinfo-ops
Last change on this file since 1d73342 was de86b35, checked in by Ted Faber <faber@…>, 13 years ago

Flat out bug in the lambda definition in access. The emulab_access deals with cases when services are requested but not export project. (That was a bug, too)

  • Property mode set to 100644
File size: 21.2 KB
Line 
1#!/usr/local/bin/python
2
3import os,sys
4import stat # for chmod constants
5import re
6import random
7import string
8import copy
9import pickle
10import logging
11import subprocess
12
13from threading import *
14from M2Crypto.SSL import SSLError
15
16from util import *
17from allocate_project import allocate_project_local, allocate_project_remote
18from fedid import fedid, generate_fedid
19from authorizer import authorizer
20from service_error import service_error
21from remote_service import xmlrpc_handler, soap_handler, service_caller
22
23import httplib
24import tempfile
25from urlparse import urlparse
26
27import topdl
28import list_log
29import proxy_emulab_segment
30import local_emulab_segment
31
32
33# Make log messages disappear if noone configures a fedd logger
34class nullHandler(logging.Handler):
35    def emit(self, record): pass
36
37fl = logging.getLogger("fedd.access")
38fl.addHandler(nullHandler())
39
40class access_base:
41    """
42    The implementation of access control based on mapping users to projects.
43
44    Users can be mapped to existing projects or have projects created
45    dynamically.  This implements both direct requests and proxies.
46    """
47
48    class parse_error(RuntimeError): pass
49
50    class access_attribute:
51        def __init__(self, attr, value, pri=1):
52            self.attr = attr
53            self.value = value
54            self.priority = pri
55        def __str__(self):
56            return "%s: %s (%d)" % (self.attr, self.value, self.priority)
57
58    def __init__(self, config=None, auth=None):
59        """
60        Initializer.  Pulls parameters out of the ConfigParser's access section.
61        """
62
63        # Make sure that the configuration is in place
64        if not config: 
65            raise RunTimeError("No config to fedd.access")
66
67        self.project_priority = config.getboolean("access", "project_priority")
68
69        self.certdir = config.get("access","certdir")
70        self.create_debug = config.getboolean("access", "create_debug")
71        self.cleanup = not config.getboolean("access", "leave_tmpfiles")
72        self.access_type = config.get("access", "type")
73        self.log = logging.getLogger("fedd.access")
74        set_log_level(config, "access", self.log)
75        self.state_lock = Lock()
76        self.state = { }
77        # subclasses fill with what and how they export.
78        self.exports = { }
79        # XXX: Configurable
80        self.imports = set(('SMB', 'seer', 'userconfig', 'seer_master',
81            'hide_hosts'))
82
83        if auth: self.auth = auth
84        else:
85            self.log.error(\
86                    "[access]: No authorizer initialized, creating local one.")
87            auth = authorizer()
88
89        self.state_filename = config.get("access", "access_state")
90        self.read_state()
91
92        # Keep cert_file and cert_pwd coming from the same place
93        self.cert_file = config.get("access", "cert_file")
94        if self.cert_file:
95            self.cert_pwd = config.get("access", "cert_pw")
96        else:
97            self.cert_file = config.get("globals", "cert_file")
98            self.sert_pwd = config.get("globals", "cert_pw")
99
100        self.trusted_certs = config.get("access", "trusted_certs") or \
101                config.get("globals", "trusted_certs")
102
103
104    @staticmethod
105    def software_list(v):
106        """
107        From a string containing a sequence of space separated pairs, return a
108        list of tuples with pairs of location and file.
109        """
110        l = [ ]
111        if v:
112            ps = v.split(" ")
113            while len(ps):
114                loc, file = ps[0:2]
115                del ps[0:2]
116                l.append((loc, file))
117        return l
118
119    @staticmethod
120    def add_kit(e, kit):
121        """
122        Add a Software object created from the list of (install, location)
123        tuples passed as kit  to the software attribute of an object e.  We
124        do this enough to break out the code, but it's kind of a hack to
125        avoid changing the old tuple rep.
126        """
127
128        s = [ topdl.Software(install=i, location=l) for i, l in kit]
129
130        if isinstance(e.software, list): e.software.extend(s)
131        else: e.software = s
132
133
134    def read_access(self, fn, access_obj=None, default=[]):
135        """
136        Read an access DB of the form
137            abac.attribute -> local_auth_data
138        The access dict is filled with mappings from the abac attributes (as
139        strings) to the access objects.  The objects are strings by default,
140        but the class constructor is called with the string following the ->
141        and whitespace in the file.
142        """
143
144        map_re = re.compile("(\S+)\s+->\s+(.*)")
145        priority_re = re.compile("([^,]+),\s*(\d+)")
146
147        if access_obj is None:
148            access_obj = lambda(x): "%s" % x
149
150        self.access = []
151        priorities = { }
152
153        f = open(fn, 'r')
154        try:
155            lineno = 0
156            for line in f:
157                lineno += 1
158                line = line.strip();
159                if len(line) == 0 or line.startswith('#'):
160                    continue
161                m = map_re.match(line)
162                if m != None:
163                    self.access.append(access_base.access_attribute(m.group(1),
164                        access_obj(m.group(2))))
165                    continue
166
167                # If a priority is found, collect them
168                m = priority_re.match(line)
169                if m:
170                    try:
171                        priorities[m.group(1)] = int(m.group(2))
172                    except ValueError, e:
173                        if self.log:
174                            self.log.debug("Bad priority in %s line %d" % \
175                                    (fn, lineno))
176                    continue
177
178                # Nothing matched to here: unknown line - raise exception
179                # (finally will close f)
180                raise self.parse_error(
181                        "Unknown statement at line %d of %s" % \
182                        (lineno, fn))
183        finally:
184            if f: f.close()
185
186        # Set priorities
187        for a in self.access:
188            if a.attr in priorities:
189                a.priority = priorities[a.attr]
190
191        # default access mappings
192        for a, v in default:
193            self.access.append(
194                    access_base.access_attribute(attr=a, value=v, pri=0))
195
196
197
198    def write_state(self):
199        if self.state_filename:
200            try:
201                f = open(self.state_filename, 'w')
202                pickle.dump(self.state, f)
203                self.log.debug("Wrote state to %s" % self.state_filename)
204            except EnvironmentError, e:
205                self.log.error("Can't write file %s: %s" % \
206                        (self.state_filename, e))
207            except pickle.PicklingError, e:
208                self.log.error("Pickling problem: %s" % e)
209            except TypeError, e:
210                self.log.error("Pickling problem (TypeError): %s" % e)
211
212
213    def read_state(self):
214        """
215        Read a new copy of access state.  Old state is overwritten.
216
217        State format is a simple pickling of the state dictionary.
218        """
219        if self.state_filename:
220            try:
221                f = open(self.state_filename, "r")
222                self.state = pickle.load(f)
223                self.log.debug("[read_state]: Read state from %s" % \
224                        self.state_filename)
225            except EnvironmentError, e:
226                self.log.warning(("[read_state]: No saved state: " +\
227                        "Can't open %s: %s") % (self.state_filename, e))
228            except EOFError, e:
229                self.log.warning(("[read_state]: " +\
230                        "Empty or damaged state file: %s:") % \
231                        self.state_filename)
232            except pickle.UnpicklingError, e:
233                self.log.warning(("[read_state]: No saved state: " + \
234                        "Unpickling failed: %s") % e)
235
236    def lookup_access(self, req, fid, filter=None, compare=None): 
237        """
238        Check all the attributes that this controller knows how to map and see
239        if the requester is allowed to use any of them.  If so return one.
240        Filter defined the objects to check - it's a function that returns true
241        for the objects to check - and cmp defines the order to check them in
242        as the cmp field of sorted().  If filter is None, all possibilities are
243        checked.  If cmp is None, the choices are sorted by priority.
244        """
245
246        # Import request credentials into this (clone later??)
247        if self.auth.import_credentials(
248                data_list=req.get('abac_credential', [])):
249            self.auth.save()
250
251        # NB: in the default case (the else), the comparison order is reversed
252        # so numerically larger priorities are checked first.
253        if compare: c = compare
254        else: c = lambda a, b: cmp(b,a)
255
256        if filter: f = filter
257        else: f = lambda(x): True
258
259        check = sorted([ a for a in self.access if f(a)], cmp=c)
260
261        # Check every attribute that we know how to map and take the first
262        # success.
263        for attr in check:
264            if self.auth.check_attribute(fid, attr.attr):
265                self.log.debug("Access succeeded for %s %s" % (attr.attr, fid))
266                # XXX: needs to deal with dynamics
267                return copy.copy(attr.value), (False, False, False), \
268                        [ fid ]
269            else:
270                self.log.debug("Access failed for %s %s" % (attr.attr, fid))
271        else:
272            raise service_error(service_error.access, "Access denied")
273
274
275
276    def get_handler(self, path, fid):
277        """
278        This function is somewhat oddly named.  It doesn't get a handler, it
279        handles GETs.  Specifically, it handls https GETs for retrieving data
280        from the repository exported by the access server.
281        """
282        self.log.info("Get handler %s %s" % (path, fid))
283        if self.auth.check_attribute(fid, path) and self.userconfdir:
284            return ("%s/%s" % (self.userconfdir, path), "application/binary")
285        else:
286            return (None, None)
287
288    def export_userconf(self, project):
289        dev_null = None
290        confid, confcert = generate_fedid("test", dir=self.userconfdir, 
291                log=self.log)
292        conffilename = "%s/%s" % (self.userconfdir, str(confid))
293        cf = None
294        try:
295            cf = open(conffilename, "w")
296            os.chmod(conffilename, stat.S_IRUSR | stat.S_IWUSR)
297        except EnvironmentError, e:
298            raise service_error(service_error.internal, 
299                    "Cannot create user configuration data")
300
301        try:
302            dev_null = open("/dev/null", "a")
303        except EnvironmentError, e:
304            self.log.error("export_userconf: can't open /dev/null: %s" % e)
305
306        cmd = "%s %s" % (self.userconfcmd, project)
307        conf = subprocess.call(cmd.split(" "),
308                stdout=cf, stderr=dev_null, close_fds=True)
309
310        self.auth.set_attribute(confid, "/%s" % str(confid))
311
312        return confid, confcert
313
314    def export_SMB(self, id, state, project, user, attrs):
315        if project and user:
316            return [{ 
317                    'id': id,
318                    'name': 'SMB',
319                    'visibility': 'export',
320                    'server': 'http://fs:139',
321                    'fedAttr': [
322                            { 'attribute': 'SMBSHARE', 'value': 'USERS' },
323                            { 'attribute': 'SMBUSER', 'value': user },
324                            { 'attribute': 'SMBPROJ', 'value': project },
325                        ]
326                    }]
327        else:
328            self.log.warn("Cannot export SMB w/o user and project")
329            return [ ]
330
331    def export_seer(self, id, state, project, user, attrs):
332        return [{ 
333                'id': id,
334                'name': 'seer',
335                'visibility': 'export',
336                'server': 'http://control:16606',
337                }]
338
339    def export_local_seer(self, id, state, project, user, attrs):
340        return [{ 
341                'id': id,
342                'name': 'local_seer_control',
343                'visibility': 'export',
344                'server': 'http://control:16606',
345                }]
346
347    def export_seer_master(self, id, state, project, user, attrs):
348        return [{ 
349                'id': id,
350                'name': 'seer_master',
351                'visibility': 'export',
352                'server': 'http://seer-master:17707',
353                }]
354
355    def export_tmcd(self, id, state, project, user, attrs):
356        return [{ 
357                'id': id,
358                'name': 'seer',
359                'visibility': 'export',
360                'server': 'http://boss:7777',
361                }]
362
363    def export_userconfig(self, id, state, project, user, attrs):
364        if self.userconfdir and self.userconfcmd \
365                and self.userconfurl:
366            cid, cert = self.export_userconf(project)
367            state['userconfig'] = unicode(cid)
368            return [{
369                    'id': id,
370                    'name': 'userconfig',
371                    'visibility': 'export',
372                    'server': "%s/%s" % (self.userconfurl, str(cid)),
373                    'fedAttr': [
374                        { 'attribute': 'cert', 'value': cert },
375                    ]
376                    }]
377        else:
378            return [ ]
379
380    def export_hide_hosts(self, id, state, project, user, attrs):
381        return [{
382                'id': id, 
383                'name': 'hide_hosts',
384                'visibility': 'export',
385                'fedAttr': [ x for x in attrs \
386                        if x.get('attribute', "") == 'hosts'],
387                }]
388
389    def export_project_export(self, id, state, project, user, attrs):
390        rv = [ ]
391        rv.extend(self.export_SMB(id, state, project, user, attrs))
392        rv.extend(self.export_userconfig(id, state, project, user, attrs))
393        return rv
394
395    def export_services(self, sreq, project=None, user=None):
396        exp = [ ]
397        state = { }
398        for s in sreq:
399            sname = s.get('name', '')
400            svis = s.get('visibility', '')
401            sattrs = s.get('fedAttr', [])
402            if svis == 'export':
403                if sname in self.exports:
404                    id = s.get('id', 'no_id')
405                    exp.extend(self.exports[sname](id, state, project, user,
406                            sattrs))
407
408        return (exp, state)
409
410    def build_access_response(self, alloc_id, ap, services):
411        """
412        Create the SOAP response.
413
414        Build the dictionary description of the response and use
415        fedd_utils.pack_soap to create the soap message.  ap is the allocate
416        project message returned from a remote project allocation (even if that
417        allocation was done locally).
418        """
419        # Because alloc_id is already a fedd_services_types.IDType_Holder,
420        # there's no need to repack it
421        msg = { 
422                'allocID': alloc_id,
423                'fedAttr': [
424                    { 'attribute': 'domain', 'value': self.domain } , 
425                    { 'attribute': 'project', 'value': 
426                        ap['project'].get('name', {}).get('localname', "???") },
427                ]
428            }
429
430        if self.dragon_endpoint:
431            msg['fedAttr'].append({'attribute': 'dragon',
432                'value': self.dragon_endpoint})
433        if self.deter_internal:
434            msg['fedAttr'].append({'attribute': 'deter_internal',
435                'value': self.deter_internal})
436        #XXX: ??
437        if self.dragon_vlans:
438            msg['fedAttr'].append({'attribute': 'vlans',
439                'value': self.dragon_vlans})
440
441        if services:
442            msg['service'] = services
443        return msg
444
445    def generate_portal_configs(self, topo, pubkey_base, secretkey_base, 
446            tmpdir, lproj, leid, connInfo, services):
447
448        def conninfo_to_dict(key, info):
449            """
450            Make a cpoy of the connection information about key, and flatten it
451            into a single dict by parsing out any feddAttrs.
452            """
453
454            rv = None
455            for i in info:
456                if key == i.get('portal', "") or \
457                        key in [e.get('element', "") \
458                        for e in i.get('member', [])]:
459                    rv = i.copy()
460                    break
461
462            else:
463                return rv
464
465            if 'fedAttr' in rv:
466                for a in rv['fedAttr']:
467                    attr = a.get('attribute', "")
468                    val = a.get('value', "")
469                    if attr and attr not in rv:
470                        rv[attr] = val
471                del rv['fedAttr']
472            return rv
473
474        # XXX: un hardcode this
475        def client_null(f, s):
476            print >>f, "Service: %s" % s['name']
477
478        def client_seer_master(f, s):
479            print >>f, 'PortalAlias: seer-master'
480
481        def client_smb(f, s):
482            print >>f, "Service: %s" % s['name']
483            smbshare = None
484            smbuser = None
485            smbproj = None
486            for a in s.get('fedAttr', []):
487                if a.get('attribute', '') == 'SMBSHARE':
488                    smbshare = a.get('value', None)
489                elif a.get('attribute', '') == 'SMBUSER':
490                    smbuser = a.get('value', None)
491                elif a.get('attribute', '') == 'SMBPROJ':
492                    smbproj = a.get('value', None)
493
494            if all((smbshare, smbuser, smbproj)):
495                print >>f, "SMBshare: %s" % smbshare
496                print >>f, "ProjectUser: %s" % smbuser
497                print >>f, "ProjectName: %s" % smbproj
498
499        def client_hide_hosts(f, s):
500            for a in s.get('fedAttr', [ ]):
501                if a.get('attribute', "") == 'hosts':
502                    print >>f, "Hide: %s" % a.get('value', "")
503
504        client_service_out = {
505                'SMB': client_smb,
506                'tmcd': client_null,
507                'seer': client_null,
508                'userconfig': client_null,
509                'project_export': client_null,
510                'seer_master': client_seer_master,
511                'hide_hosts': client_hide_hosts,
512            }
513
514        def client_seer_master_export(f, s):
515            print >>f, "AddedNode: seer-master"
516
517        def client_seer_local_export(f, s):
518            print >>f, "AddedNode: control"
519
520        client_export_service_out = {
521                'seer_master': client_seer_master_export,
522                'local_seer_control': client_seer_local_export,
523            }
524
525        def server_port(f, s):
526            p = urlparse(s.get('server', 'http://localhost'))
527            print >>f, 'port: remote:%s:%s:%s' % (p.port, p.hostname, p.port) 
528
529        def server_null(f,s): pass
530
531        def server_seer(f, s):
532            print >>f, 'seer: True'
533
534        server_service_out = {
535                'SMB': server_port,
536                'tmcd': server_port,
537                'userconfig': server_null,
538                'project_export': server_null,
539                'seer': server_seer,
540                'seer_master': server_port,
541                'hide_hosts': server_null,
542            }
543        # XXX: end un hardcode this
544
545
546        seer_out = False
547        client_out = False
548        mproj = None
549        mexp = None
550        control_gw = None
551        testbed = ""
552        # Create configuration files for the portals
553        for e in [ e for e in topo.elements \
554                if isinstance(e, topdl.Computer) and e.get_attribute('portal')]:
555            myname = e.name
556            type = e.get_attribute('portal_type')
557
558            info = conninfo_to_dict(myname, connInfo)
559
560            if not info:
561                raise service_error(service_error.req,
562                        "No connectivity info for %s" % myname)
563
564            peer = info.get('peer', "")
565            ldomain = self.domain
566            ssh_port = info.get('ssh_port', 22)
567
568            # Collect this for the client.conf file
569            if 'masterexperiment' in info:
570                mproj, meid = info['masterexperiment'].split("/", 1)
571
572            if type in ('control', 'both'):
573                testbed = e.get_attribute('testbed')
574                control_gw = myname
575
576            active = info.get('active', 'False')
577
578            cfn = "%s/%s.gw.conf" % (tmpdir, myname.lower())
579            tunnelconfig = self.tunnel_config
580            try:
581                f = open(cfn, "w")
582                if active == 'True':
583                    print >>f, "active: True"
584                    print >>f, "ssh_port: %s" % ssh_port
585                    if type in ('control', 'both'):
586                        for s in [s for s in services \
587                                if s.get('name', "") in self.imports]:
588                            server_service_out[s['name']](f, s)
589
590                if tunnelconfig:
591                    print >>f, "tunnelip: %s" % tunnelconfig
592                print >>f, "peer: %s" % peer.lower()
593                print >>f, "ssh_pubkey: /proj/%s/exp/%s/tmp/%s" % \
594                        (lproj, leid, pubkey_base)
595                print >>f, "ssh_privkey: /proj/%s/exp/%s/tmp/%s" % \
596                        (lproj, leid, secretkey_base)
597                f.close()
598            except EnvironmentError, e:
599                raise service_error(service_error.internal,
600                        "Can't write protal config %s: %s" % (cfn, e))
601
602        # Done with portals, write the client config file.
603        try:
604            f = open("%s/client.conf" % tmpdir, "w")
605            if control_gw:
606                print >>f, "ControlGateway: %s.%s.%s%s" % \
607                    (myname.lower(), leid.lower(), lproj.lower(),
608                            ldomain.lower())
609            for s in services:
610                if s.get('name',"") in self.imports and \
611                        s.get('visibility','') == 'import':
612                    client_service_out[s['name']](f, s)
613                if s.get('name', '') in self.exports and \
614                        s.get('visibility', '') == 'export' and \
615                        s['name'] in client_export_service_out:
616                    client_export_service_out[s['name']](f, s)
617            # Seer uses this.
618            if mproj and meid:
619                print >>f, "ExperimentID: %s/%s" % (mproj, meid)
620            f.close()
621        except EnvironmentError, e:
622            raise service_error(service_error.internal,
623                    "Cannot write client.conf: %s" %s)
624
625    def configure_userconf(self, services, tmpdir):
626        """
627        If the userconf service was imported, collect the configuration data.
628        """
629        for s in services:
630            s_name = s.get('name', '')
631            s_vis = s.get('visibility','')
632            if s_name  == 'userconfig' and s_vis == 'import':
633                # Collect ther server and certificate info.
634                u = s.get('server', None)
635                for a in s.get('fedAttr', []):
636                    if a.get('attribute',"") == 'cert':
637                        cert = a.get('value', None)
638                        break
639                else:
640                    cert = None
641
642                if cert:
643                    # Make a temporary certificate file for get_url.  The
644                    # finally clause removes it whether something goes
645                    # wrong (including an exception from get_url) or not.
646                    try:
647                        tfos, tn = tempfile.mkstemp(suffix=".pem")
648                        tf = os.fdopen(tfos, 'w')
649                        print >>tf, cert
650                        tf.close()
651                        self.log.debug("Getting userconf info: %s" % u)
652                        get_url(u, tn, tmpdir, "userconf")
653                        self.log.debug("Got userconf info: %s" % u)
654                    except EnvironmentError, e:
655                        raise service_error(service.error.internal, 
656                                "Cannot create temp file for " + 
657                                "userconfig certificates: %s" % e)
658                    except:
659                        t, v, st = sys.exc_info()
660                        raise service_error(service_error.internal,
661                                "Error retrieving %s: %s" % (u, v))
662                    finally:
663                        if tn: os.remove(tn)
664                else:
665                    raise service_error(service_error.req,
666                            "No certificate for retreiving userconfig")
667                break
668
669    def import_store_info(self, cf, connInfo):
670        """
671        Pull any import parameters in connInfo in.  We translate them either
672        into known member names or fedAddrs.
673        """
674
675        for c in connInfo:
676            for p in [ p for p in c.get('parameter', []) \
677                    if p.get('type', '') == 'input']:
678                name = p.get('name', None)
679                key = p.get('key', None)
680                store = p.get('store', None)
681
682                if name and key and store :
683                    req = { 'name': key, 'wait': True }
684                    self.log.debug("Waiting for %s (%s) from %s" % \
685                            (name, key, store))
686                    r = self.call_GetValue(store, req, cf)
687                    r = r.get('GetValueResponseBody', None)
688                    if r :
689                        if r.get('name', '') == key:
690                            v = r.get('value', None)
691                            if v is not None:
692                                if name == 'peer':
693                                    self.log.debug("Got peer %s" % v)
694                                    c['peer'] = v
695                                else:
696                                    self.log.debug("Got %s %s" % (name, v))
697                                    if c.has_key('fedAttr'):
698                                        c['fedAttr'].append({
699                                            'attribute': name, 'value': v})
700                                    else:
701                                        c['fedAttr']= [{
702                                            'attribute': name, 'value': v}]
703                            else:
704                                raise service_error(service_error.internal, 
705                                        'None value exported for %s'  % key)
706                        else:
707                            raise service_error(service_error.internal, 
708                                    'Different name returned for %s: %s' \
709                                            % (key, r.get('name','')))
710                    else:
711                        raise service_error(service_error.internal, 
712                            'Badly formatted response: no GetValueResponseBody')
713                else:
714                    raise service_error(service_error.internal, 
715                        'Bad Services missing info for import %s' % c)
716
717    def remove_dirs(self, dir):
718        """
719        Remove the directory tree and all files rooted at dir.  Log any errors,
720        but continue.
721        """
722        self.log.debug("[removedirs]: removing %s" % dir)
723        try:
724            for path, dirs, files in os.walk(dir, topdown=False):
725                for f in files:
726                    os.remove(os.path.join(path, f))
727                for d in dirs:
728                    os.rmdir(os.path.join(path, d))
729            os.rmdir(dir)
730        except EnvironmentError, e:
731            self.log.error("Error deleting directory tree in %s" % e);
Note: See TracBrowser for help on using the repository browser.