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

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

Support for priorities and export projects

  • Property mode set to 100644
File size: 19.7 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):
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    def write_state(self):
192        if self.state_filename:
193            try:
194                f = open(self.state_filename, 'w')
195                pickle.dump(self.state, f)
196                self.log.debug("Wrote state to %s" % self.state_filename)
197            except EnvironmentError, e:
198                self.log.error("Can't write file %s: %s" % \
199                        (self.state_filename, e))
200            except pickle.PicklingError, e:
201                self.log.error("Pickling problem: %s" % e)
202            except TypeError, e:
203                self.log.error("Pickling problem (TypeError): %s" % e)
204
205
206    def read_state(self):
207        """
208        Read a new copy of access state.  Old state is overwritten.
209
210        State format is a simple pickling of the state dictionary.
211        """
212        if self.state_filename:
213            try:
214                f = open(self.state_filename, "r")
215                self.state = pickle.load(f)
216                self.log.debug("[read_state]: Read state from %s" % \
217                        self.state_filename)
218            except EnvironmentError, e:
219                self.log.warning(("[read_state]: No saved state: " +\
220                        "Can't open %s: %s") % (self.state_filename, e))
221            except EOFError, e:
222                self.log.warning(("[read_state]: " +\
223                        "Empty or damaged state file: %s:") % \
224                        self.state_filename)
225            except pickle.UnpicklingError, e:
226                self.log.warning(("[read_state]: No saved state: " + \
227                        "Unpickling failed: %s") % e)
228
229
230    def get_handler(self, path, fid):
231        """
232        This function is somewhat oddly named.  It doesn't get a handler, it
233        handles GETs.  Specifically, it handls https GETs for retrieving data
234        from the repository exported by the access server.
235        """
236        self.log.info("Get handler %s %s" % (path, fid))
237        if self.auth.check_attribute(fid, path) and self.userconfdir:
238            return ("%s/%s" % (self.userconfdir, path), "application/binary")
239        else:
240            return (None, None)
241
242    def export_userconf(self, project):
243        dev_null = None
244        confid, confcert = generate_fedid("test", dir=self.userconfdir, 
245                log=self.log)
246        conffilename = "%s/%s" % (self.userconfdir, str(confid))
247        cf = None
248        try:
249            cf = open(conffilename, "w")
250            os.chmod(conffilename, stat.S_IRUSR | stat.S_IWUSR)
251        except EnvironmentError, e:
252            raise service_error(service_error.internal, 
253                    "Cannot create user configuration data")
254
255        try:
256            dev_null = open("/dev/null", "a")
257        except EnvironmentError, e:
258            self.log.error("export_userconf: can't open /dev/null: %s" % e)
259
260        cmd = "%s %s" % (self.userconfcmd, project)
261        conf = subprocess.call(cmd.split(" "),
262                stdout=cf, stderr=dev_null, close_fds=True)
263
264        self.auth.set_attribute(confid, "/%s" % str(confid))
265
266        return confid, confcert
267
268    def export_SMB(self, id, state, project, user, attrs):
269        if project and user:
270            return [{ 
271                    'id': id,
272                    'name': 'SMB',
273                    'visibility': 'export',
274                    'server': 'http://fs:139',
275                    'fedAttr': [
276                            { 'attribute': 'SMBSHARE', 'value': 'USERS' },
277                            { 'attribute': 'SMBUSER', 'value': user },
278                            { 'attribute': 'SMBPROJ', 'value': project },
279                        ]
280                    }]
281        else:
282            self.log.warn("Cannot export SMB w/o user and project")
283            return [ ]
284
285    def export_seer(self, id, state, project, user, attrs):
286        return [{ 
287                'id': id,
288                'name': 'seer',
289                'visibility': 'export',
290                'server': 'http://control:16606',
291                }]
292
293    def export_local_seer(self, id, state, project, user, attrs):
294        return [{ 
295                'id': id,
296                'name': 'local_seer_control',
297                'visibility': 'export',
298                'server': 'http://control:16606',
299                }]
300
301    def export_seer_master(self, id, state, project, user, attrs):
302        return [{ 
303                'id': id,
304                'name': 'seer_master',
305                'visibility': 'export',
306                'server': 'http://seer-master:17707',
307                }]
308
309    def export_tmcd(self, id, state, project, user, attrs):
310        return [{ 
311                'id': id,
312                'name': 'seer',
313                'visibility': 'export',
314                'server': 'http://boss:7777',
315                }]
316
317    def export_userconfig(self, id, state, project, user, attrs):
318        if self.userconfdir and self.userconfcmd \
319                and self.userconfurl:
320            cid, cert = self.export_userconf(project)
321            state['userconfig'] = unicode(cid)
322            return [{
323                    'id': id,
324                    'name': 'userconfig',
325                    'visibility': 'export',
326                    'server': "%s/%s" % (self.userconfurl, str(cid)),
327                    'fedAttr': [
328                        { 'attribute': 'cert', 'value': cert },
329                    ]
330                    }]
331        else:
332            return [ ]
333
334    def export_hide_hosts(self, id, state, project, user, attrs):
335        return [{
336                'id': id, 
337                'name': 'hide_hosts',
338                'visibility': 'export',
339                'fedAttr': [ x for x in attrs \
340                        if x.get('attribute', "") == 'hosts'],
341                }]
342
343    def export_project_export(self, id, state, project, user, attrs):
344        rv = [ ]
345        rv.extend(self.export_SMB(id, state, project, user, attrs))
346        rv.extend(self.export_userconfig(id, state, project, user, attrs))
347        return rv
348
349    def export_services(self, sreq, project=None, user=None):
350        exp = [ ]
351        state = { }
352        for s in sreq:
353            sname = s.get('name', '')
354            svis = s.get('visibility', '')
355            sattrs = s.get('fedAttr', [])
356            if svis == 'export':
357                if sname in self.exports:
358                    id = s.get('id', 'no_id')
359                    exp.extend(self.exports[sname](id, state, project, user,
360                            sattrs))
361
362        return (exp, state)
363
364    def build_access_response(self, alloc_id, ap, services):
365        """
366        Create the SOAP response.
367
368        Build the dictionary description of the response and use
369        fedd_utils.pack_soap to create the soap message.  ap is the allocate
370        project message returned from a remote project allocation (even if that
371        allocation was done locally).
372        """
373        # Because alloc_id is already a fedd_services_types.IDType_Holder,
374        # there's no need to repack it
375        msg = { 
376                'allocID': alloc_id,
377                'fedAttr': [
378                    { 'attribute': 'domain', 'value': self.domain } , 
379                    { 'attribute': 'project', 'value': 
380                        ap['project'].get('name', {}).get('localname', "???") },
381                ]
382            }
383
384        if self.dragon_endpoint:
385            msg['fedAttr'].append({'attribute': 'dragon',
386                'value': self.dragon_endpoint})
387        if self.deter_internal:
388            msg['fedAttr'].append({'attribute': 'deter_internal',
389                'value': self.deter_internal})
390        #XXX: ??
391        if self.dragon_vlans:
392            msg['fedAttr'].append({'attribute': 'vlans',
393                'value': self.dragon_vlans})
394
395        if services:
396            msg['service'] = services
397        return msg
398
399    def generate_portal_configs(self, topo, pubkey_base, secretkey_base, 
400            tmpdir, lproj, leid, connInfo, services):
401
402        def conninfo_to_dict(key, info):
403            """
404            Make a cpoy of the connection information about key, and flatten it
405            into a single dict by parsing out any feddAttrs.
406            """
407
408            rv = None
409            for i in info:
410                if key == i.get('portal', "") or \
411                        key in [e.get('element', "") \
412                        for e in i.get('member', [])]:
413                    rv = i.copy()
414                    break
415
416            else:
417                return rv
418
419            if 'fedAttr' in rv:
420                for a in rv['fedAttr']:
421                    attr = a.get('attribute', "")
422                    val = a.get('value', "")
423                    if attr and attr not in rv:
424                        rv[attr] = val
425                del rv['fedAttr']
426            return rv
427
428        # XXX: un hardcode this
429        def client_null(f, s):
430            print >>f, "Service: %s" % s['name']
431
432        def client_seer_master(f, s):
433            print >>f, 'PortalAlias: seer-master'
434
435        def client_smb(f, s):
436            print >>f, "Service: %s" % s['name']
437            smbshare = None
438            smbuser = None
439            smbproj = None
440            for a in s.get('fedAttr', []):
441                if a.get('attribute', '') == 'SMBSHARE':
442                    smbshare = a.get('value', None)
443                elif a.get('attribute', '') == 'SMBUSER':
444                    smbuser = a.get('value', None)
445                elif a.get('attribute', '') == 'SMBPROJ':
446                    smbproj = a.get('value', None)
447
448            if all((smbshare, smbuser, smbproj)):
449                print >>f, "SMBshare: %s" % smbshare
450                print >>f, "ProjectUser: %s" % smbuser
451                print >>f, "ProjectName: %s" % smbproj
452
453        def client_hide_hosts(f, s):
454            for a in s.get('fedAttr', [ ]):
455                if a.get('attribute', "") == 'hosts':
456                    print >>f, "Hide: %s" % a.get('value', "")
457
458        client_service_out = {
459                'SMB': client_smb,
460                'tmcd': client_null,
461                'seer': client_null,
462                'userconfig': client_null,
463                'project_export': client_null,
464                'seer_master': client_seer_master,
465                'hide_hosts': client_hide_hosts,
466            }
467
468        def client_seer_master_export(f, s):
469            print >>f, "AddedNode: seer-master"
470
471        def client_seer_local_export(f, s):
472            print >>f, "AddedNode: control"
473
474        client_export_service_out = {
475                'seer_master': client_seer_master_export,
476                'local_seer_control': client_seer_local_export,
477            }
478
479        def server_port(f, s):
480            p = urlparse(s.get('server', 'http://localhost'))
481            print >>f, 'port: remote:%s:%s:%s' % (p.port, p.hostname, p.port) 
482
483        def server_null(f,s): pass
484
485        def server_seer(f, s):
486            print >>f, 'seer: True'
487
488        server_service_out = {
489                'SMB': server_port,
490                'tmcd': server_port,
491                'userconfig': server_null,
492                'project_export': server_null,
493                'seer': server_seer,
494                'seer_master': server_port,
495                'hide_hosts': server_null,
496            }
497        # XXX: end un hardcode this
498
499
500        seer_out = False
501        client_out = False
502        mproj = None
503        mexp = None
504        control_gw = None
505        testbed = ""
506        # Create configuration files for the portals
507        for e in [ e for e in topo.elements \
508                if isinstance(e, topdl.Computer) and e.get_attribute('portal')]:
509            myname = e.name
510            type = e.get_attribute('portal_type')
511
512            info = conninfo_to_dict(myname, connInfo)
513
514            if not info:
515                raise service_error(service_error.req,
516                        "No connectivity info for %s" % myname)
517
518            peer = info.get('peer', "")
519            ldomain = self.domain
520            ssh_port = info.get('ssh_port', 22)
521
522            # Collect this for the client.conf file
523            if 'masterexperiment' in info:
524                mproj, meid = info['masterexperiment'].split("/", 1)
525
526            if type in ('control', 'both'):
527                testbed = e.get_attribute('testbed')
528                control_gw = myname
529
530            active = info.get('active', 'False')
531
532            cfn = "%s/%s.gw.conf" % (tmpdir, myname.lower())
533            tunnelconfig = self.tunnel_config
534            try:
535                f = open(cfn, "w")
536                if active == 'True':
537                    print >>f, "active: True"
538                    print >>f, "ssh_port: %s" % ssh_port
539                    if type in ('control', 'both'):
540                        for s in [s for s in services \
541                                if s.get('name', "") in self.imports]:
542                            server_service_out[s['name']](f, s)
543
544                if tunnelconfig:
545                    print >>f, "tunnelip: %s" % tunnelconfig
546                print >>f, "peer: %s" % peer.lower()
547                print >>f, "ssh_pubkey: /proj/%s/exp/%s/tmp/%s" % \
548                        (lproj, leid, pubkey_base)
549                print >>f, "ssh_privkey: /proj/%s/exp/%s/tmp/%s" % \
550                        (lproj, leid, secretkey_base)
551                f.close()
552            except EnvironmentError, e:
553                raise service_error(service_error.internal,
554                        "Can't write protal config %s: %s" % (cfn, e))
555
556        # Done with portals, write the client config file.
557        try:
558            f = open("%s/client.conf" % tmpdir, "w")
559            if control_gw:
560                print >>f, "ControlGateway: %s.%s.%s%s" % \
561                    (myname.lower(), leid.lower(), lproj.lower(),
562                            ldomain.lower())
563            for s in services:
564                if s.get('name',"") in self.imports and \
565                        s.get('visibility','') == 'import':
566                    client_service_out[s['name']](f, s)
567                if s.get('name', '') in self.exports and \
568                        s.get('visibility', '') == 'export' and \
569                        s['name'] in client_export_service_out:
570                    client_export_service_out[s['name']](f, s)
571            # Seer uses this.
572            if mproj and meid:
573                print >>f, "ExperimentID: %s/%s" % (mproj, meid)
574            f.close()
575        except EnvironmentError, e:
576            raise service_error(service_error.internal,
577                    "Cannot write client.conf: %s" %s)
578
579    def configure_userconf(self, services, tmpdir):
580        """
581        If the userconf service was imported, collect the configuration data.
582        """
583        for s in services:
584            s_name = s.get('name', '')
585            s_vis = s.get('visibility','')
586            if s_name  == 'userconfig' and s_vis == 'import':
587                # Collect ther server and certificate info.
588                u = s.get('server', None)
589                for a in s.get('fedAttr', []):
590                    if a.get('attribute',"") == 'cert':
591                        cert = a.get('value', None)
592                        break
593                else:
594                    cert = None
595
596                if cert:
597                    # Make a temporary certificate file for get_url.  The
598                    # finally clause removes it whether something goes
599                    # wrong (including an exception from get_url) or not.
600                    try:
601                        tfos, tn = tempfile.mkstemp(suffix=".pem")
602                        tf = os.fdopen(tfos, 'w')
603                        print >>tf, cert
604                        tf.close()
605                        self.log.debug("Getting userconf info: %s" % u)
606                        get_url(u, tn, tmpdir, "userconf")
607                        self.log.debug("Got userconf info: %s" % u)
608                    except EnvironmentError, e:
609                        raise service_error(service.error.internal, 
610                                "Cannot create temp file for " + 
611                                "userconfig certificates: %s" % e)
612                    except:
613                        t, v, st = sys.exc_info()
614                        raise service_error(service_error.internal,
615                                "Error retrieving %s: %s" % (u, v))
616                    finally:
617                        if tn: os.remove(tn)
618                else:
619                    raise service_error(service_error.req,
620                            "No certificate for retreiving userconfig")
621                break
622
623    def import_store_info(self, cf, connInfo):
624        """
625        Pull any import parameters in connInfo in.  We translate them either
626        into known member names or fedAddrs.
627        """
628
629        for c in connInfo:
630            for p in [ p for p in c.get('parameter', []) \
631                    if p.get('type', '') == 'input']:
632                name = p.get('name', None)
633                key = p.get('key', None)
634                store = p.get('store', None)
635
636                if name and key and store :
637                    req = { 'name': key, 'wait': True }
638                    self.log.debug("Waiting for %s (%s) from %s" % \
639                            (name, key, store))
640                    r = self.call_GetValue(store, req, cf)
641                    r = r.get('GetValueResponseBody', None)
642                    if r :
643                        if r.get('name', '') == key:
644                            v = r.get('value', None)
645                            if v is not None:
646                                if name == 'peer':
647                                    self.log.debug("Got peer %s" % v)
648                                    c['peer'] = v
649                                else:
650                                    self.log.debug("Got %s %s" % (name, v))
651                                    if c.has_key('fedAttr'):
652                                        c['fedAttr'].append({
653                                            'attribute': name, 'value': v})
654                                    else:
655                                        c['fedAttr']= [{
656                                            'attribute': name, 'value': v}]
657                            else:
658                                raise service_error(service_error.internal, 
659                                        'None value exported for %s'  % key)
660                        else:
661                            raise service_error(service_error.internal, 
662                                    'Different name returned for %s: %s' \
663                                            % (key, r.get('name','')))
664                    else:
665                        raise service_error(service_error.internal, 
666                            'Badly formatted response: no GetValueResponseBody')
667                else:
668                    raise service_error(service_error.internal, 
669                        'Bad Services missing info for import %s' % c)
670
671    def remove_dirs(self, dir):
672        """
673        Remove the directory tree and all files rooted at dir.  Log any errors,
674        but continue.
675        """
676        self.log.debug("[removedirs]: removing %s" % dir)
677        try:
678            for path, dirs, files in os.walk(dir, topdown=False):
679                for f in files:
680                    os.remove(os.path.join(path, f))
681                for d in dirs:
682                    os.rmdir(os.path.join(path, d))
683            os.rmdir(dir)
684        except EnvironmentError, e:
685            self.log.error("Error deleting directory tree in %s" % e);
Note: See TracBrowser for help on using the repository browser.