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

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

Looks like internal works now.

Had to add default entries to the access list to accomodate that, and discovered that ABAC requires strings - not unicode.

Moved lookup_access into the aceess class as most should be able to use it directly now.

  • Property mode set to 100644
File size: 12.0 KB
Line 
1#!/usr/local/bin/python
2
3import os,sys
4import re
5import string
6import copy
7import pickle
8import logging
9import time
10
11from threading import Thread, Lock
12from subprocess import Popen, call, PIPE, STDOUT
13
14from util import *
15from allocate_project import allocate_project_local, allocate_project_remote
16from fedid import fedid, generate_fedid
17from authorizer import authorizer, abac_authorizer
18from service_error import service_error
19from remote_service import xmlrpc_handler, soap_handler, service_caller
20
21import httplib
22import tempfile
23from urlparse import urlparse
24
25import topdl
26import list_log
27import proxy_emulab_segment
28import local_emulab_segment
29
30from access import access_base
31from legacy_access import legacy_access
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(access_base, legacy_access):
41    @staticmethod
42    def parse_vlans(v, log=None):
43        """
44        Parse a vlan parameter into a set of vlan ids.  Comma separated
45        sequences of vlan ranges are acceptable.
46        """
47        # the vlans can be a single integer, a comma separated list or a
48        # comma separated lists of dashed ranges.  E.g 100 or 100,300 or
49        # 100,300-305,400
50        vset = set()
51        if v:
52            for v in [ x.strip() for x in v.split(",") ]:
53                try:
54                    if v.count("-") == 1:
55                        f, t = v.split("-", 1)
56                        for i in range(int(f), int(t)+1):
57                            vset.add(i)
58                    else:
59                        vset.add(int(v))
60                except ValueError:
61                    if log:
62                        log.warn("Invalid expression in vlan list: %s" % v)
63        return vset
64
65    def __init__(self, config=None, auth=None):
66        """
67        Initializer.  Pulls parameters out of the ConfigParser's access section.
68        """
69
70        access_base.__init__(self, config, auth)
71        self.domain = config.get("access", "domain")
72        vlan_str = config.get("access", "vlans")
73        self.vlans = self.parse_vlans(vlan_str)
74
75        self.attrs = { }
76        self.access = { }
77        # State is a dict of dicts indexed by segment fedid that includes the
78        # owners of the segment as fedids (who can manipulate it, key: owners),
79        # the repo dir/user for the allocation (key: user),  Current allocation
80        # log (key: log), and GRI of the reservation once made (key: gri)
81        self.log = logging.getLogger("fedd.access")
82        set_log_level(config, "access", self.log)
83
84
85        # authorization information
86        self.auth_type = config.get('access', 'auth_type') \
87                or 'legacy'
88        self.auth_dir = config.get('access', 'auth_dir')
89        accessdb = config.get("access", "accessdb")
90        # initialize the authorization system
91        if self.auth_type == 'legacy':
92            self.access = { }
93            if accessdb:
94                self.legacy_read_access(accessdb)
95        elif self.auth_type == 'abac':
96            self.auth = abac_authorizer(load=self.auth_dir)
97            self.access = [ ]
98            if accessdb:
99                self.read_access(accessdb, default=[('access', None)])
100        else:
101            raise service_error(service_error.internal, 
102                    "Unknown auth_type: %s" % self.auth_type)
103
104        if self.auth_type == 'legacy':
105            # Add the ownership attributes to the authorizer.  Note that the
106            # indices of the allocation dict are strings, but the attributes are
107            # fedids, so there is a conversion.
108            self.state_lock.acquire()
109            for k in self.state.keys():
110                for o in self.state[k].get('owners', []):
111                    self.auth.set_attribute(o, fedid(hexstr=k))
112                self.auth.set_attribute(fedid(hexstr=k),fedid(hexstr=k))
113                # If the allocation has a vlan assigned, remove it from the
114                # available vlans
115                v = self.state[k].get('vlan', None)
116                if v:
117                    self.vlans.discard(v)
118            self.state_lock.release()
119
120            self.lookup_access = self.legacy_lookup_access_base
121        # under ABAC we use access.lookup_access
122
123
124        self.call_GetValue= service_caller('GetValue')
125        self.call_SetValue= service_caller('SetValue')
126
127        self.soap_services = {\
128            'RequestAccess': soap_handler("RequestAccess", self.RequestAccess),
129            'ReleaseAccess': soap_handler("ReleaseAccess", self.ReleaseAccess),
130            'StartSegment': soap_handler("StartSegment", self.StartSegment),
131            'TerminateSegment': soap_handler("TerminateSegment", 
132                self.TerminateSegment),
133            }
134        self.xmlrpc_services =  {\
135            'RequestAccess': xmlrpc_handler('RequestAccess',
136                self.RequestAccess),
137            'ReleaseAccess': xmlrpc_handler('ReleaseAccess',
138                self.ReleaseAccess),
139            'StartSegment': xmlrpc_handler("StartSegment", self.StartSegment),
140            'TerminateSegment': xmlrpc_handler('TerminateSegment',
141                self.TerminateSegment),
142            }
143
144    def RequestAccess(self, req, fid):
145        """
146        Handle the access request.  Proxy if not for us.
147
148        Parse out the fields and make the allocations or rejections if for us,
149        otherwise, assuming we're willing to proxy, proxy the request out.
150        """
151
152        # The dance to get into the request body
153        if req.has_key('RequestAccessRequestBody'):
154            req = req['RequestAccessRequestBody']
155        else:
156            raise service_error(service_error.req, "No request!?")
157
158        found, match, owners = self.lookup_access(req, fid)
159        # keep track of what's been added
160        allocID, alloc_cert = generate_fedid(subj="alloc", log=self.log)
161        aid = unicode(allocID)
162
163        self.state_lock.acquire()
164        self.state[aid] = { }
165        self.state[aid]['user'] = found
166        self.state[aid]['owners'] = owners
167        self.state[aid]['vlan'] = None
168        self.write_state()
169        self.state_lock.release()
170        self.auth.set_attribute(fid, allocID)
171        self.auth.set_attribute(allocID, allocID)
172        self.auth.save()
173
174        try:
175            f = open("%s/%s.pem" % (self.certdir, aid), "w")
176            print >>f, alloc_cert
177            f.close()
178        except EnvironmentError, e:
179            raise service_error(service_error.internal, 
180                    "Can't open %s/%s : %s" % (self.certdir, aid, e))
181        return { 'allocID': { 'fedid': allocID } }
182
183    def ReleaseAccess(self, req, fid):
184        # The dance to get into the request body
185        if req.has_key('ReleaseAccessRequestBody'):
186            req = req['ReleaseAccessRequestBody']
187        else:
188            raise service_error(service_error.req, "No request!?")
189
190        # Local request
191        try:
192            if req['allocID'].has_key('localname'):
193                auth_attr = aid = req['allocID']['localname']
194            elif req['allocID'].has_key('fedid'):
195                aid = unicode(req['allocID']['fedid'])
196                auth_attr = req['allocID']['fedid']
197            else:
198                raise service_error(service_error.req,
199                        "Only localnames and fedids are understood")
200        except KeyError:
201            raise service_error(service_error.req, "Badly formed request")
202
203        self.log.debug("[access] deallocation requested for %s", aid)
204        if not self.auth.check_attribute(fid, auth_attr):
205            self.log.debug("[access] deallocation denied for %s", aid)
206            raise service_error(service_error.access, "Access Denied")
207
208        self.state_lock.acquire()
209        if self.state.has_key(aid):
210            self.log.debug("Found allocation for %s" %aid)
211            del self.state[aid]
212            self.write_state()
213            self.state_lock.release()
214            # And remove the access cert
215            cf = "%s/%s.pem" % (self.certdir, aid)
216            self.log.debug("Removing %s" % cf)
217            os.remove(cf)
218            return { 'allocID': req['allocID'] } 
219        else:
220            self.state_lock.release()
221            raise service_error(service_error.req, "No such allocation")
222
223    def extract_parameters(self, top):
224        """
225        DETER's internal networking currently supports a fixed capacity link
226        between two endpoints.  Those endpoints may specify a VPN (or list or
227        range) to use.  This extracts the and vpn preferences from the segments
228        (as attributes) and the capacity of the connection as given by the
229        substrate.  The two endpoints VLAN choices are intersected to get set
230        of VLANs that are acceptable (no VLAN requiremnets means any is
231        acceptable).
232        """
233        segments = filter(lambda x: isinstance(x, topdl.Segment), top.elements)
234
235        if len(segments) != 2 or len(top.substrates) != 1:
236            raise service_error(service_error.req,
237                    "Requests to DRAGON must have exactlty two segments " +\
238                            "and one substrate")
239
240        vlans = set()
241        for s in segments:
242            v = s.get_attribute('vlans')
243            vlans &= self.parse_vlans(v)
244
245        if len(vlans) == 0:
246            vlans = None
247
248        sub = top.substrates[0]
249        if sub.capacity:
250            cap = int(sub.capacity.rate / 1000.0)
251            if cap < 1: cap = 1
252        else:
253            cap = 100
254
255        return cap, vlans
256
257    def export_store_info(self, cf, vlan, connInfo):
258        """
259        For the export requests in the connection info, install the peer names
260        at the experiment controller via SetValue calls.
261        """
262
263        for c in connInfo:
264            for p in [ p for p in c.get('parameter', []) \
265                    if p.get('type', '') == 'output']:
266
267                if p.get('name', '') != 'vlan_id':
268                    self.log.error("Unknown export parameter: %s" % \
269                            p.get('name'))
270                    continue
271
272                k = p.get('key', None)
273                surl = p.get('store', None)
274                if surl and k:
275                    value = "%s" % vlan
276                    req = { 'name': k, 'value': value }
277                    self.call_SetValue(surl, req, cf)
278                else:
279                    self.log.error("Bad export request: %s" % p)
280
281    def StartSegment(self, req, fid):
282        err = None  # Any service_error generated after tmpdir is created
283        rv = None   # Return value from segment creation
284
285        try:
286            req = req['StartSegmentRequestBody']
287            topref = req['segmentdescription']['topdldescription']
288        except KeyError:
289            raise service_error(server_error.req, "Badly formed request")
290
291        auth_attr = req['allocID']['fedid']
292        aid = "%s" % auth_attr
293        attrs = req.get('fedAttr', [])
294        if not self.auth.check_attribute(fid, auth_attr):
295            raise service_error(service_error.access, "Access denied")
296        else:
297            # See if this is a replay of an earlier succeeded StartSegment -
298            # sometimes SSL kills 'em.  If so, replay the response rather than
299            # redoing the allocation.
300            self.state_lock.acquire()
301            retval = self.state[aid].get('started', None)
302            self.state_lock.release()
303            if retval:
304                self.log.warning("Duplicate StartSegment for %s: " % aid + \
305                        "replaying response")
306                return retval
307
308        certfile = "%s/%s.pem" % (self.certdir, aid)
309
310        if topref: topo = topdl.Topology(**topref)
311        else:
312            raise service_error(service_error.req, 
313                    "Request missing segmentdescription'")
314
315        connInfo = req.get('connection', [])
316
317        cap, vlans = self.extract_parameters(topo)
318
319        # No vlans passes in, consider any vlan acceptable
320        if not vlans: 
321            vlans = self.vlans
322
323        avail = self.vlans | vlans
324
325        if len(avail) != 0:
326            vlan_no = avail.pop()
327            self.vlans.discard(vlan_no)
328        else:
329            raise service_error(service_error.federant, "No vlan available")
330
331        self.export_store_info(certfile, vlan_no, connInfo)
332
333
334        # This annotation isn't strictly necessary, but may help in debugging
335        rtopo = topo.clone()
336        for s in rtopo.substrates:
337            s.set_attribute('vlan', vlan_no)
338
339        # Grab the log (this is some anal locking, but better safe than
340        # sorry)
341        self.state_lock.acquire()
342        self.state[aid]['vlan'] = vlan_no
343        logv = "Allocated vlan: %d" % vlan_no
344        # It's possible that the StartSegment call gets retried (!).
345        # if the 'started' key is in the allocation, we'll return it rather
346        # than redo the setup.
347        self.state[aid]['started'] = { 
348                'allocID': req['allocID'],
349                'allocationLog': logv,
350                'segmentdescription': { 'topdldescription': rtopo.to_dict() }
351                }
352        retval = copy.deepcopy(self.state[aid]['started'])
353        self.write_state()
354        self.state_lock.release()
355
356        return retval
357
358    def TerminateSegment(self, req, fid):
359        try:
360            req = req['TerminateSegmentRequestBody']
361        except KeyError:
362            raise service_error(server_error.req, "Badly formed request")
363
364        auth_attr = req['allocID']['fedid']
365        aid = "%s" % auth_attr
366
367        self.log.debug("Terminate request for %s" %aid)
368        if not self.auth.check_attribute(fid, auth_attr):
369            raise service_error(service_error.access, "Access denied")
370
371        self.state_lock.acquire()
372        if self.state.has_key(aid):
373            vlan_no = self.state[aid].get('vlan', None)
374        else:
375            vlan_no = None
376        self.state_lock.release()
377        self.log.debug("Stop segment for vlan: %s" % vlan_no)
378
379        if not vlan_no:
380            raise service_error(service_error.internal, 
381                    "Can't find assigfned vlan for for %s" % aid)
382   
383        self.vlans.add(vlan_no)
384        self.state_lock.acquire()
385        self.state[aid]['vlan'] = None
386        self.state_lock.release()
387        return { 'allocID': req['allocID'] }
Note: See TracBrowser for help on using the repository browser.