Skip to content

Per-SP configuration for LDAP attribute store microservice #60

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 10, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,9 @@ correct functionality.
#### LDAP attribute store

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,11 @@ config:
idp_identifiers:
- eppn
ldap_identifier_attribute: uid
# Configuration may also be done per-SP with any
# missing parameters taken from the default if any.
# The configuration key is the entityID of the SP.
#
# For example:
https://sp.myserver.edu/shibboleth-sp
search_base: ou=People,o=MyVO,dc=example,dc=org
eduPersonPrincipalName: employeenumber
102 changes: 77 additions & 25 deletions src/satosa/micro_services/ldap_attribute_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import satosa.micro_services.base
from satosa.logging_util import satosa_logging

import copy
import logging
import ldap3

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

def __init__(self, config, *args, **kwargs):
super().__init__(*args, **kwargs)
self.config = config

def process(self, context, data):
logprefix = LdapAttributeStore.logprefix

# Initialize the configuration to use as the default configuration
# that is passed during initialization.
config = self.config
configClean = copy.deepcopy(config)
if 'bind_password' in configClean:
configClean['bind_password'] = 'XXXXXXXX'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this, together with the similar line at the comment below, a leftover from debugging?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. The intent is to log the configuration at the DEBUG level but blank out any bind password included in the configuration so that passwords are never logged.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, thanks for the explanation, I see now how it's used while logging.


satosa_logging(logger, logging.DEBUG, "{} Using default configuration {}".format(logprefix, configClean), context.state)

# Find the entityID for the SP that initiated the flow
try:
spEntityID = context.state.state_dict['SATOSA_BASE']['requester']
except KeyError as err:
satosa_logging(logger, logging.ERROR, "{} Unable to determine the entityID for the SP requester".format(logprefix), context.state)
return super().process(context, data)

satosa_logging(logger, logging.DEBUG, "{} entityID for the SP requester is {}".format(logprefix, spEntityID), context.state)

# Examine our configuration to determine if there is a per-SP configuration
if spEntityID in self.config:
config = self.config[spEntityID]
configClean = copy.deepcopy(config)
if 'bind_password' in configClean:
configClean['bind_password'] = 'XXXXXXXX'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As asked above, is this line a leftover from debugging?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. The intent is to log the configuration at the DEBUG level but blank out any bind password included in the configuration so that passwords are never logged.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, thanks for the explanation, I see now how it's used while logging.

satosa_logging(logger, logging.DEBUG, "{} For SP {} using configuration {}".format(logprefix, spEntityID, configClean), context.state)

# Obtain configuration details from the per-SP configuration or the default configuration
try:
ldap_url = self.config['ldap_url']
bind_dn = self.config['bind_dn']
bind_password = self.config['bind_password']
search_base = self.config['search_base']
search_return_attributes = self.config['search_return_attributes']
idp_identifiers = self.config['idp_identifiers']
ldap_identifier_attribute = self.config['ldap_identifier_attribute']
if 'ldap_url' in config:
ldap_url = config['ldap_url']
else:
ldap_url = self.config['ldap_url']
if 'bind_dn' in config:
bind_dn = config['bind_dn']
else:
bind_dn = self.config['bind_dn']
if 'bind_dn' in config:
bind_password = config['bind_password']
else:
bind_password = self.config['bind_password']
if 'search_base' in config:
search_base = config['search_base']
else:
search_base = self.config['search_base']
if 'search_return_attributes' in config:
search_return_attributes = config['search_return_attributes']
else:
search_return_attributes = self.config['search_return_attributes']
if 'idp_identifiers' in config:
idp_identifiers = config['idp_identifiers']
else:
idp_identifiers = self.config['idp_identifiers']
if 'ldap_identifier_attribute' in config:
ldap_identifier_attribute = config['ldap_identifier_attribute']
else:
ldap_identifier_attribute = self.config['ldap_identifier_attribute']

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

entry = None

try:
satosa_logging(logger, logging.DEBUG, "Using LDAP URL {}".format(ldap_url), context.state)
satosa_logging(logger, logging.DEBUG, "{} Using LDAP URL {}".format(logprefix, ldap_url), context.state)
server = ldap3.Server(ldap_url)

satosa_logging(logger, logging.DEBUG, "Using bind DN {}".format(bind_dn), context.state)
satosa_logging(logger, logging.DEBUG, "{} Using bind DN {}".format(logprefix, bind_dn), context.state)
connection = ldap3.Connection(server, bind_dn, bind_password, auto_bind=True)
satosa_logging(logger, logging.DEBUG, "Connected to LDAP server", context.state)
satosa_logging(logger, logging.DEBUG, "{} Connected to LDAP server".format(logprefix), context.state)


for identifier in idp_identifiers:
if entry:
break

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

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

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

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

satosa_logging(logger, logging.DEBUG, "Querying LDAP server...", context.state)
satosa_logging(logger, logging.DEBUG, "{} Querying LDAP server...".format(logprefix), context.state)
connection.search(search_base, search_filter, attributes=search_return_attributes.keys())
satosa_logging(logger, logging.DEBUG, "Done querying LDAP server", context.state)
satosa_logging(logger, logging.DEBUG, "{} Done querying LDAP server".format(logprefix), context.state)

entries = connection.entries
satosa_logging(logger, logging.DEBUG, "LDAP server returned {} entries".format(len(entries)), context.state)
satosa_logging(logger, logging.DEBUG, "{} LDAP server returned {} entries".format(logprefix, len(entries)), context.state)

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

else:
satosa_logging(logger, logging.DEBUG, "IdP did not assert attribute {}".format(identifier), context.state)
satosa_logging(logger, logging.DEBUG, "{} IdP did not assert attribute {}".format(logprefix, identifier), context.state)

except Exception as err:
satosa_logging(logger, logging.ERROR, "Caught exception: {0}".format(err), None)
satosa_logging(logger, logging.ERROR, "{} Caught exception: {0}".format(logprefix, err), None)
return super().process(context, data)

else:
satosa_logging(logger, logging.DEBUG, "Unbinding and closing connection to LDAP server", context.state)
satosa_logging(logger, logging.DEBUG, "{} Unbinding and closing connection to LDAP server".format(logprefix), context.state)
connection.unbind()

# use a found entry, if any, to populate attributes
if entry:
satosa_logging(logger, logging.DEBUG, "Using entry with DN {}".format(entry.entry_get_dn()), context.state)
satosa_logging(logger, logging.DEBUG, "{} Using entry with DN {}".format(logprefix, entry.entry_get_dn()), context.state)
data.attributes = {}
for attr in search_return_attributes.keys():
if attr in entry:
data.attributes[search_return_attributes[attr]] = entry[attr].values
satosa_logging(logger, logging.DEBUG, "Setting internal attribute {} with values {}".format(search_return_attributes[attr], entry[attr].values), context.state)
satosa_logging(logger, logging.DEBUG, "{} Setting internal attribute {} with values {}".format(logprefix, search_return_attributes[attr], entry[attr].values), context.state)

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

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