source: fedd/federation/access.py @ c002cb2

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

Structure for priority and filtering of ABAC attributes at access check time

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