source: fedd/federation/skeleton_access.py @ 2f45140

Last change on this file since 2f45140 was 6bedbdba, checked in by Ted Faber <faber@…>, 13 years ago

Split topdl and fedid out to different packages. Add differential
installs

  • Property mode set to 100644
File size: 9.1 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 deter 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
17from deter import 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 'abac'
80        self.auth_dir = config.get('access', 'auth_dir')
81        accessdb = config.get("access", "accessdb")
82        # initialize the authorization system.  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 == 'abac':
87            #  Load the current authorization state
88            self.auth = abac_authorizer(load=self.auth_dir)
89            self.access = [ ]
90            if accessdb:
91                try:
92                    self.read_access(accessdb, self.parse_access_string)
93                except EnvironmentError, e:
94                    self.log.error("Cannot read %s: %s" % \
95                            (config.get("access", "accessdb"), e))
96                    raise e
97        else:
98            raise service_error(service_error.internal, 
99                    "Unknown auth_type: %s" % self.auth_type)
100
101        # Clean the state
102        self.state_lock.acquire()
103        for k in self.state.keys():
104            # Remove any allocated integers from the available ones
105            if 'integer' in self.state[k]:
106                self.available_ints.discard(self.state[k]['integer'])
107        self.state_lock.release()
108
109        # These dictionaries register the plug-in's local routines for handline
110        # these four messages with the server code above.  There's a version
111        # for SOAP and XMLRPC, depending on which interfaces the plugin
112        # supports.  There's rarely a technical reason not to support one or
113        # the other - the plugin code almost never deals with the transport -
114        # but if a plug-in writer wanted to disable XMLRPC, they could leave
115        # the self.xmlrpc_services dictionary empty.
116        self.soap_services = {\
117            'RequestAccess': soap_handler("RequestAccess", self.RequestAccess),
118            'ReleaseAccess': soap_handler("ReleaseAccess", self.ReleaseAccess),
119            'StartSegment': soap_handler("StartSegment", self.StartSegment),
120            'TerminateSegment': soap_handler("TerminateSegment", 
121                self.TerminateSegment),
122            }
123        self.xmlrpc_services =  {\
124            'RequestAccess': xmlrpc_handler('RequestAccess',
125                self.RequestAccess),
126            'ReleaseAccess': xmlrpc_handler('ReleaseAccess',
127                self.ReleaseAccess),
128            'StartSegment': xmlrpc_handler("StartSegment", self.StartSegment),
129            'TerminateSegment': xmlrpc_handler('TerminateSegment',
130                self.TerminateSegment),
131            }
132
133    # RequestAccess and ReleaseAccess come from the base class
134
135    def StartSegment(self, req, fid):
136        """
137        Start a segment.  In this simple skeleton, this means to parse the
138        request and assign an unassigned integer to it.  We store the integer
139        in the persistent state.
140        """
141        try:
142            req = req['StartSegmentRequestBody']
143            # Get the request topology.  If not present, a KeyError is thrown.
144            topref = req['segmentdescription']['topdldescription']
145            # The fedid of the allocation we're attaching resources to
146            auth_attr = req['allocID']['fedid']
147        except KeyError:
148            raise service_error(server_error.req, "Badly formed request")
149
150        # String version of the allocation ID for keying
151        aid = "%s" % auth_attr
152        # Authorization check
153        access_ok, proof = self.auth.check_attribute(fid, auth_attr, 
154                with_proof=True)
155        if not access_ok:
156            raise service_error(service_error.access, "Access denied", 
157                    proof=proof)
158        else:
159            # See if this is a replay of an earlier succeeded StartSegment -
160            # sometimes SSL kills 'em.  If so, replay the response rather than
161            # redoing the allocation.
162            self.state_lock.acquire()
163            retval = self.state[aid].get('started', None)
164            self.state_lock.release()
165            if retval:
166                self.log.warning(
167                        "[StartSegment] Duplicate StartSegment for %s: " \
168                                % aid + \
169                        "replaying response")
170                return retval
171
172        certfile = "%s/%s.pem" % (self.certdir, aid)
173
174        # Convert the topology into topdl data structures.  Again, the
175        # skeletion doesn't do anything with it, but this is how one parses a
176        # topology request.
177        if topref: topo = topdl.Topology(**topref)
178        else:
179            raise service_error(service_error.req, 
180                    "Request missing segmentdescription'")
181
182        # The attributes of the request.  Not used by this plug-in, but that's
183        # where they are.
184        attrs = req.get('fedAttr', [])
185
186        # Gather connection information.  Used to send messages to those
187        # waiting.
188        connInfo = req.get('connection', [])
189
190        # Do the assignment,  A more complex plug-in would interface to the
191        # facility here to create and configure the allocation.
192        if len(self.available_ints) > 0:
193            # NB: lock the data structure during allocation
194            self.state_lock.acquire()
195            assigned = random.choice([ i for i in self.available_ints])
196            self.available_ints.discard(assigned)
197            self.state[aid]['integer'] = assigned
198            self.write_state()
199            self.state_lock.release()
200            self.log.debug("[StartSegment] Allocated %d to %s" \
201                    % (assigned, aid))
202        else:
203            self.log.debug("[StartSegment] No remaining resources for %s" % aid)
204            raise service_error(service_error.federant, "No available integers")
205
206        # Save the information
207        self.state_lock.acquire()
208        # It's possible that the StartSegment call gets retried (!).
209        # if the 'started' key is in the allocation, we'll return it rather
210        # than redo the setup.  The integer allocation was saved when we made
211        # it.
212        self.state[aid]['started'] = { 
213                'allocID': req['allocID'],
214                'allocationLog': "Allocatation complete",
215                'segmentdescription': { 'topdldescription': topo.to_dict() },
216                'proof': proof.to_dict(),
217                }
218        retval = copy.deepcopy(self.state[aid]['started'])
219        self.write_state()
220        self.state_lock.release()
221
222        return retval
223
224    def TerminateSegment(self, req, fid):
225        """
226        Remove the resources associated with th eallocation and stop the music.
227        In this example, this simply means removing the integer we allocated.
228        """
229        # Gather the same access information as for Start Segment
230        try:
231            req = req['TerminateSegmentRequestBody']
232        except KeyError:
233            raise service_error(server_error.req, "Badly formed request")
234
235        auth_attr = req['allocID']['fedid']
236        aid = "%s" % auth_attr
237
238        self.log.debug("Terminate request for %s" %aid)
239        # Check authorization
240        access_ok, proof = self.auth.check_attribute(fid, auth_attr, 
241                with_proof=True)
242        if not access_ok:
243            raise service_error(service_error.access, "Access denied", 
244                    proof=proof)
245
246        # Authorized: remove the integer from the allocation.  A more complex
247        # plug in would interface with the underlying facility to turn off the
248        # experiment here.
249        self.state_lock.acquire()
250        if aid in self.state:
251            assigned = self.state[aid].get('integer', None)
252            self.available_ints.add(assigned)
253            if 'integer' in self.state[aid]:
254                del self.state[aid]['integer']
255            self.write_state()
256        self.state_lock.release()
257   
258        return { 'allocID': req['allocID'], 'proof': proof.to_dict() }
Note: See TracBrowser for help on using the repository browser.