diff --git a/example/plugins/microservices/attribute_processor.yaml.example b/example/plugins/microservices/attribute_processor.yaml.example new file mode 100644 index 000000000..8d946f684 --- /dev/null +++ b/example/plugins/microservices/attribute_processor.yaml.example @@ -0,0 +1,17 @@ +module: satosa.micro_services.attribute_processor.AttributeProcessor +name: AttributeProcessor +config: + process: + - attribute: gender + processors: + - name: GenderToSchacProcessor + module: satosa.micro_services.processors.gender_processor + - attribute: identifier + processors: + - name: HashProcessor + module: satosa.micro_services.processors.hash_processor + hash_alg: sha256 + salt: abcdef0123456789 + - name: ScopeProcessor + module: satosa.micro_services.processors.scope_processor + scope: example.com diff --git a/src/satosa/micro_services/attribute_processor.py b/src/satosa/micro_services/attribute_processor.py new file mode 100644 index 000000000..1973402b2 --- /dev/null +++ b/src/satosa/micro_services/attribute_processor.py @@ -0,0 +1,73 @@ +import importlib +import json +import logging + +from satosa.exception import SATOSAError +from satosa.logging_util import satosa_logging +from satosa.micro_services.base import ResponseMicroService + + +logger = logging.getLogger(__name__) + +CONFIG_KEY_ROOT = 'process' +CONFIG_KEY_MODULE = 'module' +CONFIG_KEY_CLASSNAME = 'name' +CONFIG_KEY_ATTRIBUTE = 'attribute' +CONFIG_KEY_PROCESSORS = 'processors' + + +class AttributeProcessor(ResponseMicroService): + """ + This microservice enables users to define modules that process internal + attributes and their values. + + Example configuration: + + # file: attribute_processor.yaml + module: satosa.micro_services.attribute_processor.AttributeProcessor + process: + - attribute: gender + - name: GenderToSchacProcessor + module: satosa.micro_services.processors.gender_processor + - attribute: identifier + processors: + - name: HashProcessor + module: satosa.micro_services.processors.hash_processor + hash_alg: sha256 + salt: abcdef0123456789 + - name: ScopeProcessor + module: satosa.micro_services.processors.scope_processor + scope: example + """ + def __init__(self, config, *args, **kwargs): + super().__init__(*args, **kwargs) + self.config = config + self.processes = config[CONFIG_KEY_ROOT] + + def process(self, context, data): + for process in self.processes: + attribute = process[CONFIG_KEY_ATTRIBUTE] + processors = process[CONFIG_KEY_PROCESSORS] + for processor in processors: + module = importlib.import_module(processor[CONFIG_KEY_MODULE]) + module_cls = getattr(module, processor[CONFIG_KEY_CLASSNAME]) + instance = module_cls() + + kwargs = processor.copy() + kwargs.pop(CONFIG_KEY_MODULE) + kwargs.pop(CONFIG_KEY_CLASSNAME) + + try: + instance.process(data, attribute, **kwargs) + except AttributeProcessorWarning as w: + satosa_logging(logger, logging.WARNING, w, context.state) + + return super().process(context, data) + + +class AttributeProcessorWarning(SATOSAError): + pass + + +class AttributeProcessorError(SATOSAError): + pass diff --git a/src/satosa/micro_services/processors/__init__.py b/src/satosa/micro_services/processors/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/satosa/micro_services/processors/base_processor.py b/src/satosa/micro_services/processors/base_processor.py new file mode 100644 index 000000000..ad5eb10b5 --- /dev/null +++ b/src/satosa/micro_services/processors/base_processor.py @@ -0,0 +1,6 @@ +class BaseProcessor(object): + def __init__(self): + pass + + def process(internal_data, attribute, **kwargs): + pass diff --git a/src/satosa/micro_services/processors/gender_processor.py b/src/satosa/micro_services/processors/gender_processor.py new file mode 100644 index 000000000..282625c52 --- /dev/null +++ b/src/satosa/micro_services/processors/gender_processor.py @@ -0,0 +1,25 @@ +from .base_processor import BaseProcessor + +from enum import Enum, unique + + +@unique +class Gender(Enum): + NOT_KNOWN = 0 + MALE = 1 + FEMALE = 2 + NOT_SPECIFIED = 9 + + +class GenderToSchacProcessor(BaseProcessor): + def process(self, internal_data, attribute, **kwargs): + attributes = internal_data.attributes + value = attributes.get(attribute, [None])[0] + + if value: + representation = getattr( + Gender, value.upper().replace(' ', '_'), Gender.NOT_KNOWN) + else: + representation = Gender.NOT_SPECIFIED + + attributes[attribute][0] = str(representation.value) diff --git a/src/satosa/micro_services/processors/hash_processor.py b/src/satosa/micro_services/processors/hash_processor.py new file mode 100644 index 000000000..06ad8f928 --- /dev/null +++ b/src/satosa/micro_services/processors/hash_processor.py @@ -0,0 +1,31 @@ +from ..attribute_processor import AttributeProcessorError +from .base_processor import BaseProcessor + +import hashlib + + +CONFIG_KEY_SALT = 'salt' +CONFIG_DEFAULT_SALT = '' +CONFIG_KEY_HASHALGO = 'hash_algo' +CONFIG_DEFAULT_HASHALGO = 'sha256' + + +class HashProcessor(BaseProcessor): + def process(self, internal_data, attribute, **kwargs): + salt = kwargs.get(CONFIG_KEY_HASHALGO, CONFIG_DEFAULT_SALT) + hash_algo = kwargs.get(CONFIG_KEY_HASHALGO, CONFIG_DEFAULT_HASHALGO) + if hash_algo not in hashlib.algorithms_available: + raise AttributeProcessorError( + "Hash algorithm not supported: {}".format(hash_algo)) + + attributes = internal_data.attributes + value = attributes.get(attribute, [None])[0] + if value is None: + raise AttributeProcessorError( + "No value for attribute: {}".format(attribute)) + + hasher = hashlib.new(hash_algo) + hasher.update(value.encode('utf-8')) + hasher.update(salt.encode('utf-8')) + value_hashed = hasher.hexdigest() + attributes[attribute][0] = value_hashed diff --git a/src/satosa/micro_services/processors/scope_processor.py b/src/satosa/micro_services/processors/scope_processor.py new file mode 100644 index 000000000..81f36c415 --- /dev/null +++ b/src/satosa/micro_services/processors/scope_processor.py @@ -0,0 +1,17 @@ +from ..attribute_processor import AttributeProcessorError +from .base_processor import BaseProcessor + + +CONFIG_KEY_SCOPE = 'scope' +CONFIG_DEFAULT_SCOPE = '' + + +class ScopeProcessor(BaseProcessor): + def process(self, internal_data, attribute, **kwargs): + scope = kwargs.get(CONFIG_KEY_SCOPE, CONFIG_DEFAULT_SCOPE) + if scope is None or scope == '': + raise AttributeProcessorError("No scope set.") + + attributes = internal_data.attributes + value = attributes.get(attribute, [None])[0] + attributes[attribute][0] = value + '@' + scope