source: fedd/federation/skeleton_access.py @ cd60510

axis_examplecompt_changesinfo-ops
Last change on this file since cd60510 was 35a5879, checked in by Mike Ryan <mikeryan@…>, 14 years ago

inherit from legacy_access so legacy access checking works

  • Property mode set to 100644
File size: 13.9 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.write_state()
197        self.state_lock.release()
198        # Authorize the creating fedid and the principal representing the
199        # allocation to manipulate it.
200        self.auth.set_attribute(fid, allocID)
201        self.auth.set_attribute(allocID, allocID)
202        self.auth.save()
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            del self.state[aid]
252            self.write_state()
253            self.state_lock.release()
254            # And remove the access cert
255            cf = "%s/%s.pem" % (self.certdir, aid)
256            self.log.debug("[ReleaseAccess] Removing %s" % cf)
257            os.remove(cf)
258            return { 'allocID': req['allocID'] } 
259        else:
260            self.state_lock.release()
261            raise service_error(service_error.req, "No such allocation")
262
263    def StartSegment(self, req, fid):
264        """
265        Start a segment.  In this simple skeleton, this means to parse the
266        request and assign an unassigned integer to it.  We store the integer
267        in the persistent state.
268        """
269        try:
270            req = req['StartSegmentRequestBody']
271            # Get the request topology.  If not present, a KeyError is thrown.
272            topref = req['segmentdescription']['topdldescription']
273            # The fedid of the allocation we're attaching resources to
274            auth_attr = req['allocID']['fedid']
275        except KeyError:
276            raise service_error(server_error.req, "Badly formed request")
277
278        # String version of the allocation ID for keying
279        aid = "%s" % auth_attr
280        # Authorization check
281        if not self.auth.check_attribute(fid, auth_attr):
282            raise service_error(service_error.access, "Access denied")
283        else:
284            # See if this is a replay of an earlier succeeded StartSegment -
285            # sometimes SSL kills 'em.  If so, replay the response rather than
286            # redoing the allocation.
287            self.state_lock.acquire()
288            retval = self.state[aid].get('started', None)
289            self.state_lock.release()
290            if retval:
291                self.log.warning(
292                        "[StartSegment] Duplicate StartSegment for %s: " \
293                                % aid + \
294                        "replaying response")
295                return retval
296
297        certfile = "%s/%s.pem" % (self.certdir, aid)
298
299        # Convert the topology into topdl data structures.  Again, the
300        # skeletion doesn't do anything with it, but this is how one parses a
301        # topology request.
302        if topref: topo = topdl.Topology(**topref)
303        else:
304            raise service_error(service_error.req, 
305                    "Request missing segmentdescription'")
306
307        # The attributes of the request.  Not used by this plug-in, but that's
308        # where they are.
309        attrs = req.get('fedAttr', [])
310
311        # Gather connection information.  Used to send messages to those
312        # waiting.
313        connInfo = req.get('connection', [])
314
315        # Do the assignment,  A more complex plug-in would interface to the
316        # facility here to create and configure the allocation.
317        if len(self.available_ints) > 0:
318            # NB: lock the data structure during allocation
319            self.state_lock.acquire()
320            assigned = random.choice([ i for i in self.available_ints])
321            self.available_ints.discard(assigned)
322            self.state[aid]['integer'] = assigned
323            self.write_state()
324            self.state_lock.release()
325            self.log.debug("[StartSegment] Allocated %d to %s" \
326                    % (assigned, aid))
327        else:
328            self.log.debug("[StartSegment] No remaining resources for %s" % aid)
329            raise service_error(service_error.federant, "No available integers")
330
331        # Save the information
332        self.state_lock.acquire()
333        # It's possible that the StartSegment call gets retried (!).
334        # if the 'started' key is in the allocation, we'll return it rather
335        # than redo the setup.  The integer allocation was saved when we made
336        # it.
337        self.state[aid]['started'] = { 
338                'allocID': req['allocID'],
339                'allocationLog': "Allocatation complete",
340                'segmentdescription': { 'topdldescription': topo.to_dict() }
341                }
342        retval = copy.deepcopy(self.state[aid]['started'])
343        self.write_state()
344        self.state_lock.release()
345
346        return retval
347
348    def TerminateSegment(self, req, fid):
349        """
350        Remove the resources associated with th eallocation and stop the music.
351        In this example, this simply means removing the integer we allocated.
352        """
353        # Gather the same access information as for Start Segment
354        try:
355            req = req['TerminateSegmentRequestBody']
356        except KeyError:
357            raise service_error(server_error.req, "Badly formed request")
358
359        auth_attr = req['allocID']['fedid']
360        aid = "%s" % auth_attr
361
362        self.log.debug("Terminate request for %s" %aid)
363        # Check authorization
364        if not self.auth.check_attribute(fid, auth_attr):
365            raise service_error(service_error.access, "Access denied")
366
367        # Authorized: remove the integer from the allocation.  A more complex
368        # plug in would interface with the underlying facility to turn off the
369        # experiment here.
370        self.state_lock.acquire()
371        if aid in self.state:
372            assigned = self.state[aid].get('integer', None)
373            self.available_ints.add(assigned)
374            if 'integer' in self.state[aid]:
375                del self.state[aid]['integer']
376            self.write_state()
377        self.state_lock.release()
378   
379        return { 'allocID': req['allocID'] }
Note: See TracBrowser for help on using the repository browser.