#!/usr/local/bin/python import os,sys from BaseHTTPServer import BaseHTTPRequestHandler from ZSI import * from M2Crypto import SSL from M2Crypto.m2xmlrpclib import SSL_Transport from M2Crypto.SSL.SSLServer import SSLServer import M2Crypto.httpslib import xmlrpclib import re import string import copy from fedd_config_file import config_file from fedd_access_project import access_project from fedd_services import * from fedd_util import * from fedd_allocate_project import * import parse_detail from service_error import * import logging class nullHandler(logging.Handler): def emit(self, record): pass fl = logging.getLogger("fedd.access") fl.addHandler(nullHandler()) class fedd_access: """ The implementation of access control based on mapping users to projects. Users can be mapped to existing projects or have projects created dynamically. This implements both direct requests and proxies. """ def __init__(self, config=None): """ Initializer. Parses a configuration if one is given. """ # Read the configuration if not config: raise RunTimeError("No config to fedd_allocate") # Create instance attributes from the static lists for a in config_file.bool_attrs: setattr(self, a, getattr(config, a, False)) for a in config_file.emulab_attrs + config_file.id_attrs: setattr(self, a, getattr(config, a, None)) self.attrs = copy.deepcopy(config.attrs); self.access = copy.deepcopy(config.access); self.fedid_category = copy.deepcopy(config.fedid_category) self.fedid_default = config.fedid_default self.restricted = copy.copy(config.restricted) self.log = logging.getLogger("fedd.access") # Certs are promoted from the generic to the specific, so without a # specific proxy certificate, the main certificates are used for # proxy interactions. If no dynamic project certificates, then # proxy certs are used, and if none of those the main certs. if config.proxy_cert_file: if not self.dynamic_projects_cert_file: self.dynamic_projects_cert_file = config.proxy_cert_file self.dynamic_projects_cert_pwd = config.proxy_cert_pwd if config.proxy_trusted_certs: if not self.dynamic_projects_trusted_certs: self.dynamic_projects_trusted_certs =\ config.proxy_trusted_certs if config.cert_file: if not self.dynamic_projects_cert_file: self.dynamic_projects_cert_file = config.cert_file self.dynamic_projects_cert_pwd = config.cert_pwd if not self.proxy_cert_file: self.proxy_cert_file = config.cert_file self.proxy_cert_pwd = config.cert_pwd if config.trusted_certs: if not self.proxy_trusted_certs: self.proxy_trusted_certs = config.trusted_certs if not self.dynamic_projects_trusted_certs: self.dynamic_projects_trusted_certs = config.trusted_certs proj_certs = (self.dynamic_projects_cert_file, self.dynamic_projects_trusted_certs, self.dynamic_projects_cert_pwd) if config.dynamic_projects_url == None: self.allocate_project = \ fedd_allocate_project_local(config.dynamic_projects, config.dynamic_projects_url, proj_certs) else: self.allocate_project = \ fedd_allocate_project_remote(config.dynamic_projects, config.dynamic_projects_url, proj_certs) self.soap_handlers = {\ 'RequestAccess': make_soap_handler(\ RequestAccessRequestMessage.typecode,\ self.RequestAccess, RequestAccessResponseMessage,\ "RequestAccessResponseBody")\ } self.xmlrpc_handlers = {\ 'RequestAccess': make_xmlrpc_handler(\ self.RequestAccess, "RequestAccessResponseBody")\ } def dump_state(self): """ Dump the state read from a configuration file. Mostly for debugging. """ for a in fedd_proj.bool_attrs: print "%s: %s" % (a, getattr(self, a )) for a in fedd_proj.emulab_attrs + fedd_proj.id_attrs: print "%s: %s" % (a, getattr(self, a)) for k, v in self.attrs.iteritems(): print "%s %s" % (k, v) print "Access DB:" for k, v in self.access.iteritems(): print "%s %s" % (k, v) print "Trust DB:" for k, v in self.fedid_category.iteritems(): print "%s %s" % (k, v) print "Restricted: %s" % str(',').join(sorted(self.restricted)) def get_users(self, obj): """ Return a list of the IDs of the users in dict """ if obj.has_key('user'): return [ unpack_id(u['userID']) \ for u in obj['user'] if u.has_key('userID') ] else: return None def strip_unicode(self, obj): """Loosly de-unicode an object""" if isinstance(obj, dict): for k in obj.keys(): obj[k] = self.strip_unicode(obj[k]) return obj elif isinstance(obj, basestring): return str(obj) elif getattr(obj, "__iter__", None): return [ self.strip_unicode(x) for x in obj] else: return obj def proxy_xmlrpc_request(self, dt, req): """Send an XMLRPC proxy request. Called if the SOAP RPC fails""" # No retry loop here. Proxy servers must correctly authenticate # themselves without help try: ctx = fedd_ssl_context(self.proxy_cert_file, self.proxy_trusted_certs, password=self.proxy_cert_pwd) except SSL.SSLError: raise service_error(service_error.server_config, "Server certificates misconfigured") # Of all the dumbass things. The XMLRPC library in use here won't # properly encode unicode strings, so we make a copy of req with the # unicode objects converted. We also convert the destination testbed # to a basic string if it isn't one already. if isinstance(dt, str): url = dt else: url = str(dt) r = copy.deepcopy(req) self.strip_unicode(r) transport = SSL_Transport(ctx) port = xmlrpclib.ServerProxy(url, transport=transport) # Reconstruct the full request message try: resp = port.RequestAccess( { "RequestAccessRequestBody": r}) resp, method = xmlrpclib.loads(resp) except xmlrpclib.Fault, f: se = service_error(None, f.faultString, f.faultCode) raise se except xmlrpclib.Error, e: raise service_error(service_error.proxy, "Remote XMLRPC Fault: %s" % e) if resp[0].has_key('RequestAccessResponseBody'): return resp[0]['RequestAccessResponseBody'] else: raise service_error(service_error.proxy, "Bad proxy response") def proxy_request(self, dt, req): """ Send req on to the real destination in dt and return the response Req is just the requestType object. This function re-wraps it. It also rethrows any faults. """ # No retry loop here. Proxy servers must correctly authenticate # themselves without help try: ctx = fedd_ssl_context(self.proxy_cert_file, self.proxy_trusted_certs, password=self.proxy_cert_pwd) except SSL.SSLError: raise service_error(service_error.server_config, "Server certificates misconfigured") loc = feddServiceLocator(); port = loc.getfeddPortType(dt, transport=M2Crypto.httpslib.HTTPSConnection, transdict={ 'ssl_context' : ctx }) # Reconstruct the full request message msg = RequestAccessRequestMessage() msg.set_element_RequestAccessRequestBody( pack_soap(msg, "RequestAccessRequestBody", req)) try: resp = port.RequestAccess(msg) except ZSI.ParseException, e: raise service_error(service_error.proxy, "Bad format message (XMLRPC??): %s" % str(e)) r = unpack_soap(resp) if r.has_key('RequestAccessResponseBody'): return r['RequestAccessResponseBody'] else: raise service_error(service_error.proxy, "Bad proxy response") def permute_wildcards(self, a, p): """Return a copy of a with various fields wildcarded. The bits of p control the wildcards. A set bit is a wildcard replacement with the lowest bit being user then project then testbed. """ if p & 1: user = [""] else: user = a[2] if p & 2: proj = "" else: proj = a[1] if p & 4: tb = "" else: tb = a[0] return (tb, proj, user) def find_access(self, search): """ Search the access DB for a match on this tuple. Return the matching access tuple and the user that matched. NB, if the initial tuple fails to match we start inserting wildcards in an order determined by self.project_priority. Try the list of users in order (when wildcarded, there's only one user in the list). """ if self.project_priority: perm = (0, 1, 2, 3, 4, 5, 6, 7) else: perm = (0, 2, 1, 3, 4, 6, 5, 7) for p in perm: s = self.permute_wildcards(search, p) # s[2] is None on an anonymous, unwildcarded request if s[2] != None: for u in s[2]: if self.access.has_key((s[0], s[1], u)): return (self.access[(s[0], s[1], u)], u) else: if self.access.has_key(s): return (self.access[s], None) return None, None def lookup_access(self, req, fid): """ Determine the allowed access for this request. Return the access and which fields are dynamic. The fedid is needed to construct the request """ # Search keys tb = None project = None user = None # Return values rp = access_project(None, ()) ru = None principal_type = self.fedid_category.get(fid, self.fedid_default) if principal_type == "testbed": tb = fid if req.has_key('project'): p = req['project'] if p.has_key('name'): project = unpack_id(p['name']) user = self.get_users(p) else: user = self.get_users(req) # Now filter by prinicpal type if principal_type == "user": if user != None: fedids = [ u for u in user if isinstance(u, type(fid))] if len(fedids) > 1: raise service_error(service_error.req, "User asserting multiple fedids") elif len(fedids) == 1 and fedids[0] != fid: raise service_error(service_error.req, "User asserting different fedid") project = None tb = None elif principal_type == "project": if isinstance(project, type(fid)) and fid != project: raise service_error(service_error.req, "Project asserting different fedid") tb = None # Ready to look up access found, user_match = self.find_access((tb, project, user)) if found == None: raise service_error(service_error.access, "Access denied") # resolve and in found dyn_proj = False dyn_user = False if found[0].name == "": if project != None: rp.name = project else : raise service_error(\ service_error.server_config, "Project matched when no project given") elif found[0].name == "": rp.name = None dyn_proj = True else: rp.name = found[0].name rp.node_types = found[0].node_types; if found[1] == "": if user_match == "": if user != None: ru = user[0] else: raise service_error(\ service_error.server_config, "Matched on anonymous request") else: ru = user_match elif found[1] == "": ru = None dyn_user = True return (rp, ru), (dyn_user, dyn_proj) def build_response(self, alloc_id, ap): """ Create the SOAP response. Build the dictionary description of the response and use fedd_utils.pack_soap to create the soap message. NB that alloc_id is a fedd_services_types.IDType_Holder pulled from the incoming message. ap is the allocate project message returned from a remote project allocation (even if that allocation was done locally). """ # Because alloc_id is already a fedd_services_types.IDType_Holder, # there's no need to repack it msg = { 'allocID': alloc_id, 'emulab': { 'domain': self.domain, 'boss': self.boss, 'ops': self.ops, 'fileServer': self.fileserver, 'eventServer': self.eventserver, 'project': ap['project'] }, } if len(self.attrs) > 0: msg['emulab']['fedAttr'] = \ [ { 'attribute': x, 'value' : y } \ for x,y in self.attrs.iteritems()] return msg def RequestAccess(self, req, fid): if req.has_key('RequestAccessRequestBody'): req = req['RequestAccessRequestBody'] else: raise service_error(service_error.req, "No request!?") if req.has_key('destinationTestbed'): dt = unpack_id(req['destinationTestbed']) if dt == None or dt == self.testbed: # Request for this fedd found, dyn = self.lookup_access(req, fid) restricted = None ap = None # Check for access to restricted nodes if req.has_key('resources') and req['resources'].has_key('node'): resources = req['resources'] restricted = [ t for n in resources['node'] \ if n.has_key('hardware') \ for t in n['hardware'] \ if t in self.restricted ] inaccessible = [ t for t in restricted \ if t not in found[0].node_types] if len(inaccessible) > 0: raise service_error(service_error.access, "Access denied (nodetypes %s)" % \ str(', ').join(inaccessible)) ssh = [ x['sshPubkey'] \ for x in req['access'] if x.has_key('sshPubkey')] if len(ssh) > 0: if dyn[1]: # Compose the dynamic project request # (only dynamic, dynamic currently allowed) preq = { 'AllocateProjectRequestBody': \ { 'project' : {\ 'user': [ \ { 'access': [ { 'sshPubkey': s } ] } \ for s in ssh ] \ }\ }\ } if restricted != None and len(restricted) > 0: preq['AllocateProjectRequestBody']['resources'] = \ [ {'node': { 'hardware' : [ h ] } } \ for h in restricted ] ap = self.allocate_project.dynamic_project(preq) else: # XXX ssh key additions ap = { 'project': \ { 'name' : { 'localname' : found[0].name },\ 'user' : [ {\ 'userID': { 'localname' : found[1] }, \ 'access': [ { 'sshPubkey': s } for s in ssh]}\ ]\ }\ } else: raise service_error(service_error.req, "SSH access parameters required") resp = self.build_response(req['allocID'], ap) return resp else: p_fault = None # Any SOAP failure (sent unless XMLRPC works) try: # Proxy the request using SOAP return self.proxy_request(dt, req) except service_error, e: if e.code == service_error.proxy: p_fault = None else: raise except ZSI.FaultException, f: p_fault = f.fault.detail[0] # If we could not get a valid SOAP response to the request above, # try the same address using XMLRPC and let any faults flow back # out. if p_fault == None: return self.proxy_xmlrpc_request(dt, req) else: # Build the fault body = p_fault.get_element_FeddFaultBody() if body != None: raise service_error(body.get_element_code(), body.get_element_desc()); else: raise service_error(\ service_error.proxy, "Undefined fault from proxy??"); def get_soap_services(self): return self.soap_handlers def get_xmlrpc_services(self): return self.xmlrpc_handlers