Skip to content

Commit 7c2ad61

Browse files
skorandac00kiemon5ter
authored andcommitted
First commit of primary identifier microservice
1 parent 5ecf9d6 commit 7c2ad61

File tree

1 file changed

+203
-0
lines changed

1 file changed

+203
-0
lines changed
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
"""
2+
SATOSA microservice that uses a configured ordered list of
3+
attributes that may be asserted by a SAML IdP to construct
4+
a primary identifier or key for the user and assert it as
5+
the value for a configured attribute, for example uid.
6+
"""
7+
8+
import satosa.micro_services.base
9+
from satosa.logging_util import satosa_logging
10+
from satosa.response import Redirect
11+
12+
import copy
13+
import logging
14+
import urllib.parse
15+
16+
logger = logging.getLogger(__name__)
17+
18+
class PrimaryIdentifier(satosa.micro_services.base.ResponseMicroService):
19+
"""
20+
Use a configured ordered list of attributes to construct a primary
21+
identifier for the user and assert it as a particular configured
22+
attribute. If a primary identifier cannot be found or constructed
23+
handle the error in a configured way that may be to ignore
24+
the error or redirect to an external error handling service.
25+
"""
26+
logprefix = "PRIMARY_IDENTIFIER:"
27+
28+
def __init__(self, config, *args, **kwargs):
29+
super().__init__(*args, **kwargs)
30+
self.config = config
31+
32+
def constructPrimaryIdentifier(self, data, ordered_identifier_candidates):
33+
"""
34+
Construct and return a primary identifier value from the
35+
data asserted by the IdP using the ordered list of candidates
36+
from the configuration.
37+
"""
38+
logprefix = PrimaryIdentifier.logprefix
39+
context = self.context
40+
41+
attributes = data.attributes
42+
satosa_logging(logger, logging.DEBUG, "{} Input attributes {}".format(logprefix, attributes), context.state)
43+
44+
value = None
45+
46+
for candidate in ordered_identifier_candidates:
47+
satosa_logging(logger, logging.DEBUG, "{} Considering candidate {}".format(logprefix, candidate), context.state)
48+
49+
# Get the values asserted by the IdP for the configured list of attribute names for this candidate
50+
# and substitute None if the IdP did not assert any value for a configured attribute.
51+
values = [ attributes.get(attribute_name, [None])[0] for attribute_name in candidate['attribute_names'] ]
52+
satosa_logging(logger, logging.DEBUG, "{} Found candidate values {}".format(logprefix, values), context.state)
53+
54+
# If one of the configured attribute names is name_id then if there is also a configured
55+
# name_id_format add the value for the NameID of that format if it was asserted by the IdP
56+
# or else add the value None.
57+
if 'name_id' in candidate['attribute_names']:
58+
nameid_value = None
59+
if 'name_id' in data.to_dict():
60+
name_id = data.to_dict()['name_id']
61+
satosa_logging(logger, logging.DEBUG, "{} IdP asserted NameID {}".format(logprefix, name_id), context.state)
62+
if 'name_id_format' in candidate:
63+
if candidate['name_id_format'] in name_id:
64+
nameid_value = name_id[candidate['name_id_format']]
65+
66+
# Only add the NameID value asserted by the IdP if it is not already
67+
# in the list of values. This is necessary because some non-compliant IdPs
68+
# have been known, for example, to assert the value of eduPersonPrincipalName
69+
# in the value for SAML2 persistent NameID as well as asserting
70+
# eduPersonPrincipalName.
71+
if nameid_value not in values:
72+
satosa_logging(logger, logging.DEBUG, "{} Added NameID {} to candidate values".format(logprefix, nameid_value), context.state)
73+
values.append(nameid_value)
74+
else:
75+
satosa_logging(logger, logging.WARN, "{} NameID {} value also asserted as attribute value".format(logprefix, nameid_value), context.state)
76+
77+
# If no value was asserted by the IdP for one of the configured list of attribute names
78+
# for this candidate then go onto the next candidate.
79+
if None in values:
80+
satosa_logging(logger, logging.DEBUG, "{} Candidate is missing value so skipping".format(logprefix), context.state)
81+
continue
82+
83+
# All values for the configured list of attribute names are present
84+
# so we can create a primary identifer. Add a scope if configured
85+
# to do so.
86+
if 'add_scope' in candidate:
87+
if candidate['add_scope'] == 'issuer_entityid':
88+
scope = data.to_dict()['auth_info']['issuer']
89+
else:
90+
scope = candidate['add_scope']
91+
satosa_logging(logger, logging.DEBUG, "{} Added scope {} to values".format(logprefix, scope), context.state)
92+
values.append(scope)
93+
94+
# Concatenate all values to create the primary identifier.
95+
value = ''.join(values)
96+
break
97+
98+
return value
99+
100+
def process(self, context, data):
101+
logprefix = PrimaryIdentifier.logprefix
102+
self.context = context
103+
104+
# Initialize the configuration to use as the default configuration
105+
# that is passed during initialization.
106+
config = self.config
107+
108+
satosa_logging(logger, logging.DEBUG, "{} Using default configuration {}".format(logprefix, config), context.state)
109+
110+
# Find the entityID for the SP that initiated the flow
111+
try:
112+
spEntityID = context.state.state_dict['SATOSA_BASE']['requester']
113+
except KeyError as err:
114+
satosa_logging(logger, logging.ERROR, "{} Unable to determine the entityID for the SP requester".format(logprefix), context.state)
115+
return super().process(context, data)
116+
117+
satosa_logging(logger, logging.DEBUG, "{} entityID for the SP requester is {}".format(logprefix, spEntityID), context.state)
118+
119+
# Find the entityID for the IdP that issued the assertion
120+
try:
121+
idpEntityID = data.to_dict()['auth_info']['issuer']
122+
except KeyError as err:
123+
satosa_logging(logger, logging.ERROR, "{} Unable to determine the entityID for the IdP issuer".format(logprefix), context.state)
124+
return super().process(context, data)
125+
126+
# Examine our configuration to determine if there is a per-IdP configuration
127+
if idpEntityID in self.config:
128+
config = self.config[idpEntityID]
129+
satosa_logging(logger, logging.DEBUG, "{} For IdP {} using configuration {}".format(logprefix, idpEntityID, config), context.state)
130+
131+
# Examine our configuration to determine if there is a per-SP configuration.
132+
# An SP configuration overrides an IdP configuration when there is a conflict.
133+
if spEntityID in self.config:
134+
config = self.config[spEntityID]
135+
satosa_logging(logger, logging.DEBUG, "{} For SP {} using configuration {}".format(logprefix, spEntityID, config), context.state)
136+
137+
# Obtain configuration details from the per-SP configuration or the default configuration
138+
try:
139+
if 'ordered_identifier_candidates' in config:
140+
ordered_identifier_candidates = config['ordered_identifier_candidates']
141+
else:
142+
ordered_identifier_candidates = self.config['ordered_identifier_candidates']
143+
if 'primary_identifier' in config:
144+
primary_identifier = config['primary_identifier']
145+
elif 'primary_identifier' in self.config:
146+
primary_identifier = self.config['primary_identifier']
147+
else:
148+
primary_identifier = 'uid'
149+
if 'clear_input_attributes' in config:
150+
clear_input_attributes = config['clear_input_attributes']
151+
elif 'clear_input_attributes' in self.config:
152+
clear_input_attributes = self.config['clear_input_attributes']
153+
else:
154+
clear_input_attributes = False
155+
if 'ignore' in config:
156+
ignore = True
157+
else:
158+
ignore = False
159+
if 'on_error' in config:
160+
on_error = config['on_error']
161+
elif 'on_error' in self.config:
162+
on_error = self.config['on_error']
163+
else:
164+
on_error = None
165+
166+
except KeyError as err:
167+
satosa_logging(logger, logging.ERROR, "{} Configuration '{}' is missing".format(logprefix, err), context.state)
168+
return super().process(context, data)
169+
170+
# Ignore this SP entirely if so configured.
171+
if ignore:
172+
satosa_logging(logger, logging.INFO, "{} Ignoring SP {}".format(logprefix, spEntityID), context.state)
173+
return super().process(context, data)
174+
175+
# Construct the primary identifier.
176+
satosa_logging(logger, logging.DEBUG, "{} Constructing primary identifier".format(logprefix), context.state)
177+
primary_identifier_val = self.constructPrimaryIdentifier(data, ordered_identifier_candidates)
178+
179+
if not primary_identifier_val:
180+
satosa_logging(logger, logging.WARN, "{} No primary identifier found".format(logprefix), context.state)
181+
if on_error:
182+
# Redirect to the configured error handling service with
183+
# the entityIDs for the target SP and IdP used by the user
184+
# as query string parameters (URL encoded).
185+
encodedSpEntityID = urllib.parse.quote_plus(spEntityID)
186+
encodedIdpEntityID = urllib.parse.quote_plus(data.to_dict()['auth_info']['issuer'])
187+
url = "{}?sp={}&idp={}".format(on_error, encodedSpEntityID, encodedIdpEntityID)
188+
satosa_logging(logger, logging.INFO, "{} Redirecting to {}".format(logprefix, url), context.state)
189+
return Redirect(url)
190+
191+
satosa_logging(logger, logging.INFO, "{} Found primary identifier: {}".format(logprefix, primary_identifier_val), context.state)
192+
193+
# Clear input attributes if so configured.
194+
if clear_input_attributes:
195+
satosa_logging(logger, logging.DEBUG, "{} Clearing values for these input attributes: {}".format(logprefix, data.attributes), context.state)
196+
data.attributes = {}
197+
198+
# Set the primary identifier attribute to the value found.
199+
data.attributes[primary_identifier] = primary_identifier_val
200+
satosa_logging(logger, logging.DEBUG, "{} Setting attribute {} to value {}".format(logprefix, primary_identifier, primary_identifier_val), context.state)
201+
202+
satosa_logging(logger, logging.DEBUG, "{} returning data.attributes {}".format(logprefix, str(data.attributes)), context.state)
203+
return super().process(context, data)

0 commit comments

Comments
 (0)