-
Notifications
You must be signed in to change notification settings - Fork 123
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.
module: python.path.example.module_name.NameOfMicroservice
name: An example micro-service skeleton
config:
capitalize_name: True
"""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
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)
# 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": [],
},
}
}
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,
}