|
| 1 | +import re |
| 2 | +import pystache |
| 3 | + |
| 4 | +from .base import ResponseMicroService |
| 5 | +from ..util import get_dict_defaults |
| 6 | + |
| 7 | +class MustachAttrValue(object): |
| 8 | + def __init__(self, attr_name, values): |
| 9 | + self._attr_name = attr_name |
| 10 | + self._values = values |
| 11 | + if any(['@' in v for v in values]): |
| 12 | + local_parts = [] |
| 13 | + domain_parts = [] |
| 14 | + scopes = dict() |
| 15 | + for v in values: |
| 16 | + (local_part, sep, domain_part) = v.partition('@') |
| 17 | + # probably not needed now... |
| 18 | + local_parts.append(local_part) |
| 19 | + domain_parts.append(domain_part) |
| 20 | + scopes[domain_part] = True |
| 21 | + self._scopes = list(scopes.keys()) |
| 22 | + else: |
| 23 | + self._scopes = None |
| 24 | + |
| 25 | + def __str__(self): |
| 26 | + return ";".join(self._values) |
| 27 | + |
| 28 | + @property |
| 29 | + def values(self): |
| 30 | + [{self._attr_name: v} for v in self._values] |
| 31 | + |
| 32 | + @property |
| 33 | + def value(self): |
| 34 | + if len(self._values) == 1: |
| 35 | + return self._values[0] |
| 36 | + else: |
| 37 | + return self._values |
| 38 | + |
| 39 | + @property |
| 40 | + def first(self): |
| 41 | + if len(self._values) > 0: |
| 42 | + return self._values[0] |
| 43 | + else: |
| 44 | + return "" |
| 45 | + |
| 46 | + @property |
| 47 | + def scope(self): |
| 48 | + if self._scopes is not None: |
| 49 | + return self._scopes[0] |
| 50 | + return "" |
| 51 | + |
| 52 | + |
| 53 | +class AddSyntheticAttributes(ResponseMicroService): |
| 54 | + """ |
| 55 | +A class that add generated or synthetic attributes to a response set. Attribute |
| 56 | +generation is done using mustach (http://mustache.github.io) templates. The |
| 57 | +following example configuration illustrates most common features: |
| 58 | +
|
| 59 | +```yaml |
| 60 | +module: satosa.micro_services.attribute_generation.AddSyntheticAttributes |
| 61 | +name: AddSyntheticAttributes |
| 62 | +config: |
| 63 | + synthetic_attributes: |
| 64 | + target_provider1: |
| 65 | + requester1: |
| 66 | + eduPersonAffiliation: member;employee |
| 67 | + default: |
| 68 | + default: |
| 69 | + schacHomeOrganization: {{eduPersonPrincipalName.scope}} |
| 70 | + schacHomeOrganizationType: tomfoolery provider |
| 71 | +
|
| 72 | +``` |
| 73 | +
|
| 74 | +The use of "" and 'default' is synonymous. Attribute rules are not |
| 75 | +overloaded or inherited. For instance a response from "target_provider1" |
| 76 | +and requester1 in the above config will generate a (static) attribute |
| 77 | +set of 'member' and 'employee' for the eduPersonAffiliation attribute |
| 78 | +and nothing else. Note that synthetic attributes override existing |
| 79 | +attributes if present. |
| 80 | +
|
| 81 | +*Evaluating and interpreting templates* |
| 82 | +
|
| 83 | +Attribute values are split on combinations of ';' and newline so that |
| 84 | +a template resulting in the following text: |
| 85 | +``` |
| 86 | +a; |
| 87 | +b;c |
| 88 | +``` |
| 89 | +results in three attribute values: 'a','b' and 'c'. Templates are |
| 90 | +evaluated with a single context that represents the response attributes |
| 91 | +before the microservice is processed. De-referencing the attribute |
| 92 | +name as in '{{name}}' results in a ';'-separated list of all attribute |
| 93 | +values. This notation is useful when you know there is only a single |
| 94 | +attribute value in the set. |
| 95 | +
|
| 96 | +*Special contexts* |
| 97 | +
|
| 98 | +For treating the values as a list - eg for interating using mustach, |
| 99 | +use the .values sub-context For instance to synthesize all first-last |
| 100 | +name combinations do this: |
| 101 | +
|
| 102 | +``` |
| 103 | +{{#givenName.values}} |
| 104 | + {{#sn.values}}{{givenName}} {{sn}}{{/sn.values}} |
| 105 | +{{/givenName.values}} |
| 106 | +``` |
| 107 | +
|
| 108 | +Note that the .values sub-context behaves as if it is an iterator |
| 109 | +over single-value context with the same key name as the original |
| 110 | +attribute name. |
| 111 | +
|
| 112 | +The .scope sub-context evalues to the right-hand part of any @ |
| 113 | +sign. This is assumed to be single valued. |
| 114 | +
|
| 115 | +The .first sub-context evalues to the first value of a context |
| 116 | +which may be safer to use if the attribute is multivalued but |
| 117 | +you don't care which value is used in a template. |
| 118 | + """ |
| 119 | + |
| 120 | + def __init__(self, config, *args, **kwargs): |
| 121 | + super().__init__(*args, **kwargs) |
| 122 | + self.synthetic_attributes = config["synthetic_attributes"] |
| 123 | + |
| 124 | + def _synthesize(self, attributes, requester, provider): |
| 125 | + syn_attributes = dict() |
| 126 | + context = dict() |
| 127 | + |
| 128 | + for attr_name,values in attributes.items(): |
| 129 | + context[attr_name] = MustachAttrValue(attr_name, values) |
| 130 | + |
| 131 | + recipes = get_dict_defaults(self.synthetic_attributes, requester, provider) |
| 132 | + for attr_name, fmt in recipes.items(): |
| 133 | + syn_attributes[attr_name] = [v.strip().strip(';') for v in re.split("[;\n]+", pystache.render(fmt, context))] |
| 134 | + return syn_attributes |
| 135 | + |
| 136 | + def process(self, context, data): |
| 137 | + data.attributes.update(self._synthesize(data.attributes, data.requester, data.auth_info.issuer)) |
| 138 | + return super().process(context, data) |
0 commit comments