source: fedd/federation/skeleton_access.py @ c65b7e4

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

Access controllers delete (some) unused ABAC attrs.

  • Property mode set to 100644
File size: 14.0 KB
Line 
1#!/usr/local/bin/python
2
3import os,sys
4import re
5import string
6import copy
7import pickle
8import logging
9import random
10
11from util import *
12from fedid import fedid, generate_fedid
13from authorizer import authorizer, abac_authorizer
14from service_error import service_error
15from remote_service import xmlrpc_handler, soap_handler, service_caller
16
17import topdl
18
19from access import access_base
20from legacy_access import legacy_access
21
22# Make log messages disappear if noone configures a fedd logger.  This is
23# something of an incantation, but basically it creates a logger object
24# registered to fedd.access if no other module above us has.  It's an extra
25# belt for the suspenders.
26class nullHandler(logging.Handler):
27    def emit(self, record): pass
28
29fl = logging.getLogger("fedd.access")
30fl.addHandler(nullHandler())
31
32
33# The plug-in itself.
34class access(access_base, legacy_access):
35    """
36    This is a demonstration plug-in for fedd.  It responds to all the
37    experiment_control requests and keeps internal state.  The allocations it
38    makes are simple integers associated with each valid request.  It makes use
39    of the general routines in access.access_base.
40
41    Detailed comments in the code and info at
42    """
43
44    @staticmethod 
45    def parse_access_string(s):
46        """
47        Parse a parenthesized string from the access db by removing the parens.
48        If the string isn't in parens, we just return it with whitespace
49        trimmed in either case.
50        """
51        st = s.strip()
52        if st.startswith("(") and st.endswith(")"): return st[1:-1]
53        else: return st
54
55    def __init__(self, config=None, auth=None):
56        """
57        Initializer.  Pulls parameters out of the ConfigParser's access
58        section, and initializes simple internal state.  This version reads a
59        maximum integer to assign from the configuration file, while most other
60        configuration entries  are read by the base class. 
61
62        An access database in the cannonical format is also read as well as a
63        state database that is a hash of internal state.  Routines to
64        manipulate these are in the base class, but specializations appear
65        here.
66
67        The access database maps users to a simple string.
68        """
69
70        # Calling the base initializer, which reads canonical configuration
71        # information and initializes canonical members.
72        access_base.__init__(self, config, auth)
73        # Reading the maximum integer parameter from the configuration file
74        self.maxint = config.getint("access", "maxint") or 5
75        # The available integers
76        self.available_ints = set(range(0,self.maxint))
77
78        # authorization information
79        self.auth_type = config.get('access', 'auth_type') \
80                or 'legacy'
81        self.auth_dir = config.get('access', 'auth_dir')
82        accessdb = config.get("access", "accessdb")
83        # initialize the authorization system.  In each case we make a call to
84        # read the access database that maps from authorization information
85        # into local information.  The local information is parsed by the
86        # translator above.
87        if self.auth_type == 'legacy':
88            self.access = { }
89            if accessdb:
90                try:
91                    self.legacy_read_access(accessdb, self.parse_access_string)
92                except EnvironmentError, e:
93                    self.log.error("Cannot read %s: %s" % \
94                            (config.get("access", "accessdb"), e))
95                    raise e
96            # The base class initializer has read the state dictionary from the
97            # state file, if there is one.  The state variable includes
98            # information about each active allocation, keyed by the allocation
99            # identifier.  This loop extracts the owners stored with each
100            # allocation and associates an access attribute with them.  Each
101            # owner is allowed to access each thing they own.  This is a
102            # specialization of the state handling.  ABAC records this
103            # information explicitly so this loop only executes for legacy
104            # code.
105            self.state_lock.acquire()
106            for k in self.state.keys():
107                # Add the owners
108                for o in self.state[k].get('owners', []):
109                    self.auth.set_attribute(o, fedid(hexstr=k))
110                # The principal represented by the allocation itself is also
111                # allowed to make accesses.
112                self.auth.set_attribute(fedid(hexstr=k),fedid(hexstr=k))
113            self.state_lock.release()
114            # This access controller does not specialize the process of looking
115            # up local information.  This aliases the lookup_access method to
116            # be easier to read.
117            self.lookup_access = self.legacy_lookup_access_base
118        elif self.auth_type == 'abac':
119            #  Load the current authorization state
120            self.auth = abac_authorizer(load=self.auth_dir)
121            self.access = [ ]
122            if accessdb:
123                try:
124                    self.read_access(accessdb, self.parse_access_string)
125                except EnvironmentError, e:
126                    self.log.error("Cannot read %s: %s" % \
127                            (config.get("access", "accessdb"), e))
128                    raise e
129        else:
130            raise service_error(service_error.internal, 
131                    "Unknown auth_type: %s" % self.auth_type)
132
133        # Clean the state
134        self.state_lock.acquire()
135        for k in self.state.keys():
136            # Remove any allocated integers from the available ones
137            if 'integer' in self.state[k]:
138                self.available_ints.discard(self.state[k]['integer'])
139        self.state_lock.release()
140
141        # These dictionaries register the plug-in's local routines for handline
142        # these four messages with the server code above.  There's a version
143        # for SOAP and XMLRPC, depending on which interfaces the plugin
144        # supports.  There's rarely a technical reason not to support one or
145        # the other - the plugin code almost never deals with the transport -
146        # but if a plug-in writer wanted to disable XMLRPC, they could leave
147        # the self.xmlrpc_services dictionary empty.
148        self.soap_services = {\
149            'RequestAccess': soap_handler("RequestAccess", self.RequestAccess),
150            'ReleaseAccess': soap_handler("ReleaseAccess", self.ReleaseAccess),
151            'StartSegment': soap_handler("StartSegment", self.StartSegment),
152            'TerminateSegment': soap_handler("TerminateSegment", 
153                self.TerminateSegment),
154            }
155        self.xmlrpc_services =  {\
156            'RequestAccess': xmlrpc_handler('RequestAccess',
157                self.RequestAccess),
158            'ReleaseAccess': xmlrpc_handler('ReleaseAccess',
159                self.ReleaseAccess),
160            'StartSegment': xmlrpc_handler("StartSegment", self.StartSegment),
161            'TerminateSegment': xmlrpc_handler('TerminateSegment',
162                self.TerminateSegment),
163            }
164
165    def RequestAccess(self, req, fid):
166        """
167        Handle an access request.  Success here maps the requester into the
168        local access control space and establishes state about that user keyed
169        to a fedid.  We also save a copy of the certificate underlying that
170        fedid so this allocation can access configuration information and
171        shared parameters on the experiment controller.
172        """
173
174        # The dance to get into the request body
175        if req.has_key('RequestAccessRequestBody'):
176            req = req['RequestAccessRequestBody']
177        else:
178            raise service_error(service_error.req, "No request!?")
179
180        # Base class lookup routine.  If this fails, it throws a service
181        # exception denying access that triggers a fault response back to the
182        # caller.
183        found, match, owners = self.lookup_access(req, fid)
184        self.log.info(
185                "[RequestAccess] Access granted to %s with local creds %s" % \
186                (match, found))
187        # Make a fedid for this allocation
188        allocID, alloc_cert = generate_fedid(subj="alloc", log=self.log)
189        aid = unicode(allocID)
190
191        # Store the data about this allocation:
192        self.state_lock.acquire()
193        self.state[aid] = { }
194        self.state[aid]['user'] = found
195        self.state[aid]['owners'] = owners
196        self.state[aid]['auth'] = set()
197        # Authorize the creating fedid and the principal representing the
198        # allocation to manipulate it.
199        self.append_allocation_authorization(aid, 
200                ((fid, allocID), (allocID, allocID)))
201        self.write_state()
202        self.state_lock.release()
203
204        # Create a directory to stash the certificate in, ans stash it.
205        try:
206            f = open("%s/%s.pem" % (self.certdir, aid), "w")
207            print >>f, alloc_cert
208            f.close()
209        except EnvironmentError, e:
210            raise service_error(service_error.internal, 
211                    "Can't open %s/%s : %s" % (self.certdir, aid, e))
212        self.log.debug('[RequestAccess] Returning allocation ID: %s' % allocID)
213        return { 'allocID': { 'fedid': allocID } }
214
215    def ReleaseAccess(self, req, fid):
216        """
217        Release the allocation granted earlier.  Access to the allocation is
218        checked and if valid, the state and cached certificate are destroyed.
219        """
220        # The dance to get into the request body
221        if req.has_key('ReleaseAccessRequestBody'):
222            req = req['ReleaseAccessRequestBody']
223        else:
224            raise service_error(service_error.req, "No request!?")
225
226        # Pull a key out of the request.  One can request to delete an
227        # allocation by a local human readable name or by a fedid.  This finds
228        # both choices.
229        try:
230            if 'localname' in req['allocID']:
231                auth_attr = aid = req['allocID']['localname']
232            elif 'fedid' in req['allocID']:
233                aid = unicode(req['allocID']['fedid'])
234                auth_attr = req['allocID']['fedid']
235            else:
236                raise service_error(service_error.req,
237                        "Only localnames and fedids are understood")
238        except KeyError:
239            raise service_error(service_error.req, "Badly formed request")
240
241        self.log.debug("[ReleaseAccess] deallocation requested for %s", aid)
242        #  Confirm access
243        if not self.auth.check_attribute(fid, auth_attr):
244            self.log.debug("[ReleaseAccess] deallocation denied for %s", aid)
245            raise service_error(service_error.access, "Access Denied")
246
247        # If there is an allocation in the state, delete it.  Note the locking.
248        self.state_lock.acquire()
249        if aid in self.state:
250            self.log.debug("[ReleaseAccess] Found allocation for %s" %aid)
251            self.clear_allocation_authorization(aid)
252            del self.state[aid]
253            self.write_state()
254            self.state_lock.release()
255            # And remove the access cert
256            cf = "%s/%s.pem" % (self.certdir, aid)
257            self.log.debug("[ReleaseAccess] Removing %s" % cf)
258            os.remove(cf)
259            return { 'allocID': req['allocID'] } 
260        else:
261            self.state_lock.release()
262            raise service_error(service_error.req, "No such allocation")
263
264    def StartSegment(self, req, fid):
265        """
266        Start a segment.  In this simple skeleton, this means to parse the
267        request and assign an unassigned integer to it.  We store the integer
268        in the persistent state.
269        """
270        try:
271            req = req['StartSegmentRequestBody']
272            # Get the request topology.  If not present, a KeyError is thrown.
273            topref = req['segmentdescription']['topdldescription']
274            # The fedid of the allocation we're attaching resources to
275            auth_attr = req['allocID']['fedid']
276        except KeyError:
277            raise service_error(server_error.req, "Badly formed request")
278
279        # String version of the allocation ID for keying
280        aid = "%s" % auth_attr
281        # Authorization check
282        if not self.auth.check_attribute(fid, auth_attr):
283            raise service_error(service_error.access, "Access denied")
284        else:
285            # See if this is a replay of an earlier succeeded StartSegment -
286            # sometimes SSL kills 'em.  If so, replay the response rather than
287            # redoing the allocation.
288            self.state_lock.acquire()
289            retval = self.state[aid].get('started', None)
290            self.state_lock.release()
291            if retval:
292                self.log.warning(
293                        "[StartSegment] Duplicate StartSegment for %s: " \
294                                % aid + \
295                        "replaying response")
296                return retval
297
298        certfile = "%s/%s.pem" % (self.certdir, aid)
299
300        # Convert the topology into topdl data structures.  Again, the
301        # skeletion doesn't do anything with it, but this is how one parses a
302        # topology request.
303        if topref: topo = topdl.Topology(**topref)
304        else:
305            raise service_error(service_error.req, 
306                    "Request missing segmentdescription'")
307
308        # The attributes of the request.  Not used by this plug-in, but that's
309        # where they are.
310        attrs = req.get('fedAttr', [])
311
312        # Gather connection information.  Used to send messages to those
313        # waiting.
314        connInfo = req.get('connection', [])
315
316        # Do the assignment,  A more complex plug-in would interface to the
317        # facility here to create and configure the allocation.
318        if len(self.available_ints) > 0:
319            # NB: lock the data structure during allocation
320            self.state_lock.acquire()
321            assigned = random.choice([ i for i in self.available_ints])
322            self.available_ints.discard(assigned)
323            self.state[aid]['integer'] = assigned
324            self.write_state()
325            self.state_lock.release()
326            self.log.debug("[StartSegment] Allocated %d to %s" \
327                    % (assigned, aid))
328        else:
329            self.log.debug("[StartSegment] No remaining resources for %s" % aid)
330            raise service_error(service_error.federant, "No available integers")
331
332        # Save the information
333        self.state_lock.acquire()
334        # It's possible that the StartSegment call gets retried (!).
335        # if the 'started' key is in the allocation, we'll return it rather
336        # than redo the setup.  The integer allocation was saved when we made
337        # it.
338        self.state[aid]['started'] = { 
339                'allocID': req['allocID'],
340                'allocationLog': "Allocatation complete",
341                'segmentdescription': { 'topdldescription': topo.to_dict() }
342                }
343        retval = copy.deepcopy(self.state[aid]['started'])
344        self.write_state()
345        self.state_lock.release()
346
347        return retval
348
349    def TerminateSegment(self, req, fid):
350        """
351        Remove the resources associated with th eallocation and stop the music.
352        In this example, this simply means removing the integer we allocated.
353        """
354        # Gather the same access information as for Start Segment
355        try:
356            req = req['TerminateSegmentRequestBody']
357        except KeyError:
358            raise service_error(server_error.req, "Badly formed request")
359
360        auth_attr = req['allocID']['fedid']
361        aid = "%s" % auth_attr
362
363        self.log.debug("Terminate request for %s" %aid)
364        # Check authorization
365        if not self.auth.check_attribute(fid, auth_attr):
366            raise service_error(service_error.access, "Access denied")
367
368        # Authorized: remove the integer from the allocation.  A more complex
369        # plug in would interface with the underlying facility to turn off the
370        # experiment here.
371        self.state_lock.acquire()
372        if aid in self.state:
373            assigned = self.state[aid].get('integer', None)
374            self.available_ints.add(assigned)
375            if 'integer' in self.state[aid]:
376                del self.state[aid]['integer']
377            self.write_state()
378        self.state_lock.release()
379   
380        return { 'allocID': req['allocID'] }
Note: See TracBrowser for help on using the repository browser.