Skip to content
This repository was archived by the owner on Jan 10, 2019. It is now read-only.

Commit d26f4b0

Browse files
committed
First commit of the IdP Metadata Attribute Store
1 parent befc9f4 commit d26f4b0

File tree

2 files changed

+258
-0
lines changed

2 files changed

+258
-0
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
module: idp_metadata_attribute_store.IdpMetadataAttributeStore
2+
name: IdpMetadataAttributeStore
3+
config:
4+
default:
5+
display_name:
6+
# SATOSA internal attribute name to use
7+
internal_attribute_name: idpdisplayname
8+
# Language preference. 'en' or English is the default
9+
# if not specified.
10+
lang: en
11+
organization_name:
12+
internal_attribute_name: idporgname
13+
organization_display_name:
14+
internal_attribute_name: idporgdisplayname
15+
16+
# Configuration may also be done per-IdP with any
17+
# missing parameters taken from the default if any.
18+
# The configuration key is the entityID of the IdP.
19+
#
20+
# For example:
21+
https://idp.myorg.edu/idp/shibboleth:
22+
display_name:
23+
internal_attribute_name: othername
24+
lang: jp
25+
# The microservice may be configured to ignore a particular IdP.
26+
https://login.other.org.edu/idp/shibboleth:
27+
ignore: true
28+
29+
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
"""
2+
SATOSA microservice that includes in the assertion
3+
attributes taken from SAML metadata about the SAML
4+
IdP used for authentication.
5+
6+
The attributes that may be asserted from the SAML
7+
metadata for the IdP include
8+
9+
<mdui:DisplayName>
10+
<OrganiationName>
11+
<OrganizationDisplayName>
12+
13+
A typical configuration would be
14+
15+
module: idp_metadata_attribute_store.IdpMetadataAttributeStore
16+
name: IdpMetadataAttributeStore
17+
config:
18+
default:
19+
display_name:
20+
# SATOSA internal attribute name to use
21+
internal_attribute_name: idpdisplayname
22+
# Language preference with 'en' or English as default
23+
lang: en
24+
organization_name:
25+
internal_attribute_name: idporgname
26+
lang: en
27+
organization_display_name:
28+
internal_attribute_name: idporgdisplayname
29+
lang: en
30+
31+
# Configuration may also be done per-IdP with any
32+
# missing parameters taken from the default if any.
33+
# The configuration key is the entityID of the IdP.
34+
#
35+
# For example:
36+
https://login.myorg.edu/idp/shibboleth:
37+
display_name:
38+
internal_attribute_name: othername
39+
lang: jp
40+
# The microservice may be configured to ignore a particular IdP.
41+
https://login.other.org.edu/idp/shibboleth:
42+
ignore: true
43+
"""
44+
45+
import satosa.micro_services.base
46+
from satosa.logging_util import satosa_logging
47+
from satosa.exception import SATOSAError
48+
49+
import copy
50+
import logging
51+
52+
logger = logging.getLogger(__name__)
53+
54+
class IdpMetadataAttributeStoreError(SATOSAError):
55+
"""
56+
LDAP attribute store error
57+
"""
58+
pass
59+
60+
class IdpMetadataAttributeStore(satosa.micro_services.base.ResponseMicroService):
61+
"""
62+
Use the metadata store attached to the proxy SP in the context
63+
to lookup metadata about the IdP entity making the assertion
64+
and include metadata details as attributes in the assertion sent
65+
to the SP that made the request.
66+
"""
67+
68+
config_defaults = { 'ignore' : False }
69+
70+
def __init__(self, config, *args, **kwargs):
71+
super().__init__(*args, **kwargs)
72+
73+
if 'default' in config and "" in config:
74+
msg = """Use either 'default' or "" in config but not both"""
75+
satosa_logging(logger, logging.ERROR, msg, None)
76+
raise IdpMetadataAttributeStoreError(msg)
77+
78+
if "" in config:
79+
config['default'] = config.pop("")
80+
81+
if 'default' not in config:
82+
msg = "No default configuration is present"
83+
satosa_logging(logger, logging.ERROR, msg, None)
84+
raise IdpMetadataAttributeStoreError(msg)
85+
86+
self.config = {}
87+
88+
# Process the default configuration first then any per-IdP overrides.
89+
idp_list = ['default']
90+
idp_list.extend([ key for key in config.keys() if key != 'default' ])
91+
92+
for idp in idp_list:
93+
if not isinstance(config[idp], dict):
94+
msg = "Configuration value for {} must be a dictionary"
95+
satosa_logging(logger, logging.ERROR, msg, None)
96+
raise IdpMetadataAttributeStoreError(msg)
97+
98+
# Initialize configuration using module defaults then update
99+
# with configuration defaults and then per-IdP overrides.
100+
idp_config = copy.deepcopy(IdpMetadataAttributeStore.config_defaults)
101+
if 'default' in self.config:
102+
idp_config.update(self.config['default'])
103+
idp_config.update(config[idp])
104+
105+
self.config[idp] = idp_config
106+
107+
satosa_logging(logger, logging.INFO, "IdP Metadata Attribute Store microservice initialized", None)
108+
109+
def _first_lang_element_text(self, elements, lang='en'):
110+
"""
111+
Loop over the list representing XML elements that contain text and find
112+
the first text value for the input lang where 'en' or English is the
113+
default lang.
114+
115+
Each item in the list is a dictionary with keys
116+
117+
__class__
118+
lang
119+
text
120+
121+
as expected from the metadata returned for an entity by the MetadataStore
122+
class from pysaml2.
123+
124+
If no element has the input lang then return the text from the first
125+
element.
126+
127+
If no element has text then return an empty string.
128+
"""
129+
for e in elements:
130+
if lang in e:
131+
if 'text' in e:
132+
return e['text']
133+
134+
for e in elements:
135+
if 'text' in e:
136+
return e['text']
137+
138+
return ''
139+
140+
def process(self, context, data):
141+
"""
142+
Default interface for microservices. Process the input data for
143+
the input context.
144+
"""
145+
self.context = context
146+
147+
# Find the entityID for the IdP that issued the assertion.
148+
try:
149+
idp_entity_id = data.to_dict()['auth_info']['issuer']
150+
except KeyError as err:
151+
satosa_logging(logger, logging.ERROR, "Unable to determine the entityID for the IdP issuer", context.state)
152+
return super().process(context, data)
153+
154+
# Get the configuration for the IdP.
155+
if idp_entity_id in self.config.keys():
156+
config = self.config[idp_entity_id]
157+
else:
158+
config = self.config['default']
159+
160+
satosa_logging(logger, logging.DEBUG, "Using config {}".format(config), context.state)
161+
162+
# Ignore this IdP if so configured.
163+
if config['ignore']:
164+
satosa_logging(logger, logging.INFO, "Ignoring IdP {}".format(idp_entity_id), context.state)
165+
return super().process(context, data)
166+
167+
# Get the metadata store the SP for the proxy is using. This
168+
# will be an instance of the class MetadataStore from mdstore.py
169+
# in pysaml2.
170+
metadata_store = context.internal_data['metadata_store']
171+
172+
# Get the metadata for the IdP.
173+
try:
174+
metadata = metadata_store[idp_entity_id]
175+
except Exception as err:
176+
satosa_logging(logger, logging.ERROR, "Unable to retrieve metadata for IdP {}".format(idp_entity_id), context.state)
177+
return super().process(context, data)
178+
179+
satosa_logging(logger, logging.DEBUG, "Metadata for IdP {} is {}".format(idp_entity_id, metadata), context.state)
180+
181+
# Find the mdui:DisplayName for the IdP if so configured.
182+
if 'display_name' in config:
183+
lang = config['display_name'].get('lang', 'en')
184+
try:
185+
# We assume there is only one IDPSSODescriptor in the IdP metadata.
186+
extensions = metadata['idpsso_descriptor'][0]['extensions']['extension_elements']
187+
for e in extensions:
188+
if e['__class__'] == 'urn:oasis:names:tc:SAML:metadata:ui&UIInfo':
189+
display_name_elements = e['display_name']
190+
display_name = self._first_lang_element_text(display_name_elements, lang)
191+
break
192+
193+
if display_name:
194+
satosa_logging(logger, logging.DEBUG, "display_name is {}".format(display_name), context.state)
195+
data.attributes[config['display_name']['internal_attribute_name']] = display_name
196+
197+
except Exception as err:
198+
satosa_logging(logger, logging.WARN, "Unable to determine display name for {}".format(idp_entity_id), context.state)
199+
200+
# Find the OrganizationDisplayName for the IdP if so configured.
201+
if 'organization_display_name' in config:
202+
lang = config['organization_display_name'].get('lang', 'en')
203+
try:
204+
org_display_name_elements = metadata['organization']['organization_display_name']
205+
organization_display_name = self._first_lang_element_text(org_display_name_elements, lang)
206+
207+
if organization_display_name:
208+
satosa_logging(logger, logging.DEBUG, "organization_display_name is {}".format(organization_display_name), context.state)
209+
data.attributes[config['organization_display_name']['internal_attribute_name']] = organization_display_name
210+
211+
except Exception as err:
212+
satosa_logging(logger, logging.WARN, "Unable to determine organization display name for {}".format(idp_entity_id), context.state)
213+
214+
# Find the OrganizationName for the IdP if so configured.
215+
if 'organization_name' in config:
216+
lang = config['organization_name'].get('lang', 'en')
217+
try:
218+
org_name_elements = metadata['organization']['organization_name']
219+
organization_name = self._first_lang_element_text(org_name_elements, lang)
220+
221+
if organization_name:
222+
satosa_logging(logger, logging.DEBUG, "organization_name is {}".format(organization_name), context.state)
223+
data.attributes[config['organization_name']['internal_attribute_name']] = organization_name
224+
225+
except Exception as err:
226+
satosa_logging(logger, logging.WARN, "Unable to determine organization display name for {}".format(idp_entity_id), context.state)
227+
228+
satosa_logging(logger, logging.DEBUG, "Returning data.attributes {}".format(str(data.attributes)), context.state)
229+
return super().process(context, data)

0 commit comments

Comments
 (0)