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

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

ABAC into the skeleton

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