From: Tony Mack Date: Wed, 8 Apr 2009 01:28:53 +0000 (+0000) Subject: reorganizing code. moving some things out of the server interface classes and into... X-Git-Tag: sfa-0.9-0@14641~519 X-Git-Url: http://git.onelab.eu/?a=commitdiff_plain;h=294d13d0b6c77497f97558f718d81ef6656f30a7;p=sfa.git reorganizing code. moving some things out of the server interface classes and into there own classes --- diff --git a/geni/util/api.py b/geni/util/api.py new file mode 100644 index 00000000..14008da9 --- /dev/null +++ b/geni/util/api.py @@ -0,0 +1,363 @@ +# +# Geniwrapper XML-RPC and SOAP interfaces +# +# + +import sys +import os +import traceback +import string +import xmlrpclib +from geni.util.auth import Auth +from geni.util.config import * +from geni.util.faults import * +from geni.util.debug import * + +# See "2.2 Characters" in the XML specification: +# +# #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] +# avoiding +# [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF] + +invalid_xml_ascii = map(chr, range(0x0, 0x8) + [0xB, 0xC] + range(0xE, 0x1F)) +xml_escape_table = string.maketrans("".join(invalid_xml_ascii), "?" * len(invalid_xml_ascii)) + +def xmlrpclib_escape(s, replace = string.replace): + """ + xmlrpclib does not handle invalid 7-bit control characters. This + function augments xmlrpclib.escape, which by default only replaces + '&', '<', and '>' with entities. + """ + + # This is the standard xmlrpclib.escape function + s = replace(s, "&", "&") + s = replace(s, "<", "<") + s = replace(s, ">", ">",) + + # Replace invalid 7-bit control characters with '?' + return s.translate(xml_escape_table) + +def xmlrpclib_dump(self, value, write): + """ + xmlrpclib cannot marshal instances of subclasses of built-in + types. This function overrides xmlrpclib.Marshaller.__dump so that + any value that is an instance of one of its acceptable types is + marshalled as that type. + + xmlrpclib also cannot handle invalid 7-bit control characters. See + above. + """ + + # Use our escape function + args = [self, value, write] + if isinstance(value, (str, unicode)): + args.append(xmlrpclib_escape) + + try: + # Try for an exact match first + f = self.dispatch[type(value)] + except KeyError: + raise + # Try for an isinstance() match + for Type, f in self.dispatch.iteritems(): + if isinstance(value, Type): + f(*args) + return + raise TypeError, "cannot marshal %s objects" % type(value) + else: + f(*args) + +# You can't hide from me! +xmlrpclib.Marshaller._Marshaller__dump = xmlrpclib_dump + +# SOAP support is optional +try: + import SOAPpy + from SOAPpy.Parser import parseSOAPRPC + from SOAPpy.Types import faultType + from SOAPpy.NS import NS + from SOAPpy.SOAPBuilder import buildSOAP +except ImportError: + SOAPpy = None + +import geni.methods + +def import_deep(name): + mod = __import__(name) + components = name.split('.') + for comp in components[1:]: + mod = getattr(mod, comp) + return mod + +class GeniAPI: + + # flat list of method names + methods = geni.methods.methods + + def __init__(self, config = "/usr/share/geniwrapper/geni/util/geni_config", encoding = "utf-8", peer_cert = None, interface = None): + self.encoding = encoding + + # Better just be documenting the API + if config is None: + return + + # Load configuration + self.config = Config(config) + self.auth = Auth(peer_cert) + self.interface = interface + self.plshell = self.getPLCShell() + self.basedir = self.config.GENI_BASE_DIR + os.sep + self.server_basedir = self.basedir + os.sep + "geni" + os.sep + self.hrn = self.config.GENI_INTERFACE_HRN + + + def getPLCShell(self): + self.plauth = {'Username': self.config.GENI_PLC_USER, + 'AuthMethod': 'password', + 'AuthString': self.config.GENI_PLC_PASSWORD} + try: + import PLC.Shell + shell = PLC.Shell.Shell(globals = globals()) + shell.AuthCheck(self.plauth) + return shell + except ImportError: + # connect via xmlrpc + plc_host = self.config.GENI_PLC_HOST + plc_port = self.config.GENI_PLC_PORT + plc_api_path = self.config.GENI_PLC_API_PATH + url = "https://%(plc_host)s:%(plc_port)s/%(plc_api_path)s/" % \ + locals() + + shell = xmlrpclib.Server(url, verbose = 0, allow_none = True) + shell.AuthCheck(self.plauth) + return shell + + def fill_record_pl_info(self, record): + """ + Fill in the planetlab specific fields of a Geni record. This + involves calling the appropraite PLC method to retrie the + dtabase record for the object. + + PLC data is filled into the pl_fino field of the record. + + @param record record to fill in field (in/out param) + """ + type = record.get_type() + pointer = record.get_pointer() + + # records with pointer==-1 do not have plc info associated with them. + # for example, the top level authority records which are + # authorities, but not PL "sites" + if pointer == -1: + record.set_pl_info({}) + return + + if (type == "sa") or (type == "ma"): + pl_res = self.plshell.GetSites(self.plauth, [pointer]) + elif (type == "slice"): + pl_res = self.plshell.GetSlices(self.plauth, [pointer]) + elif (type == "user"): + pl_res = self.plshell.GetPersons(self.plauth, [pointer]) + key_ids = pl_res[0]['key_ids'] + keys = self.plshell.GetKeys(self.plauth, key_ids) + pubkeys = [] + if keys: + pubkeys = [key['key'] for key in keys] + pl_res[0]['keys'] = pubkeys + elif (type == "node"): + pl_res = self.plshell.GetNodes(self.plauth, [pointer]) + else: + raise UnknownGeniType(type) + + if not pl_res: + # the planetlab record no longer exists + # TODO: delete the geni record ? + raise PlanetLabRecordDoesNotExist(record.get_name()) + + record.set_pl_info(pl_res[0]) + + + def lookup_users(self, auth_table, user_id_list, role="*"): + record_list = [] + for person_id in user_id_list: + user_records = auth_table.find("user", person_id, "pointer") + for user_record in user_records: + self.fill_record_info(user_record) + + user_roles = user_record.get_pl_info().get("roles") + if (role=="*") or (role in user_roles): + record_list.append(user_record.get_name()) + return record_list + + def fill_record_geni_info(self, record): + geni_info = {} + type = record.get_type() + + if (type == "slice"): + auth_table = self.auth.get_auth_table(self.auth.get_authority(record.get_name())) + person_ids = record.pl_info.get("person_ids", []) + researchers = self.lookup_users(auth_table, person_ids) + geni_info['researcher'] = researchers + + elif (type == "sa"): + auth_table = self.auth.get_auth_table(record.get_name()) + person_ids = record.pl_info.get("person_ids", []) + pis = self.lookup_users(auth_table, person_ids, "pi") + geni_info['pi'] = pis + # TODO: OrganizationName + + elif (type == "ma"): + auth_table = self.auth.get_auth_table(record.get_name()) + person_ids = record.pl_info.get("person_ids", []) + operators = self.lookup_users(auth_table, person_ids, "tech") + geni_info['operator'] = operators + # TODO: OrganizationName + + auth_table = self.auth.get_auth_table(record.get_name()) + person_ids = record.pl_info.get("person_ids", []) + owners = self.lookup_users(auth_table, person_ids, "admin") + geni_info['owner'] = owners + + elif (type == "node"): + geni_info['dns'] = record.pl_info.get("hostname", "") + # TODO: URI, LatLong, IP, DNS + + elif (type == "user"): + geni_info['email'] = record.pl_info.get("email", "") + # TODO: PostalAddress, Phone + + record.set_geni_info(geni_info) + + def fill_record_info(self, record): + """ + Given a geni record, fill in the PLC specific and Geni specific + fields in the record. + """ + self.fill_record_pl_info(record) + self.fill_record_geni_info(record) + + def update_membership_list(self, oldRecord, record, listName, addFunc, delFunc): + # get a list of the HRNs tht are members of the old and new records^M + if oldRecord: + if oldRecord.pl_info == None: + oldRecord.pl_info = {} + oldList = oldRecord.get_geni_info().get(listName, []) + else: + oldList = [] + newList = record.get_geni_info().get(listName, []) + + # if the lists are the same, then we don't have to update anything + if (oldList == newList): + return + + # build a list of the new person ids, by looking up each person to get + # their pointer + newIdList = [] + for hrn in newList: + userRecord = self.resolve_raw("user", hrn)[0] + newIdList.append(userRecord.get_pointer()) + + # build a list of the old person ids from the person_ids field of the + # pl_info + if oldRecord: + oldIdList = oldRecord.plinfo.get("person_ids", []) + containerId = oldRecord.get_pointer() + else: + # if oldRecord==None, then we are doing a Register, instead of an + # update. + oldIdList = [] + containerId = record.get_pointer() + + # add people who are in the new list, but not the oldList + for personId in newIdList: + if not (personId in oldIdList): + print "adding id", personId, "to", record.get_name() + addFunc(self.plauth, personId, containerId) + + # remove people who are in the old list, but not the new list + for personId in oldIdList: + if not (personId in newIdList): + print "removing id", personId, "from", record.get_name() + delFunc(self.plauth, personId, containerId) + + def update_membership(self, oldRecord, record): + if record.type == "slice": + self.update_membership_list(oldRecord, record, 'researcher', + self.plshell.AddPersonToSlice, + self.plshell.DeletePersonFromSlice) + elif record.type == "sa": + # TODO + pass + elif record.type == "ma": + # TODO + pass + + + def callable(self, method): + """ + Return a new instance of the specified method. + """ + # Look up method + if method not in self.methods: + raise GeniInvalidAPIMethod, method + + # Get new instance of method + try: + classname = method.split(".")[-1] + module = __import__("geni.methods." + method, globals(), locals(), [classname]) + callablemethod = getattr(module, classname)(self) + return getattr(module, classname)(self) + except ImportError, AttributeError: + raise GeniInvalidAPIMethod, method + + def call(self, source, method, *args): + """ + Call the named method from the specified source with the + specified arguments. + """ + function = self.callable(method) + function.source = source + return function(*args) + + def handle(self, source, data): + """ + Handle an XML-RPC or SOAP request from the specified source. + """ + + # Parse request into method name and arguments + try: + interface = xmlrpclib + (args, method) = xmlrpclib.loads(data) + methodresponse = True + except Exception, e: + if SOAPpy is not None: + interface = SOAPpy + (r, header, body, attrs) = parseSOAPRPC(data, header = 1, body = 1, attrs = 1) + method = r._name + args = r._aslist() + # XXX Support named arguments + else: + raise e + + try: + result = self.call(source, method, *args) + except Exception, fault: + traceback.print_exc(file = log) + # Handle expected faults + if interface == xmlrpclib: + result = fault + methodresponse = None + elif interface == SOAPpy: + result = faultParameter(NS.ENV_T + ":Server", "Method Failed", method) + result._setDetail("Fault %d: %s" % (fault.faultCode, fault.faultString)) + + # Return result + if interface == xmlrpclib: + if not isinstance(result, GeniFault): + result = (result,) + + data = xmlrpclib.dumps(result, methodresponse = True, encoding = self.encoding, allow_none = 1) + elif interface == SOAPpy: + data = buildSOAP(kw = {'%sResponse' % method: {'Result': result}}, encoding = self.encoding) + + return data diff --git a/geni/util/auth.py b/geni/util/auth.py new file mode 100644 index 00000000..746f0c0b --- /dev/null +++ b/geni/util/auth.py @@ -0,0 +1,197 @@ +# +# GeniAPI authentication +# +# + +import time +from geni.util.faults import * +from geni.util.excep import * +from geni.util.credential import Credential +from geni.util.trustedroot import TrustedRootList +from geni.util.hierarchy import Hierarchy +from geni.util.rights import RightList +from geni.util.genitable import * + + +class Auth: + """ + Credential based authentication + """ + + def __init__(self, peer_cert): + self.peer_cert = peer_cert + self.hierarchy = Hierarchy() + self.trusted_cert_list = TrustedRootList().get_list() + + + + def check(self, cred, operation): + """ + Check the credential against the peer cert (callerGID included + in the credential matches the caller that is connected to the + HTTPS connection, check if the credential was signed by a + trusted cert and check if the credential is allowd to perform + the specified operation. + """ + self.client_cred = Credential(string = cred) + self.client_gid = self.client_cred.get_gid_caller() + self.object_gid = self.client_cred.get_gid_object() + + # make sure the client_gid is not blank + if not self.client_gid: + raise MissingCallerGID(self.client_cred.get_subject()) + + # make sure the client_gid matches client's certificate + peer_cert = self.peer_cert + if not peer_cert.is_pubkey(self.client_gid.get_pubkey()): + raise ConnectionKeyGIDMismatch(self.client_gid.get_subject()) + + # make sure the client is allowed to perform the operation + if operation: + if not self.client_cred.can_perform(operation): + raise InsufficientRights(operation) + + if self.trusted_cert_list: + self.client_cred.verify_chain(self.trusted_cert_list) + if self.client_gid: + self.client_gid.verify_chain(self.trusted_cert_list) + if self.object_gid: + self.object_gid.verify_chain(self.trusted_cert_list) + + return True + + + def get_auth_info(self, auth_hrn): + """ + Given an authority name, return the information for that authority. + This is basically a stub that calls the hierarchy module. + + @param auth_hrn human readable name of authority + """ + + return self.hierarchy.get_auth_info(auth_hrn) + + + def get_auth_table(self, auth_name): + """ + Given an authority name, return the database table for that authority. + If the databse table does not exist, then one will be automatically + created. + + @param auth_name human readable name of authority + """ + auth_info = self.get_auth_info(auth_name) + table = GeniTable(hrn=auth_name, + cninfo=auth_info.get_dbinfo()) + # if the table doesn't exist, then it means we haven't put any records + # into this authority yet. + + if not table.exists(): + print >> log, "Registry: creating table for authority", auth_name + table.create() + + return table + + def veriry_auth_belongs_to_me(self, name): + """ + Verify that an authority belongs to our hierarchy. + This is basically left up to the implementation of the hierarchy + module. If the specified name does not belong, ane exception is + thrown indicating the caller should contact someone else. + + @param auth_name human readable name of authority + """ + + self.get_auth_info(name) + + + def verify_object_belongs_to_me(self, name): + """ + Verify that an object belongs to our hierarchy. By extension, + this implies that the authority that owns the object belongs + to our hierarchy. If it does not an exception is thrown. + + @param name human readable name of object + """ + auth_name = self.get_authority(name) + if not auth_name: + # the root authority belongs to the registry by default? + # TODO: is this true? + return + self.verify_auth_belongs_to_me(auth_name) + + def verify_auth_belongs_to_me(self, name): + # get auth info will throw an exception if the authority doesnt exist + self.get_auth_info(name) + + + def verify_object_permission(self, name): + """ + Verify that the object gid that was specified in the credential + allows permission to the object 'name'. This is done by a simple + prefix test. For example, an object_gid for plc.arizona would + match the objects plc.arizona.slice1 and plc.arizona. + + @param name human readable name to test + """ + object_hrn = self.object_gid.get_hrn() + if object_hrn == name: + return + if name.startswith(object_hrn + "."): + return + raise PermissionError(name) + + def verify_cancreate_credential(self, src_cred, record): + """ + Verify that a user can retrive a particular type of credential. + For slices, the user must be on the researcher list. For SA and + MA the user must be on the pi and operator lists respectively + """ + + type = record.get_type() + cred_object_hrn = src_cred.get_gid_object().get_hrn() + if cred_object_hrn in [self.config.GENI_REGISTRY_ROOT_AUTH]: + return + if type=="slice": + researchers = record.get_geni_info().get("researcher", []) + if not (cred_object_hrn in researchers): + raise PermissionError(cred_object_hrn + " is not in researcher list for " + record.get_name()) + elif type == "sa": + pis = record.get_geni_info().get("pi", []) + if not (cred_object_hrn in pis): + raise PermissionError(cred_object_hrn + " is not in pi list for " + record.get_name()) + elif type == "ma": + operators = record.get_geni_info().get("operator", []) + if not (cred_object_hrn in operators): + raise PermissionError(cred_object_hrn + " is not in operator list for " + record.get_name()) + + def get_leaf(self, hrn): + parts = hrn.split(".") + return ".".join(parts[-1:]) + + def get_authority(self, hrn): + + parts = hrn.split(".") + return ".".join(parts[:-1]) + + def get_auth_type(self, type): + if (type=="slice") or (type=="user") or (type=="sa"): + return "sa" + elif (type=="component") or (type=="ma"): + return "ma" + else: + raise UnknownGeniType(type) + + def hrn_to_pl_slicename(self, hrn): + parts = hrn.split(".") + return parts[-2] + "_" + parts[-1] + + # assuming hrn is the hrn of an authority, return the plc authority name + def hrn_to_pl_authname(self, hrn): + parts = hrn.split(".") + return parts[-1] + + # assuming hrn is the hrn of an authority, return the plc login_base + def hrn_to_pl_login_base(self, hrn): + return self.hrn_to_pl_authname(hrn) + diff --git a/geni/util/faults.py b/geni/util/faults.py new file mode 100644 index 00000000..63327923 --- /dev/null +++ b/geni/util/faults.py @@ -0,0 +1,62 @@ +# +# GeniAPI XML-RPC faults +# +# + +import xmlrpclib + +class GeniFault(xmlrpclib.Fault): + def __init__(self, faultCode, faultString, extra = None): + if extra: + faultString += ": " + extra + xmlrpclib.Fault.__init__(self, faultCode, faultString) + +class GeniInvalidAPIMethod(GeniFault): + def __init__(self, method, role = None, extra = None): + faultString = "Invalid method " + method + if role: + faultString += " for role " + role + GeniFault.__init__(self, 100, faultString, extra) + +class GeniInvalidArgumentCount(GeniFault): + def __init__(self, got, min, max = min, extra = None): + if min != max: + expected = "%d-%d" % (min, max) + else: + expected = "%d" % min + faultString = "Expected %s arguments, got %d" % \ + (expected, got) + GeniFault.__init__(self, 101, faultString, extra) + +class GeniInvalidArgument(GeniFault): + def __init__(self, extra = None, name = None): + if name is not None: + faultString = "Invalid %s value" % name + else: + faultString = "Invalid argument" + GeniFault.__init__(self, 102, faultString, extra) + +class GeniAuthenticationFailure(GeniFault): + def __init__(self, extra = None): + faultString = "Failed to authenticate call" + GeniFault.__init__(self, 103, faultString, extra) + +class GeniDBError(GeniFault): + def __init__(self, extra = None): + faultString = "Database error" + GeniFault.__init__(self, 106, faultString, extra) + +class GeniPermissionDenied(GeniFault): + def __init__(self, extra = None): + faultString = "Permission denied" + GeniFault.__init__(self, 108, faultString, extra) + +class GeniNotImplemented(GeniFault): + def __init__(self, extra = None): + faultString = "Not fully implemented" + GeniFault.__init__(self, 109, faultString, extra) + +class GeniAPIError(GeniFault): + def __init__(self, extra = None): + faultString = "Internal API error" + GeniFault.__init__(self, 111, faultString, extra) diff --git a/geni/util/parameter.py b/geni/util/parameter.py new file mode 100644 index 00000000..2a52ce48 --- /dev/null +++ b/geni/util/parameter.py @@ -0,0 +1,105 @@ +# +# Shared type definitions +# +# Mark Huang +# Copyright (C) 2006 The Trustees of Princeton University +# +# $Id: Parameter.py 5574 2007-10-25 20:33:17Z thierry $ +# + +from types import * +from geni.util.faults import * + +class Parameter: + """ + Typed value wrapper. Use in accepts and returns to document method + parameters. Set the optional and default attributes for + sub-parameters (i.e., dict fields). + """ + + def __init__(self, type, doc = "", + min = None, max = None, + optional = None, + ro = False, + nullok = False): + # Basic type of the parameter. Must be a builtin type + # that can be marshalled by XML-RPC. + self.type = type + + # Documentation string for the parameter + self.doc = doc + + # Basic value checking. For numeric types, the minimum and + # maximum possible values, inclusive. For string types, the + # minimum and maximum possible UTF-8 encoded byte lengths. + self.min = min + self.max = max + + # Whether the sub-parameter is optional or not. If None, + # unknown whether it is optional. + self.optional = optional + + # Whether the DB field is read-only. + self.ro = ro + + # Whether the DB field can be NULL. + self.nullok = nullok + + def type(self): + return self.type + + def __repr__(self): + return repr(self.type) + +class Mixed(tuple): + """ + A list (technically, a tuple) of types. Use in accepts and returns + to document method parameters that may return mixed types. + """ + + def __new__(cls, *types): + return tuple.__new__(cls, types) + + +def python_type(arg): + """ + Returns the Python type of the specified argument, which may be a + Python type, a typed value, or a Parameter. + """ + + if isinstance(arg, Parameter): + arg = arg.type + + if isinstance(arg, type): + return arg + else: + return type(arg) + +def xmlrpc_type(arg): + """ + Returns the XML-RPC type of the specified argument, which may be a + Python type, a typed value, or a Parameter. + """ + + arg_type = python_type(arg) + + if arg_type == NoneType: + return "nil" + elif arg_type == IntType or arg_type == LongType: + return "int" + elif arg_type == bool: + return "boolean" + elif arg_type == FloatType: + return "double" + elif arg_type in StringTypes: + return "string" + elif arg_type == ListType or arg_type == TupleType: + return "array" + elif arg_type == DictType: + return "struct" + elif arg_type == Mixed: + # Not really an XML-RPC type but return "mixed" for + # documentation purposes. + return "mixed" + else: + raise GeniAPIError, "XML-RPC cannot marshal %s objects" % arg_type