source: fedd/fedd_access.py @ 4ed10ae

axis_examplecompt_changesinfo-opsversion-1.30version-2.00version-3.01version-3.02
Last change on this file since 4ed10ae was 4ed10ae, checked in by Ted Faber <faber@…>, 15 years ago

Proxy key additions working

  • Property mode set to 100644
File size: 21.4 KB
Line 
1#!/usr/local/bin/python
2
3import os,sys
4
5from BaseHTTPServer import BaseHTTPRequestHandler
6from ZSI import *
7from M2Crypto import SSL
8from M2Crypto.m2xmlrpclib import SSL_Transport
9from M2Crypto.SSL.SSLServer import SSLServer
10import M2Crypto.httpslib
11import xmlrpclib
12
13import re
14import string
15import copy
16
17from fedd_access_project import access_project
18from fedd_services import *
19from fedd_util import *
20from fedd_allocate_project import *
21import parse_detail
22from service_error import *
23import logging
24
25
26# Make log messages disappear if noone configures a fedd logger
27class nullHandler(logging.Handler):
28    def emit(self, record): pass
29
30fl = logging.getLogger("fedd.access")
31fl.addHandler(nullHandler())
32
33class fedd_access:
34    """
35    The implementation of access control based on mapping users to projects.
36
37    Users can be mapped to existing projects or have projects created
38    dynamically.  This implements both direct requests and proxies.
39    """
40
41    class parse_error(RuntimeError): pass
42
43    bool_attrs = ("dynamic_projects", "project_priority")
44    emulab_attrs = ("boss", "ops", "domain", "fileserver", "eventserver")
45    id_attrs = ("testbed", "proxy",
46            "proxy_cert_file", "proxy_cert_pwd", "proxy_trusted_certs",
47            "dynamic_projects_url", "dynamic_projects_cert_file", 
48            "dynamic_projects_cert_pwd", "dynamic_projects_trusted_certs")
49    id_list_attrs = ("restricted",)
50
51    def read_trust(self, trust):
52        """
53        Read a trust file that splits fedids into testbeds, users or projects
54
55        Format is:
56
57        [type]
58        fedid
59        fedid
60        default: type
61        """
62        lineno = 0;
63        cat = None
64        cat_re = re.compile("\[(user|testbed|project)\]$", re.IGNORECASE)
65        fedid_re = re.compile("[" + string.hexdigits + "]+$")
66        default_re = re.compile("default:\s*(user|testbed|project)$", 
67                re.IGNORECASE)
68
69        f = open(trust, "r")
70        for line in f:
71            lineno += 1
72            line = line.strip()
73            if len(line) == 0 or line.startswith("#"):
74                continue
75            # Category line
76            m = cat_re.match(line)
77            if m != None:
78                cat = m.group(1).lower()
79                continue
80            # Fedid line
81            m = fedid_re.match(line)
82            if m != None:
83                if cat != None:
84                    self.fedid_category[fedid(hexstr=m.string)] = cat
85                else:
86                    raise self.parse_error(\
87                            "Bad fedid in trust file (%s) line: %d" % \
88                            (trust, lineno))
89                continue
90            # default line
91            m = default_re.match(line)
92            if m != None:
93                self.fedid_default = m.group(1).lower()
94                continue
95            # Nothing matched - bad line, raise exception
96            f.close()
97            raise self.parse_error(\
98                    "Unparsable line in trustfile %s line %d" % (trust, lineno))
99        f.close()
100
101    def read_access(self, config):
102        """
103        Read a configuration file and set internal parameters.
104
105        The format is more complex than one might hope.  The basic format is
106        attribute value pairs separated by colons(:) on a signle line.  The
107        attributes in bool_attrs, emulab_attrs and id_attrs can all be set
108        directly using the name: value syntax.  E.g.
109        boss: hostname
110        sets self.boss to hostname.  In addition, there are access lines of the
111        form (tb, proj, user) -> (aproj, auser) that map the first tuple of
112        names to the second for access purposes.  Names in the key (left side)
113        can include "<NONE> or <ANY>" to act as wildcards or to require the
114        fields to be empty.  Similarly aproj or auser can be <SAME> or
115        <DYNAMIC> indicating that either the matching key is to be used or a
116        dynamic user or project will be created.  These names can also be
117        federated IDs (fedid's) if prefixed with fedid:.  Finally, the aproj
118        can be followed with a colon-separated list of node types to which that
119        project has access (or will have access if dynamic).
120        Testbed attributes outside the forms above can be given using the
121        format attribute: name value: value.  The name is a single word and the
122        value continues to the end of the line.  Empty lines and lines startin
123        with a # are ignored.
124
125        Parsing errors result in a self.parse_error exception being raised.
126        """
127        lineno=0
128        name_expr = "["+string.ascii_letters + string.digits + "\.\-_]+"
129        fedid_expr = "fedid:[" + string.hexdigits + "]+"
130        key_name = "(<ANY>|<NONE>|"+fedid_expr + "|"+ name_expr + ")"
131        access_proj = "(<DYNAMIC>(?::" + name_expr +")*|"+ \
132                "<SAME>" + "(?::" + name_expr + ")*|" + \
133                fedid_expr + "(?::" + name_expr + ")*|" + \
134                name_expr + "(?::" + name_expr + ")*)"
135        access_name = "(<DYNAMIC>|<SAME>|" + fedid_expr + "|"+ name_expr + ")"
136
137        restricted_re = re.compile("restricted:\s*(.*)", re.IGNORECASE)
138        attr_re = re.compile('attribute:\s*([\._\-a-z0-9]+)\s+value:\s*(.*)',
139                re.IGNORECASE)
140        access_re = re.compile('\('+key_name+'\s*,\s*'+key_name+'\s*,\s*'+
141                key_name+'\s*\)\s*->\s*\('+access_proj + '\s*,\s*' + 
142                access_name + '\s*,\s*' + access_name + '\s*\)', re.IGNORECASE)
143
144        def parse_name(n):
145            if n.startswith('fedid:'): return fedid(n[len('fedid:'):])
146            else: return n
147
148        f = open(config, "r");
149        for line in f:
150            lineno += 1
151            line = line.strip();
152            if len(line) == 0 or line.startswith('#'):
153                continue
154
155            # Extended (attribute: x value: y) attribute line
156            m = attr_re.match(line)
157            if m != None:
158                attr, val = m.group(1,2)
159                self.attrs[attr] = val
160                continue
161
162            # Restricted entry
163            m = restricted_re.match(line)
164            if m != None:
165                val = m.group(1)
166                self.restricted.append(val)
167                continue
168
169            # Access line (t, p, u) -> (ap, cu, su) line
170            m = access_re.match(line)
171            if m != None:
172                access_key = tuple([ parse_name(x) for x in m.group(1,2,3)])
173                aps = m.group(4).split(":");
174                if aps[0] == 'fedid:':
175                    del aps[0]
176                    aps[0] = fedid(hexstr=aps[0])
177
178                cu = parse_name(m.group(5))
179                su = parse_name(m.group(6))
180
181                access_val = (access_project(aps[0], aps[1:]),
182                        parse_name(m.group(5)), parse_name(m.group(6)))
183
184                self.access[access_key] = access_val
185                continue
186
187            # Nothing matched to here: unknown line - raise exception
188            f.close()
189            raise self.parse_error("Unknown statement at line %d of %s" % \
190                    (lineno, config))
191        f.close()
192
193
194    def __init__(self, config=None):
195        """
196        Initializer.  Pulls parameters out of the ConfigParser's access section.
197        """
198
199        # Make sure that the configuration is in place
200        if config: 
201            if not config.has_section("access"):
202                config.add_section("access")
203            if not config.has_section("globals"):
204                config.add_section("globals")
205        else:
206            raise RunTimeError("No config to fedd_access")
207
208
209        # Create instance attributes from the static lists
210        for a in fedd_access.bool_attrs:
211            if config.has_option("access", a):
212                setattr(self, a, config.get("access", a))
213            else:
214                setattr(self, a, False)
215
216        for a in fedd_access.emulab_attrs + fedd_access.id_attrs:
217            if config.has_option("access", a):
218                setattr(self, a, config.get("access",a))
219            else:
220                setattr(self, a, None)
221
222        self.attrs = { }
223        self.access = { }
224        self.restricted = [ ]
225        self.fedid_category = { }
226        self.fedid_default = "testbed"
227        if config.has_option("access", "accessdb"):
228            self.read_access(config.get("access", "accessdb"))
229        if config.has_option("access", "trustdb"):
230            self.read_trust(config.get("access", "trustdb"))
231
232        self.log = logging.getLogger("fedd.access")
233
234        # Certs are promoted from the generic to the specific, so without a
235        # specific proxy certificate, the main certificates are used for
236        # proxy interactions. If no dynamic project certificates, then
237        # proxy certs are used, and if none of those the main certs.
238
239        if config.has_option("globals", "proxy_cert_file"):
240            if not self.dynamic_projects_cert_file:
241                self.dynamic_projects_cert_file = \
242                        config.get("globals", "proxy_cert_file")
243                if config.has_option("globals", "porxy_cert_pwd"):
244                    self.dynamic_projects_cert_pwd = \
245                            config.get("globals", "proxy_cert_pwd")
246
247        if config.has_option("globals", "proxy_trusted_certs"):
248            if not self.dynamic_projects_trusted_certs:
249                self.dynamic_projects_trusted_certs =\
250                        config.get("globals", proxy_trusted_certs)
251
252        if config.has_option("globals", "cert_file"):
253            has_pwd = config.has_option("globals", "cert_pwd")
254            if not self.dynamic_projects_cert_file:
255                self.dynamic_projects_cert_file = \
256                        config.get("globals", "cert_file")
257                if has_pwd: 
258                    self.dynamic_projects_cert_pwd = \
259                            config.get("globals", "cert_pwd")
260            if not self.proxy_cert_file:
261                self.proxy_cert_file = config.get("globals", "cert_file")
262                if has_pwd:
263                    self.proxy_cert_pwd = config.get("globals", "cert_pwd")
264
265        if config.get("globals", "trusted_certs"):
266            if not self.proxy_trusted_certs:
267                self.proxy_trusted_certs = \
268                        config.get("globals", "trusted_certs")
269            if not self.dynamic_projects_trusted_certs:
270                self.dynamic_projects_trusted_certs = \
271                        config.get("globals", "trusted_certs")
272
273        proj_certs = (self.dynamic_projects_cert_file, 
274                self.dynamic_projects_trusted_certs,
275                self.dynamic_projects_cert_pwd)
276
277        self.soap_services = {\
278            'RequestAccess': make_soap_handler(\
279                RequestAccessRequestMessage.typecode,\
280                self.RequestAccess, RequestAccessResponseMessage,\
281                "RequestAccessResponseBody")\
282            }
283        self.xmlrpc_services =  {\
284            'RequestAccess': make_xmlrpc_handler(\
285                self.RequestAccess, "RequestAccessResponseBody")\
286            }
287
288
289        if not config.has_option("access", "dynamic_projects_url"):
290            self.allocate_project = \
291                fedd_allocate_project_local(config)
292        else:
293            self.allocate_project = \
294                fedd_allocate_project_remote(config)
295
296        # If the project allocator exports services, put them in this object's
297        # maps so that classes that instantiate this can call the services.
298        self.soap_services.update(self.allocate_project.soap_services)
299        self.xmlrpc_services.update(self.allocate_project.xmlrpc_services)
300
301    def dump_state(self):
302        """
303        Dump the state read from a configuration file.  Mostly for debugging.
304        """
305        for a in fedd_access.bool_attrs:
306            print "%s: %s" % (a, getattr(self, a ))
307        for a in fedd_access.emulab_attrs + fedd_access.id_attrs:
308            print "%s: %s" % (a, getattr(self, a))
309        for k, v in self.attrs.iteritems():
310            print "%s %s" % (k, v)
311        print "Access DB:"
312        for k, v in self.access.iteritems():
313            print "%s %s" % (k, v)
314        print "Trust DB:"
315        for k, v in self.fedid_category.iteritems():
316            print "%s %s" % (k, v)
317        print "Restricted: %s" % str(',').join(sorted(self.restricted))
318
319    def get_users(self, obj):
320        """
321        Return a list of the IDs of the users in dict
322        """
323        if obj.has_key('user'):
324            return [ unpack_id(u['userID']) \
325                    for u in obj['user'] if u.has_key('userID') ]
326        else:
327            return None
328
329    def proxy_xmlrpc_request(self, dt, req):
330        """Send an XMLRPC proxy request.  Called if the SOAP RPC fails"""
331
332        # No retry loop here.  Proxy servers must correctly authenticate
333        # themselves without help
334        try:
335            ctx = fedd_ssl_context(self.proxy_cert_file, 
336                    self.proxy_trusted_certs, password=self.proxy_cert_pwd)
337        except SSL.SSLError:
338            raise service_error(service_error.server_config,
339                    "Server certificates misconfigured")
340
341        # Of all the dumbass things.  The XMLRPC library in use here won't
342        # properly encode unicode strings, so we make a copy of req with the
343        # unicode objects converted.  We also convert the destination testbed
344        # to a basic string if it isn't one already.
345        if isinstance(dt, str): url = dt
346        else: url = str(dt)
347
348        r = strip_unicode(copy.deepcopy(req))
349       
350        transport = SSL_Transport(ctx)
351        port = xmlrpclib.ServerProxy(url, transport=transport)
352
353        # Reconstruct the full request message
354        try:
355            resp = port.RequestAccess(
356                    { "RequestAccessRequestBody": r})
357            resp, method = xmlrpclib.loads(resp)
358        except xmlrpclib.Fault, f:
359            se = service_error(None, f.faultString, f.faultCode)
360            raise se
361        except xmlrpclib.Error, e:
362            raise service_error(service_error.proxy, 
363                    "Remote XMLRPC Fault: %s" % e)
364       
365        if resp[0].has_key('RequestAccessResponseBody'):
366            return resp[0]['RequestAccessResponseBody']
367        else:
368            raise service_error(service_error.proxy, 
369                    "Bad proxy response")
370
371    def proxy_request(self, dt, req):
372        """
373        Send req on to the real destination in dt and return the response
374
375        Req is just the requestType object.  This function re-wraps it.  It
376        also rethrows any faults.
377        """
378        # No retry loop here.  Proxy servers must correctly authenticate
379        # themselves without help
380        try:
381            ctx = fedd_ssl_context(self.proxy_cert_file, 
382                    self.proxy_trusted_certs, password=self.proxy_cert_pwd)
383        except SSL.SSLError:
384            raise service_error(service_error.server_config, 
385                    "Server certificates misconfigured")
386
387        loc = feddServiceLocator();
388        port = loc.getfeddPortType(dt,
389                transport=M2Crypto.httpslib.HTTPSConnection, 
390                transdict={ 'ssl_context' : ctx })
391
392        # Reconstruct the full request message
393        msg = RequestAccessRequestMessage()
394        msg.set_element_RequestAccessRequestBody(
395                pack_soap(msg, "RequestAccessRequestBody", req))
396        try:
397            resp = port.RequestAccess(msg)
398        except ZSI.ParseException, e:
399            raise service_error(service_error.proxy,
400                    "Bad format message (XMLRPC??): %s" %
401                    str(e))
402        r = unpack_soap(resp)
403
404        if r.has_key('RequestAccessResponseBody'):
405            return r['RequestAccessResponseBody']
406        else:
407            raise service_error(service_error.proxy,
408                    "Bad proxy response")
409
410    def permute_wildcards(self, a, p):
411        """Return a copy of a with various fields wildcarded.
412
413        The bits of p control the wildcards.  A set bit is a wildcard
414        replacement with the lowest bit being user then project then testbed.
415        """
416        if p & 1: user = ["<any>"]
417        else: user = a[2]
418        if p & 2: proj = "<any>"
419        else: proj = a[1]
420        if p & 4: tb = "<any>"
421        else: tb = a[0]
422
423        return (tb, proj, user)
424
425    def find_access(self, search):
426        """
427        Search the access DB for a match on this tuple.  Return the matching
428        access tuple and the user that matched.
429       
430        NB, if the initial tuple fails to match we start inserting wildcards in
431        an order determined by self.project_priority.  Try the list of users in
432        order (when wildcarded, there's only one user in the list).
433        """
434        if self.project_priority: perm = (0, 1, 2, 3, 4, 5, 6, 7)
435        else: perm = (0, 2, 1, 3, 4, 6, 5, 7)
436
437        for p in perm: 
438            s = self.permute_wildcards(search, p)
439            # s[2] is None on an anonymous, unwildcarded request
440            if s[2] != None:
441                for u in s[2]:
442                    if self.access.has_key((s[0], s[1], u)):
443                        return (self.access[(s[0], s[1], u)], u)
444            else:
445                if self.access.has_key(s):
446                    return (self.access[s], None)
447        return None, None
448
449    def lookup_access(self, req, fid):
450        """
451        Determine the allowed access for this request.  Return the access and
452        which fields are dynamic.
453
454        The fedid is needed to construct the request
455        """
456        # Search keys
457        tb = None
458        project = None
459        user = None
460        # Return values
461        rp = access_project(None, ())
462        ru = None
463
464
465        principal_type = self.fedid_category.get(fid, self.fedid_default)
466
467        if principal_type == "testbed": tb = fid
468
469        if req.has_key('project'):
470            p = req['project']
471            if p.has_key('name'):
472                project = unpack_id(p['name'])
473            user = self.get_users(p)
474        else:
475            user = self.get_users(req)
476
477        # Now filter by prinicpal type
478        if principal_type == "user":
479            if user != None:
480                fedids = [ u for u in user if isinstance(u, type(fid))]
481                if len(fedids) > 1:
482                    raise service_error(service_error.req,
483                            "User asserting multiple fedids")
484                elif len(fedids) == 1 and fedids[0] != fid:
485                    raise service_error(service_error.req,
486                            "User asserting different fedid")
487            project = None
488            tb = None
489        elif principal_type == "project":
490            if isinstance(project, type(fid)) and fid != project:
491                raise service_error(service_error.req,
492                        "Project asserting different fedid")
493            tb = None
494
495        # Ready to look up access
496        found, user_match = self.find_access((tb, project, user))
497       
498        if found == None:
499            raise service_error(service_error.access,
500                    "Access denied")
501
502        # resolve <dynamic> and <same> in found
503        dyn_proj = False
504        dyn_create_user = False
505        dyn_service_user = False
506
507        if found[0].name == "<same>":
508            if project != None:
509                rp.name = project
510            else : 
511                raise service_error(\
512                        service_error.server_config,
513                        "Project matched <same> when no project given")
514        elif found[0].name == "<dynamic>":
515            rp.name = None
516            dyn_proj = True
517        else:
518            rp.name = found[0].name
519        rp.node_types = found[0].node_types;
520
521        if found[1] == "<same>":
522            if user_match == "<any>":
523                if user != None: rcu = user[0]
524                else: raise service_error(\
525                        service_error.server_config,
526                        "Matched <same> on anonymous request")
527            else:
528                rcu = user_match
529        elif found[1] == "<dynamic>":
530            rcu = None
531            dyn_create_user = True
532       
533        if found[2] == "<same>":
534            if user_match == "<any>":
535                if user != None: rsu = user[0]
536                else: raise service_error(\
537                        service_error.server_config,
538                        "Matched <same> on anonymous request")
539            else:
540                rsu = user_match
541        elif found[2] == "<dynamic>":
542            rsu = None
543            dyn_service_user = True
544
545        return (rp, rcu, rsu), (dyn_create_user, dyn_service_user, dyn_proj)
546
547    def build_response(self, alloc_id, ap):
548        """
549        Create the SOAP response.
550
551        Build the dictionary description of the response and use
552        fedd_utils.pack_soap to create the soap message.  NB that alloc_id is
553        a fedd_services_types.IDType_Holder pulled from the incoming message.
554        ap is the allocate project message returned from a remote project
555        allocation (even if that allocation was done locally).
556        """
557        # Because alloc_id is already a fedd_services_types.IDType_Holder,
558        # there's no need to repack it
559        msg = { 
560                'allocID': alloc_id,
561                'emulab': { 
562                    'domain': self.domain,
563                    'boss': self.boss,
564                    'ops': self.ops,
565                    'fileServer': self.fileserver,
566                    'eventServer': self.eventserver,
567                    'project': ap['project']
568                },
569            }
570        if len(self.attrs) > 0:
571            msg['emulab']['fedAttr'] = \
572                [ { 'attribute': x, 'value' : y } \
573                        for x,y in self.attrs.iteritems()]
574        return msg
575
576    def RequestAccess(self, req, fid):
577        """
578        Handle the access request.  Proxy if not for us.
579
580        Parse out the fields and make the allocations or rejections if for us,
581        otherwise, assuming we're willing to proxy, proxy the request out.
582        """
583
584        # The dance to get into the request body
585        if req.has_key('RequestAccessRequestBody'):
586            req = req['RequestAccessRequestBody']
587        else:
588            raise service_error(service_error.req, "No request!?")
589
590        if req.has_key('destinationTestbed'):
591            dt = unpack_id(req['destinationTestbed'])
592
593        if dt == None or dt == self.testbed:
594            # Request for this fedd
595            found, dyn = self.lookup_access(req, fid)
596            restricted = None
597            ap = None
598
599            # Check for access to restricted nodes
600            if req.has_key('resources') and req['resources'].has_key('node'):
601                resources = req['resources']
602                restricted = [ t for n in resources['node'] \
603                                if n.has_key('hardware') \
604                                    for t in n['hardware'] \
605                                        if t in self.restricted ]
606                inaccessible = [ t for t in restricted \
607                                    if t not in found[0].node_types]
608                if len(inaccessible) > 0:
609                    raise service_error(service_error.access,
610                            "Access denied (nodetypes %s)" % \
611                            str(', ').join(inaccessible))
612            # These collect the keys for teh two roles into single sets, one
613            # for creation and one for service.  The sets are a simple way to
614            # eliminate duplicates
615            create_ssh = set([ x['sshPubkey'] \
616                    for x in req['createAccess'] \
617                        if x.has_key('sshPubkey')])
618
619            service_ssh = set([ x['sshPubkey'] \
620                    for x in req['serviceAccess'] \
621                        if x.has_key('sshPubkey')])
622
623            if len(create_ssh) > 0 and len(service_ssh) >0: 
624                if dyn[1]: 
625                    # Compose the dynamic project request
626                    # (only dynamic, dynamic currently allowed)
627                    preq = { 'AllocateProjectRequestBody': \
628                                { 'project' : {\
629                                    'user': [ \
630                                    { \
631                                        'access': [ { 'sshPubkey': s } \
632                                            for s in service_ssh ], 
633                                         'role': "serviceAccess",\
634                                    }, \
635                                    { \
636                                        'access': [ { 'sshPubkey': s } \
637                                            for s in create_ssh ], 
638                                         'role': "experimentCreation",\
639                                    }, \
640                                    ], \
641                                    }\
642                                }\
643                            }
644                    if restricted != None and len(restricted) > 0:
645                        preq['AllocateProjectRequestBody']['resources'] = \
646                            [ {'node': { 'hardware' :  [ h ] } } \
647                                    for h in restricted ]
648                               
649                    ap = self.allocate_project.dynamic_project(preq)
650                else:
651                    # XXX ssh key additions
652                    preq = {'StaticProjectRequestBody' : \
653                            { 'project': \
654                                { 'name' : { 'localname' : found[0].name },\
655                                  'user' : [ \
656                                    {\
657                                        'userID': { 'localname' : found[1] }, \
658                                        'access': [ { 'sshPubkey': s } 
659                                            for s in create_ssh ],
660                                        'role': 'experimentCreation'\
661                                    },\
662                                    {\
663                                        'userID': { 'localname' : found[2] }, \
664                                        'access': [ { 'sshPubkey': s } 
665                                            for s in service_ssh ],
666                                        'role': 'serviceAccess'\
667                                    },\
668                                ]}\
669                            }\
670                    }
671                    if restricted != None and len(restricted) > 0:
672                        preq['StaticProjectRequestBody']['resources'] = \
673                            [ {'node': { 'hardware' :  [ h ] } } \
674                                    for h in restricted ]
675                    ap = self.allocate_project.static_project(preq)
676            else:
677                raise service_error(service_error.req, 
678                        "SSH access parameters required")
679
680            resp = self.build_response(req['allocID'], ap)
681            return resp
682        else:
683            p_fault = None      # Any SOAP failure (sent unless XMLRPC works)
684            try:
685                # Proxy the request using SOAP
686                return self.proxy_request(dt, req)
687            except service_error, e:
688                if e.code == service_error.proxy: p_fault = None
689                else: raise
690            except ZSI.FaultException, f:
691                p_fault = f.fault.detail[0]
692                   
693
694            # If we could not get a valid SOAP response to the request above,
695            # try the same address using XMLRPC and let any faults flow back
696            # out.
697            if p_fault == None:
698                return self.proxy_xmlrpc_request(dt, req)
699            else:
700                # Build the fault
701                body = p_fault.get_element_FeddFaultBody()
702                if body != None:
703                    raise service_error(body.get_element_code(),
704                                body.get_element_desc());
705                else:
706                    raise service_error(\
707                            service_error.proxy,
708                            "Undefined fault from proxy??");
Note: See TracBrowser for help on using the repository browser.