source: fedd/federation/access.py @ 78f2668

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

Move some functions from access to legacy_access. Rename functions so abac is the default

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