Skip to content

Commit 3e19014

Browse files
authored
Merge pull request #60 from skoranda/ldap_attribute_store_per_sp
Per-SP configuration for LDAP attribute store microservice
2 parents 65dad06 + 4310017 commit 3e19014

File tree

3 files changed

+88
-26
lines changed

3 files changed

+88
-26
lines changed

doc/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,9 @@ correct functionality.
490490
#### LDAP attribute store
491491

492492
An identifier such as eduPersonPrincipalName asserted by an IdP can be used to look up a person record
493-
in an LDAP directory to find attributes to assert about the authenticated user to the SP. To use the
493+
in an LDAP directory to find attributes to assert about the authenticated user to the SP. The identifier
494+
to consume from the IdP, the LDAP directory details, and the mapping of attributes found in the
495+
directory may all be confingured on a per-SP basis. To use the
494496
LDAP microservice install the extra necessary dependencies with `pip install satosa[ldap]` and then see the
495497
[example config](../example/plugins/microservices/ldap_attribute_store.yaml.example).
496498

example/plugins/microservices/ldap_attribute_store.yaml.example

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,11 @@ config:
1515
idp_identifiers:
1616
- eppn
1717
ldap_identifier_attribute: uid
18+
# Configuration may also be done per-SP with any
19+
# missing parameters taken from the default if any.
20+
# The configuration key is the entityID of the SP.
21+
#
22+
# For example:
23+
https://sp.myserver.edu/shibboleth-sp
24+
search_base: ou=People,o=MyVO,dc=example,dc=org
25+
eduPersonPrincipalName: employeenumber

src/satosa/micro_services/ldap_attribute_store.py

Lines changed: 77 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import satosa.micro_services.base
99
from satosa.logging_util import satosa_logging
1010

11+
import copy
1112
import logging
1213
import ldap3
1314

@@ -19,88 +20,139 @@ class LdapAttributeStore(satosa.micro_services.base.ResponseMicroService):
1920
to lookup a person record in LDAP and obtain attributes
2021
to assert about the user to the frontend receiving service.
2122
"""
23+
logprefix = "LDAP_ATTRIBUTE_STORE:"
2224

2325
def __init__(self, config, *args, **kwargs):
2426
super().__init__(*args, **kwargs)
2527
self.config = config
2628

2729
def process(self, context, data):
30+
logprefix = LdapAttributeStore.logprefix
31+
32+
# Initialize the configuration to use as the default configuration
33+
# that is passed during initialization.
34+
config = self.config
35+
configClean = copy.deepcopy(config)
36+
if 'bind_password' in configClean:
37+
configClean['bind_password'] = 'XXXXXXXX'
38+
39+
satosa_logging(logger, logging.DEBUG, "{} Using default configuration {}".format(logprefix, configClean), context.state)
40+
41+
# Find the entityID for the SP that initiated the flow
42+
try:
43+
spEntityID = context.state.state_dict['SATOSA_BASE']['requester']
44+
except KeyError as err:
45+
satosa_logging(logger, logging.ERROR, "{} Unable to determine the entityID for the SP requester".format(logprefix), context.state)
46+
return super().process(context, data)
47+
48+
satosa_logging(logger, logging.DEBUG, "{} entityID for the SP requester is {}".format(logprefix, spEntityID), context.state)
49+
50+
# Examine our configuration to determine if there is a per-SP configuration
51+
if spEntityID in self.config:
52+
config = self.config[spEntityID]
53+
configClean = copy.deepcopy(config)
54+
if 'bind_password' in configClean:
55+
configClean['bind_password'] = 'XXXXXXXX'
56+
satosa_logging(logger, logging.DEBUG, "{} For SP {} using configuration {}".format(logprefix, spEntityID, configClean), context.state)
57+
58+
# Obtain configuration details from the per-SP configuration or the default configuration
2859
try:
29-
ldap_url = self.config['ldap_url']
30-
bind_dn = self.config['bind_dn']
31-
bind_password = self.config['bind_password']
32-
search_base = self.config['search_base']
33-
search_return_attributes = self.config['search_return_attributes']
34-
idp_identifiers = self.config['idp_identifiers']
35-
ldap_identifier_attribute = self.config['ldap_identifier_attribute']
60+
if 'ldap_url' in config:
61+
ldap_url = config['ldap_url']
62+
else:
63+
ldap_url = self.config['ldap_url']
64+
if 'bind_dn' in config:
65+
bind_dn = config['bind_dn']
66+
else:
67+
bind_dn = self.config['bind_dn']
68+
if 'bind_dn' in config:
69+
bind_password = config['bind_password']
70+
else:
71+
bind_password = self.config['bind_password']
72+
if 'search_base' in config:
73+
search_base = config['search_base']
74+
else:
75+
search_base = self.config['search_base']
76+
if 'search_return_attributes' in config:
77+
search_return_attributes = config['search_return_attributes']
78+
else:
79+
search_return_attributes = self.config['search_return_attributes']
80+
if 'idp_identifiers' in config:
81+
idp_identifiers = config['idp_identifiers']
82+
else:
83+
idp_identifiers = self.config['idp_identifiers']
84+
if 'ldap_identifier_attribute' in config:
85+
ldap_identifier_attribute = config['ldap_identifier_attribute']
86+
else:
87+
ldap_identifier_attribute = self.config['ldap_identifier_attribute']
3688

3789
except KeyError as err:
38-
satosa_logging(logger, logging.ERROR, "Configuration '{key}' is missing".format(key=err), context.state)
90+
satosa_logging(logger, logging.ERROR, "{} Configuration '{}' is missing".format(logprefix, err), context.state)
3991
return super().process(context, data)
4092

4193
entry = None
4294

4395
try:
44-
satosa_logging(logger, logging.DEBUG, "Using LDAP URL {}".format(ldap_url), context.state)
96+
satosa_logging(logger, logging.DEBUG, "{} Using LDAP URL {}".format(logprefix, ldap_url), context.state)
4597
server = ldap3.Server(ldap_url)
4698

47-
satosa_logging(logger, logging.DEBUG, "Using bind DN {}".format(bind_dn), context.state)
99+
satosa_logging(logger, logging.DEBUG, "{} Using bind DN {}".format(logprefix, bind_dn), context.state)
48100
connection = ldap3.Connection(server, bind_dn, bind_password, auto_bind=True)
49-
satosa_logging(logger, logging.DEBUG, "Connected to LDAP server", context.state)
101+
satosa_logging(logger, logging.DEBUG, "{} Connected to LDAP server".format(logprefix), context.state)
50102

51103

52104
for identifier in idp_identifiers:
53105
if entry:
54106
break
55107

56-
satosa_logging(logger, logging.DEBUG, "Using IdP asserted attribute {}".format(identifier), context.state)
108+
satosa_logging(logger, logging.DEBUG, "{} Using IdP asserted attribute {}".format(logprefix, identifier), context.state)
57109

58110
if identifier in data.attributes:
59-
satosa_logging(logger, logging.DEBUG, "IdP asserted {} values for attribute {}".format(len(data.attributes[identifier]),identifier), context.state)
111+
satosa_logging(logger, logging.DEBUG, "{} IdP asserted {} values for attribute {}".format(logprefix, len(data.attributes[identifier]),identifier), context.state)
60112

61113
for identifier_value in data.attributes[identifier]:
62-
satosa_logging(logger, logging.DEBUG, "Considering IdP asserted value {} for attribute {}".format(identifier_value, identifier), context.state)
114+
satosa_logging(logger, logging.DEBUG, "{} Considering IdP asserted value {} for attribute {}".format(logprefix, identifier_value, identifier), context.state)
63115

64116
search_filter = '({0}={1})'.format(ldap_identifier_attribute, identifier_value)
65-
satosa_logging(logger, logging.DEBUG, "Constructed search filter {}".format(search_filter), context.state)
117+
satosa_logging(logger, logging.DEBUG, "{} Constructed search filter {}".format(logprefix, search_filter), context.state)
66118

67-
satosa_logging(logger, logging.DEBUG, "Querying LDAP server...", context.state)
119+
satosa_logging(logger, logging.DEBUG, "{} Querying LDAP server...".format(logprefix), context.state)
68120
connection.search(search_base, search_filter, attributes=search_return_attributes.keys())
69-
satosa_logging(logger, logging.DEBUG, "Done querying LDAP server", context.state)
121+
satosa_logging(logger, logging.DEBUG, "{} Done querying LDAP server".format(logprefix), context.state)
70122

71123
entries = connection.entries
72-
satosa_logging(logger, logging.DEBUG, "LDAP server returned {} entries".format(len(entries)), context.state)
124+
satosa_logging(logger, logging.DEBUG, "{} LDAP server returned {} entries".format(logprefix, len(entries)), context.state)
73125

74126
# for now consider only the first entry found (if any)
75127
if len(entries) > 0:
76128
if len(entries) > 1:
77-
satosa_logging(logger, logging.WARN, "LDAP server returned {} entries using IdP asserted attribute {}".format(len(entries), identifier), context.state)
129+
satosa_logging(logger, logging.WARN, "{} LDAP server returned {} entries using IdP asserted attribute {}".format(logprefix, len(entries), identifier), context.state)
78130
entry = entries[0]
79131
break
80132

81133
else:
82-
satosa_logging(logger, logging.DEBUG, "IdP did not assert attribute {}".format(identifier), context.state)
134+
satosa_logging(logger, logging.DEBUG, "{} IdP did not assert attribute {}".format(logprefix, identifier), context.state)
83135

84136
except Exception as err:
85-
satosa_logging(logger, logging.ERROR, "Caught exception: {0}".format(err), None)
137+
satosa_logging(logger, logging.ERROR, "{} Caught exception: {0}".format(logprefix, err), None)
86138
return super().process(context, data)
87139

88140
else:
89-
satosa_logging(logger, logging.DEBUG, "Unbinding and closing connection to LDAP server", context.state)
141+
satosa_logging(logger, logging.DEBUG, "{} Unbinding and closing connection to LDAP server".format(logprefix), context.state)
90142
connection.unbind()
91143

92144
# use a found entry, if any, to populate attributes
93145
if entry:
94-
satosa_logging(logger, logging.DEBUG, "Using entry with DN {}".format(entry.entry_get_dn()), context.state)
146+
satosa_logging(logger, logging.DEBUG, "{} Using entry with DN {}".format(logprefix, entry.entry_get_dn()), context.state)
95147
data.attributes = {}
96148
for attr in search_return_attributes.keys():
97149
if attr in entry:
98150
data.attributes[search_return_attributes[attr]] = entry[attr].values
99-
satosa_logging(logger, logging.DEBUG, "Setting internal attribute {} with values {}".format(search_return_attributes[attr], entry[attr].values), context.state)
151+
satosa_logging(logger, logging.DEBUG, "{} Setting internal attribute {} with values {}".format(logprefix, search_return_attributes[attr], entry[attr].values), context.state)
100152

101153
else:
102154
# We should probably have an option here to clear attributes from IdP
103155
pass
104156

105-
satosa_logging(logger, logging.DEBUG, "returning data.attributes %s" % str(data.attributes), context.state)
157+
satosa_logging(logger, logging.DEBUG, "{} returning data.attributes {}".format(logprefix, str(data.attributes)), context.state)
106158
return super().process(context, data)

0 commit comments

Comments
 (0)