Skip to content

Commit 1993926

Browse files
author
Rebecka Gulliksson
committed
Merge branch 'skoranda-ldap_attribute_store'
2 parents 7a43597 + fb802bf commit 1993926

File tree

4 files changed

+133
-0
lines changed

4 files changed

+133
-0
lines changed

doc/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,13 @@ the same REST API).
487487
This micro service must be the last in the list of configured micro services in the `proxy_conf.yaml` to ensure
488488
correct functionality.
489489

490+
#### LDAP attribute store
491+
492+
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
494+
LDAP microservice install the extra necessary dependencies with `pip install satosa[ldap]` and then see the
495+
[example config](../example/plugins/microservices/ldap_attribute_store.yaml.example).
496+
490497
### Custom plugins
491498

492499
It's possible to write custom plugins which can be loaded by SATOSA. They have to be contained in a Python module,
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module: plugins.microservices.ldap_attribute_store.LdapAttributeStore
2+
name: LdapAttributeStore
3+
config:
4+
ldap_url: ldaps://ldap.example.org
5+
bind_dn: cn=admin,dc=example,dc=org
6+
bind_password: xxxxxxxx
7+
search_base: ou=People,dc=example,dc=org
8+
search_return_attributes:
9+
# format is LDAP attribute name : internal attribute name
10+
sn: surname
11+
givenName: givenname
12+
mail: mail
13+
employeeNumber: employeenumber
14+
isMemberOf: ismemberof
15+
idp_identifiers:
16+
- eppn
17+
ldap_identifier_attribute: uid

setup.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
"Werkzeug",
2626
"click"
2727
],
28+
extras_require={
29+
"ldap": ["ldap3"],
30+
},
2831
zip_safe=False,
2932
classifiers=[
3033
"Programming Language :: Python :: 3 :: Only",
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""
2+
SATOSA microservice that uses an identifier asserted by
3+
the home organization SAML IdP as a key to search an LDAP
4+
directory for a record and then consume attributes from
5+
the record and assert them to the receiving SP.
6+
"""
7+
8+
import satosa.micro_services.base
9+
from satosa.logging_util import satosa_logging
10+
11+
import logging
12+
import ldap3
13+
14+
logger = logging.getLogger(__name__)
15+
16+
class LdapAttributeStore(satosa.micro_services.base.ResponseMicroService):
17+
"""
18+
Use identifier provided by the backend authentication service
19+
to lookup a person record in LDAP and obtain attributes
20+
to assert about the user to the frontend receiving service.
21+
"""
22+
23+
def __init__(self, config, *args, **kwargs):
24+
super().__init__(*args, **kwargs)
25+
self.config = config
26+
27+
def process(self, context, data):
28+
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']
36+
37+
except KeyError as err:
38+
satosa_logging(logger, logging.ERROR, "Configuration '{key}' is missing".format(key=err), context.state)
39+
return super().process(context, data)
40+
41+
entry = None
42+
43+
try:
44+
satosa_logging(logger, logging.DEBUG, "Using LDAP URL {}".format(ldap_url), context.state)
45+
server = ldap3.Server(ldap_url)
46+
47+
satosa_logging(logger, logging.DEBUG, "Using bind DN {}".format(bind_dn), context.state)
48+
connection = ldap3.Connection(server, bind_dn, bind_password, auto_bind=True)
49+
satosa_logging(logger, logging.DEBUG, "Connected to LDAP server", context.state)
50+
51+
52+
for identifier in idp_identifiers:
53+
if entry:
54+
break
55+
56+
satosa_logging(logger, logging.DEBUG, "Using IdP asserted attribute {}".format(identifier), context.state)
57+
58+
if identifier in data.attributes:
59+
satosa_logging(logger, logging.DEBUG, "IdP asserted {} values for attribute {}".format(len(data.attributes[identifier]),identifier), context.state)
60+
61+
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)
63+
64+
search_filter = '({0}={1})'.format(ldap_identifier_attribute, identifier_value)
65+
satosa_logging(logger, logging.DEBUG, "Constructed search filter {}".format(search_filter), context.state)
66+
67+
satosa_logging(logger, logging.DEBUG, "Querying LDAP server...", context.state)
68+
connection.search(search_base, search_filter, attributes=search_return_attributes.keys())
69+
satosa_logging(logger, logging.DEBUG, "Done querying LDAP server", context.state)
70+
71+
entries = connection.entries
72+
satosa_logging(logger, logging.DEBUG, "LDAP server returned {} entries".format(len(entries)), context.state)
73+
74+
# for now consider only the first entry found (if any)
75+
if len(entries) > 0:
76+
if len(entries) > 1:
77+
satosa_logging(logger, logging.WARN, "LDAP server returned {} entries using IdP asserted attribute {}".format(len(entries), identifier), context.state)
78+
entry = entries[0]
79+
break
80+
81+
else:
82+
satosa_logging(logger, logging.DEBUG, "IdP did not assert attribute {}".format(identifier), context.state)
83+
84+
except Exception as err:
85+
satosa_logging(logger, logging.ERROR, "Caught exception: {0}".format(err), None)
86+
return super().process(context, data)
87+
88+
else:
89+
satosa_logging(logger, logging.DEBUG, "Unbinding and closing connection to LDAP server", context.state)
90+
connection.unbind()
91+
92+
# use a found entry, if any, to populate attributes
93+
if entry:
94+
satosa_logging(logger, logging.DEBUG, "Using entry with DN {}".format(entry.entry_get_dn()), context.state)
95+
data.attributes = {}
96+
for attr in search_return_attributes.keys():
97+
if attr in entry:
98+
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)
100+
101+
else:
102+
# We should probably have an option here to clear attributes from IdP
103+
pass
104+
105+
satosa_logging(logger, logging.DEBUG, "returning data.attributes %s" % str(data.attributes), context.state)
106+
return super().process(context, data)

0 commit comments

Comments
 (0)