######################################################################## # File: RequestValidator.py # Author: Krzysztof.Ciba@NOSPAMgmail.com # Date: 2012/09/18 07:55:16 ######################################################################## """ :mod: RequestValidator ====================== .. module: RequestValidator :synopsis: request validator .. moduleauthor:: Krzysztof.Ciba@NOSPAMgmail.com A general and simple request validator checking for required attributes and logic. It checks if required attributes are set/unset but not for their values. RequestValidator class implements the DIRACSingleton pattern, no global object is required to keep a single instance. If you need to extend this one with your own specific checks consider: * for adding Operation or Files required attributes use :any:`addReqAttrsCheck` function:: RequestValidator().addReqAttrsCheck( "FooOperation", operationAttrs = [ "Bar", "Buzz"], filesAttrs = [ "LFN" ] ) * for adding generic check define a new callable object ( function or functor ) which takes only one argument, say for functor:: class MyValidator( RequestValidator ): @staticmethod def hasFoo( request ): if not request.Foo: return S_ERROR("Foo not set") return S_OK() * or function:: def hasBar( request ): if not request.Bar: return S_ERROR("Bar not set") return S_OK() and add this one to the validators set by calling `RequestValidator().addValidator`, i.e.:: RequestValidator().addValidator( MyValidator.hasFoo ) RequestValidator().addValidator( hasFoo ) Notice that all validators should always return S_ERROR/S_OK, no exceptions from that whatsoever! """ from __future__ import absolute_import from __future__ import division from __future__ import print_function __RCSID__ = "5c7bcc34c (2021-10-20 17:55:20 +0200) Christophe Haen " # # # @file RequestValidator.py # @author Krzysztof.Ciba@NOSPAMgmail.com # @date 2012/09/18 07:55:37 # @brief Definition of RequestValidator class. # # import import inspect import six # # from DIRAC from DIRAC import S_OK, S_ERROR, gConfig, gLogger from DIRAC.Core.Security.Properties import FULL_DELEGATION, LIMITED_DELEGATION from DIRAC.Core.Utilities.DIRACSingleton import DIRACSingleton from DIRAC.ConfigurationSystem.Client import PathFinder ######################################################################## @six.add_metaclass(DIRACSingleton) class RequestValidator(object): """ .. class:: RequestValidator This class validates newly created requests (before saving them in RequestDB) for required attributes. """ # # dict with required attrs reqAttrs = { "ForwardDISET": {"Operation": ["Arguments"], "Files": []}, "PutAndRegister": {"Operation": ["TargetSE"], "Files": ["LFN", "PFN"]}, "ReplicateAndRegister": {"Operation": ["TargetSE"], "Files": ["LFN"]}, "PhysicalRemoval": {"Operation": ["TargetSE"], "Files": ["LFN"]}, "RemoveFile": {"Operation": [], "Files": ["LFN"]}, "RemoveReplica": {"Operation": ["TargetSE"], "Files": ["LFN"]}, "ReTransfer": { "Operation": ["TargetSE"], "Files": ["LFN", "PFN"], }, "RegisterFile": { "Operation": [], "Files": ["LFN", "PFN", "ChecksumType", "Checksum", "GUID"], }, "RegisterReplica": { "Operation": ["TargetSE"], "Files": ["LFN", "PFN"], }, } # All the operationHandlers defined in the CS opHandlers = set() def __init__(self): """c'tor just setting validation order """ self.validator = ( self._hasRequestName, self._hasOwner, self._hasOperations, self._hasType, self._hasFiles, self._hasRequiredAttrs, self._hasChecksumAndChecksumType, ) configPath = PathFinder.getAgentSection("RequestManagement/RequestExecutingAgent") # # operation handlers over here opHandlersPath = "%s/%s" % (configPath, "OperationHandlers") opHandlers = gConfig.getSections(opHandlersPath) if not opHandlers["OK"]: gLogger.error(opHandlers["Message"]) else: self.opHandlers = set(opHandlers["Value"]) @classmethod def addReqAttrsCheck(cls, operationType, operationAttrs=None, filesAttrs=None): """add required attributes of Operation of type :operationType: :param str operationType: Operation.Type :param operationAttrs: required Operation attributes :type operationAttrs: python:list :param filesAttrs: required Files attributes :type filesAttrs: python:list """ toUpdate = {"Operation": operationAttrs if operationAttrs else [], "Files": filesAttrs if filesAttrs else []} if operationType not in cls.reqAttrs: cls.reqAttrs[operationType] = {"Operation": [], "Files": []} for key, attrList in cls.reqAttrs[operationType].items(): cls.reqAttrs[operationType][key] = list(set(attrList + toUpdate[key])) @classmethod def addValidator(cls, fcnObj): """add `fcnObj` validator""" if not callable(fcnObj): return S_ERROR("supplied argument is not callable") args = inspect.getargspec(fcnObj).args if len(args) not in (1, 2): return S_ERROR("wrong number of arguments for supplied function object") cls.validator = cls.validator + tuple( fcnObj, ) return S_OK() def validate(self, request): """validation of a given `request` :param ~Request.Request request: Request instance """ for validator in self.validator: isValid = validator(request) if not isValid["OK"]: return isValid # # if we're here request is more or less valid return S_OK() @staticmethod def _hasDIRACSetup(request): """required attribute - DIRACSetup""" if not request.DIRACSetup: return S_ERROR("DIRACSetup not set") return S_OK() @staticmethod def _hasOwner(request): """required attributes OwnerDn and OwnerGroup""" if not request.OwnerDN: return S_ERROR("Request '%s' is missing OwnerDN value" % request.RequestName) if not request.OwnerGroup: return S_ERROR("Request '%s' is missing OwnerGroup value" % request.RequestName) return S_OK() @staticmethod def _hasRequestName(request): """required attribute: RequestName""" if not request.RequestName: return S_ERROR("RequestName not set") return S_OK() @staticmethod def _hasOperations(request): """at least one operation is in""" if not len(request): return S_ERROR("Operations not present in request '%s'" % request.RequestName) return S_OK() @staticmethod def _hasType(request): """operation type is set""" for operation in request: if not operation.Type: return S_ERROR( "Operation #%d in request '%s' hasn't got Type set" % (request.indexOf(operation), request.RequestName) ) return S_OK() @classmethod def _hasFiles(cls, request): """check for files presence""" for operation in request: if operation.Type not in cls.reqAttrs: return S_OK() if cls.reqAttrs[operation.Type]["Files"] and not len(operation): return S_ERROR( "Operation #%d of type '%s' hasn't got files to process." % (request.indexOf(operation), operation.Type) ) if not cls.reqAttrs[operation.Type]["Files"] and len(operation): return S_ERROR( "Operation #%d of type '%s' has got files to process." % (request.indexOf(operation), operation.Type) ) return S_OK() @classmethod def _hasRequiredAttrs(cls, request): """check required attributes for operations and files""" for operation in request: if operation.Type in cls.reqAttrs: opAttrs = cls.reqAttrs[operation.Type]["Operation"] for opAttr in opAttrs: if not getattr(operation, opAttr): return S_ERROR( "Operation #%d of type '%s' is missing %s attribute." % (request.indexOf(operation), operation.Type, opAttr) ) fileAttrs = cls.reqAttrs[operation.Type]["Files"] for opFile in operation: for fileAttr in fileAttrs: if not getattr(opFile, fileAttr): return S_ERROR( "Operation #%d of type '%s' is missing %s attribute for file." % (request.indexOf(operation), operation.Type, fileAttr) ) return S_OK() @classmethod def _hasChecksumAndChecksumType(cls, request): """Checksum and ChecksumType should be specified""" for operation in request: for opFile in operation: if any([opFile.Checksum, opFile.ChecksumType]) and not all([opFile.Checksum, opFile.ChecksumType]): return S_ERROR( "File in operation #%d is missing Checksum (%s) or ChecksumType (%s)" % (request.indexOf(operation), opFile.Checksum, opFile.ChecksumType) ) return S_OK() def _hasExistingOperationTypes(self, request): """Check that there is a handler defined in the CS for each operation type""" requiredHandlers = set([op.Type for op in request]) nonExistingHandlers = requiredHandlers - self.opHandlers if nonExistingHandlers: return S_ERROR( "The following operation type(s) have no handlers defined in the CS: %s" % nonExistingHandlers ) return S_OK() @staticmethod def setAndCheckRequestOwner(request, remoteCredentials): """ CAUTION: meant to be called on the server side. (does not make much sense otherwise) Sets the ownerDN and ownerGroup of the Request from the client's credentials. If they are already set, make sure the client is allowed to do so (FULL_DELEGATION or LIMITED_DELEGATION). This is the case of pilots or the RequestExecutingAgent :param request: the request to test :param remoteCredentials: credentials from the clients :returns: True if everything is fine, False otherwise """ credDN = remoteCredentials["DN"] credGroup = remoteCredentials["group"] credProperties = remoteCredentials["properties"] # If the owner or the group was not set, we use the one of the credentials if not request.OwnerDN or not request.OwnerGroup: request.OwnerDN = credDN request.OwnerGroup = credGroup return True # From here onward, we expect the ownerDN/group to already have a value # If the credentials in the Request match those from the credentials, it's OK if request.OwnerDN == credDN and request.OwnerGroup == credGroup: return True # From here, something/someone is putting a request on behalf of someone else # Only allow this if the credentials have Full or Limited delegation properties if FULL_DELEGATION in credProperties or LIMITED_DELEGATION in credProperties: return True return False