Skip to content

Anatomy of a response micro service

Ivan Kanakarakis edited this page Apr 27, 2020 · 11 revisions

SATOSA micro-services

Anatomy of a response micro_service

A micro-service has configuration and code. The configuration is given to the micro-service on initialization. Any validation on the configuration should be done at this point. The micro-service is loaded and awaits execution.

When the right time comes, the micro-service is invoked through the process() function. The process() function receives two parameters: the context of the current request, and data which reflects the internal data representation of the protocol information, as transformed by the corresponding frontend or backend.

The micro-service (most of the time) will add/remove/change data.attributes and in the end it will return the result for the next micro-service to work upon.

Configuration example

module: python.path.example.module_name.NameOfMicroservice
name: An example micro-service skeleton
config:
  capitalize_name: True

Code example

"""A micro-service example."""

import logging

import satosa
from satosa.micro_services.base import ResponseMicroService

from .helpers import ExtraLogger


# see the logging helper, below
logger = ExtraLogger(logging.getLogger(__name__))


class NameOfMicroservice(ResponseMicroService):
    """
    A micro-service to show an example skeleton.

    Example configuration:

    ```yaml
    module: python.path.example.module_name.NameOfMicroservice
    name: An example micro-service skeleton
    config:
        capitalize_name: True
    ```
    """

    def __init__(self, config, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.capitalize_name = config.get("capitalize_name", False)
        logger.debug("initialized the micro-service")

    # context: satosa.context.Context
    # data: satosa.internal.InternalData
    def process(self, context, data):
        logger.bind("session_id", context.state.session_id)

        logger.debug("running the micro-service")

        # do things with data.attributes
        if self.capitalize_name:
            data.attributes["name"] = [
                value.upper() for value in data.attributes.get("name", [])
            ]

            # log a message
            logger.info("name has been capitilized")

        logger.debug("end of the micro-service")
        # return updated context and data
        return super().process(context, data)

And a logging helper, separately, to avoid duplication in every micro-service. This may become part of core in the future.

from logging import LoggerAdapter


class ExtraLogger(LoggerAdapter):
    def __init__(self, logger, extra={}):
        super().__init__(logger, extra)

    def bind(self, key, value):
        self.extra[key] = value

    def process(self, msg, kwargs):
        extra = " ".join(
            "[{key}: {value}]".format(key=key, value=value)
            for key, value in self.extra.items()
        )
        logline = "{extra} {msg}".format(extra=extra, msg=msg).lstrip()
        return logline, kwargs

Test example

from unittest import TestCase

from satosa.context import Context
from satosa.state import State
from satosa.internal import AuthenticationInformation
from satosa.internal import InternalData

from python.path.example.module_name import NameOfMicroservice


class NameOfMicroserviceTests(TestCase):
    def setUp(self):
        config = {
            "capitalize_name": True
        }

        plugin = NameOfMicroservice(config, "name", "url")
        plugin.next = lambda context, data: (context, data)

        context = Context()
        context.state = State()
        data = InternalData(
            auth_info=AuthenticationInformation(
                issuer="entity-id-of-issuer",
                auth_class_ref="urn:oasis:names:tc:SAML:2.0:ac:classes:Password",
            ),
            requester="entity-id-of-service",
            requester_name=[{"text": "name of service", "lang": "en"}],
            subject_id="the-subject-identifier",
            subject_type="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
            attributes={
                "edupersontargetedid": ["an-edupersontargetedid-value"],
                "displayname": ["SomeName SomeSurname"],
                "givenname": ["SomeGivenName"],
                "mail": ["someone@example.org"],
                "name": ["SomeName"],
                "surname": ["SomeSurname"],
                "epsa": ["someone@example.org", "staff@example.org"],
                "eppn": ["an-eppn-value@example.org"],
            },
        )

        self.context = context
        self.data = data
        self.plugin = plugin

    def test_should_capitalize(self):
        new_context, new_data = self.plugin.process(self.context, self.data)
        expected = [value.upper() for value in self.data.attributes["name"]]
        self.assertEqual(new_data.attributes["name"], expected)

    def test_should_not_capitalize(self):
        self.plugin.capitalize_name = False
        new_context, new_data = self.plugin.process(self.context, self.data)
        expected = self.data.attributes["name"]
        self.assertEqual(new_data.attributes["name"], expected)

Example content of data param

# type: satosa.internal.InternalData
data = {
    # type: satosa.internal.AuthenticationInformation
    # This part holds the authentication information
    # namely, the authentication context classes (ie, the LoA reference)
    # and the issuer of the given identity (ie, the IdP entity-id)
    "auth_info": {
        "auth_class_ref": "urn:oasis:names:tc:SAML:2.0:ac:classes:Password",
        "timestamp": "2020-02-22T19:30:04Z",
        "issuer": "https://www.rediris.es/sir/umaidp",
    },

    # the requester of the authentication; this is typically the SP (or RP) entity-id
    "requester": "https://example.org/saml-sp/metadata.xml",
    "requester_name": [{"text": None, "lang": "en"}],

    # the subject identifier as expressed in different protocols, along with its type or format.
    # this will match `NameID` for SAML2 and `sub` for OIDC.
    "subject_id": "69e83a116ed953279999d4463541c2799795c816",
    "subject_type": "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",

    # the attributes of the subject as expressed in different protocols.
    # this will be a representation of claims for OIDC,
    # or attribute statements of an assertion for SAML2.
    "attributes": {
        "edupersontargetedid": ["04e8192d48a040f3ef495999958908f8aa10b4a5"],
        "displayname": ["Somename Somesurname"],
        "givenname": ["Somename"],
        "mail": ["someone@uma.es"],
        "name": ["Somename"],
        "surname": ["Somesurname"],
        "epsa": ["alum@uma.es", "staff@uma.es"],
        "eppn": ["06100004X@uma.es"],
        "spuc": [
            "urn:schac:personalUniqueCode:es:rediris:sir:mbid:{sha1}0c938d124632017100980299997b1ab174789657",
            "urn:schac:personalUniqueCode:es:uma:CAU:id:822",
            "urn:schac:personalUniqueCode:es:uma:ESC:code:a98b1a8c-9215-11e9-8545-000077349997",
            "urn:schac:personalUniqueCode:es:uma:codUni:06100004X",
        ],
    },

    # this has been added by another micro-service; namely metainfo
    # the role of that micro-service is to collect metadata information about entities
    # and present it in a unified way.
    # other micro-services can lookup the metadata of an entity using this structure.
    "metadata": {
        "https://www.rediris.es/sir/umaidp": {
            "display_name": "University of Malaga",
            "privacy_statement": None,
            "contacts": [
                {
                    "contact_type": "technical",
                    "given_name": "SIR helpdesk",
                    "email_address": ["mailto:sir@rediris.es"],
                },
                {
                    "contact_type": "other",
                    "given_name": "RedIRIS SIRTFI-CSIRT Team",
                    "email_address": ["mailto:sirtfi-csirt@rediris.es"],
                },
            ],
            "entity_categories": [],
            "supported_entity_categories": [
                "http://refeds.org/category/research-and-scholarship"
            ],
            "assurance_certifications": ["https://refeds.org/sirtfi"],
        },
        "https://example.org/saml-sp/metadata.xml": {
            "display_name": None,
            "privacy_statement": None,
            "contacts": [],
            "entity_categories": [],
            "supported_entity_categories": [],
            "assurance_certifications": [],
        },
    }
}

Example content of context param

context = {
    "cookie": 'SATOSA_PROXY_STATE="..."',
    "internal_data": {
        "metadata_store": <saml2.mdstore.MetadataStore object at 0x7fc60e32cb70>
    },
    "request": None,
    "request_authorization": "",

    # type: satosa.state.State
    "state": {
        "ROUTER": "Saml2IDP",
        "SATOSA_BASE": {"requester": "..."},
        "Saml2IDP": {
            "relay_state": "https://example.org/authenticate?as=some-sp",
            "resp_args": {
                "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
                "destination": "https://example.org/saml-sp/saml2-acs",
                "in_response_to": "_e923792fde97aa0b6bc82999cba5274a61eae4b96c",
                "name_id_policy": """<ns0:NameIDPolicy xmlns:ns0="urn:oasis:names:tc:SAML:2.0:protocol" AllowCreate="true" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient"/>""",
                "sp_entity_id": "...",
            },
        },
        "memorized_idp": "...",
    },

    "target_backend": "saml2sp",
    "target_frontend": None,
    "target_micro_service": None,
}