Skip to content

Commit cd4344d

Browse files
committed
[Microservice] ldap_attribute_store_no_pool
1 parent c56a1e0 commit cd4344d

File tree

1 file changed

+194
-0
lines changed

1 file changed

+194
-0
lines changed
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
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+
from satosa.micro_services.base import ResponseMicroService
9+
from satosa.logging_util import satosa_logging
10+
from satosa.response import Redirect
11+
12+
import logging
13+
import ldap3
14+
import urllib
15+
16+
from ldap3.core.exceptions import LDAPException
17+
18+
from . ldap_attribute_store import (LdapAttributeStore,
19+
LdapAttributeStoreError)
20+
21+
logger = logging.getLogger(__name__)
22+
23+
class LdapAttributeStoreNoPool(LdapAttributeStore):
24+
"""
25+
Use identifier provided by the backend authentication service
26+
to lookup a person record in LDAP and obtain attributes
27+
to assert about the user to the frontend receiving service.
28+
"""
29+
30+
def _ldap_connection_factory(self, config):
31+
"""
32+
Use the input configuration to instantiate and return
33+
a ldap3 Connection object.
34+
"""
35+
ldap_url = config['ldap_url']
36+
bind_dn = config['bind_dn']
37+
bind_password = config['bind_password']
38+
39+
if not ldap_url:
40+
raise LdapAttributeStoreError("ldap_url is not configured")
41+
if not bind_dn:
42+
raise LdapAttributeStoreError("bind_dn is not configured")
43+
if not bind_password:
44+
raise LdapAttributeStoreError("bind_password is not configured")
45+
46+
server = ldap3.Server(config['ldap_url'])
47+
48+
satosa_logging(logger, logging.DEBUG, "Creating a new LDAP connection", None)
49+
satosa_logging(logger, logging.DEBUG, "Using LDAP URL {}".format(ldap_url), None)
50+
satosa_logging(logger, logging.DEBUG, "Using bind DN {}".format(bind_dn), None)
51+
52+
try:
53+
connection = ldap3.Connection(
54+
server,
55+
bind_dn,
56+
bind_password,
57+
auto_bind=False, # creates anonymous session open and bound to the server with a synchronous communication strategy
58+
client_strategy=ldap3.RESTARTABLE,
59+
read_only=True,
60+
version=3)
61+
satosa_logging(logger, logging.DEBUG, "Successfully connected to LDAP server", None)
62+
63+
except LDAPException as e:
64+
msg = "Caught exception when connecting to LDAP server: {}".format(e)
65+
satosa_logging(logger, logging.ERROR, msg, None)
66+
raise LdapAttributeStoreError(msg)
67+
68+
return connection
69+
70+
def process(self, context, data):
71+
"""
72+
Default interface for microservices. Process the input data for
73+
the input context.
74+
"""
75+
self.context = context
76+
77+
# Find the entityID for the SP that initiated the flow.
78+
try:
79+
sp_entity_id = context.state.state_dict['SATOSA_BASE']['requester']
80+
except KeyError as err:
81+
satosa_logging(logger, logging.ERROR, "Unable to determine the entityID for the SP requester", context.state)
82+
return super().process(context, data)
83+
84+
satosa_logging(logger, logging.DEBUG, "entityID for the SP requester is {}".format(sp_entity_id), context.state)
85+
86+
# Get the configuration for the SP.
87+
if sp_entity_id in self.config.keys():
88+
config = self.config[sp_entity_id]
89+
else:
90+
config = self.config['default']
91+
92+
satosa_logging(logger, logging.DEBUG, "Using config {}".format(self._filter_config(config)), context.state)
93+
94+
# Ignore this SP entirely if so configured.
95+
if config['ignore']:
96+
satosa_logging(logger, logging.INFO, "Ignoring SP {}".format(sp_entity_id), None)
97+
return super().process(context, data)
98+
99+
# The list of values for the LDAP search filters that will be tried in order to find the
100+
# LDAP directory record for the user.
101+
filter_values = []
102+
103+
# Loop over the configured list of identifiers from the IdP to consider and find
104+
# asserted values to construct the ordered list of values for the LDAP search filters.
105+
for candidate in config['ordered_identifier_candidates']:
106+
value = self._construct_filter_value(candidate, data)
107+
# If we have constructed a non empty value then add it as the next filter value
108+
# to use when searching for the user record.
109+
if value:
110+
filter_values.append(value)
111+
satosa_logging(logger, logging.DEBUG, "Added search filter value {} to list of search filters".format(value), context.state)
112+
113+
# Initialize an empty LDAP record. The first LDAP record found using the ordered
114+
# list of search filter values will be the record used.
115+
record = None
116+
results = None
117+
exp_msg = ''
118+
119+
for filter_val in filter_values:
120+
connection = config['connection']
121+
search_filter = '({0}={1})'.format(config['ldap_identifier_attribute'], filter_val)
122+
# show ldap filter
123+
satosa_logging(logger, logging.INFO, "LDAP query for {}".format(search_filter), context.state)
124+
satosa_logging(logger, logging.DEBUG, "Constructed search filter {}".format(search_filter), context.state)
125+
126+
try:
127+
# message_id only works in REUSABLE async connection strategy
128+
results = connection.search(config['search_base'], search_filter, attributes=config['search_return_attributes'].keys())
129+
except LDAPException as err:
130+
exp_msg = "Caught LDAP exception: {}".format(err)
131+
except LdapAttributeStoreError as err:
132+
exp_msg = "Caught LDAP Attribute Store exception: {}".format(err)
133+
except Exception as err:
134+
exp_msg = "Caught unhandled exception: {}".format(err)
135+
136+
if exp_msg:
137+
satosa_logging(logger, logging.ERROR, exp_msg, context.state)
138+
return super().process(context, data)
139+
140+
if not results:
141+
satosa_logging(logger, logging.DEBUG, "Querying LDAP server: Nop results for {}.".format(filter_val), context.state)
142+
continue
143+
responses = connection.entries
144+
145+
satosa_logging(logger, logging.DEBUG, "Done querying LDAP server", context.state)
146+
satosa_logging(logger, logging.DEBUG, "LDAP server returned {} records".format(len(responses)), context.state)
147+
148+
# for now consider only the first record found (if any)
149+
if len(responses) > 0:
150+
if len(responses) > 1:
151+
satosa_logging(logger, logging.WARN, "LDAP server returned {} records using search filter value {}".format(len(responses), filter_val), context.state)
152+
record = responses[0]
153+
break
154+
155+
# Before using a found record, if any, to populate attributes
156+
# clear any attributes incoming to this microservice if so configured.
157+
if config['clear_input_attributes']:
158+
satosa_logging(logger, logging.DEBUG, "Clearing values for these input attributes: {}".format(data.attributes), context.state)
159+
data.attributes = {}
160+
161+
# this adapts records with different search and conenction strategy (sync without pool)
162+
r = dict()
163+
r['dn'] = record.entry_dn
164+
r['attributes'] = record.entry_attributes_as_dict
165+
record = r
166+
# ends adaptation
167+
168+
# Use a found record, if any, to populate attributes and input for NameID
169+
if record:
170+
satosa_logging(logger, logging.DEBUG, "Using record with DN {}".format(record["dn"]), context.state)
171+
satosa_logging(logger, logging.DEBUG, "Record with DN {} has attributes {}".format(record["dn"], record["attributes"]), context.state)
172+
173+
# Populate attributes as configured.
174+
self._populate_attributes(config, record, context, data)
175+
176+
# Populate input for NameID if configured. SATOSA core does the hashing of input
177+
# to create a persistent NameID.
178+
self._populate_input_for_name_id(config, record, context, data)
179+
180+
else:
181+
satosa_logging(logger, logging.WARN, "No record found in LDAP so no attributes will be added", context.state)
182+
on_ldap_search_result_empty = config['on_ldap_search_result_empty']
183+
if on_ldap_search_result_empty:
184+
# Redirect to the configured URL with
185+
# the entityIDs for the target SP and IdP used by the user
186+
# as query string parameters (URL encoded).
187+
encoded_sp_entity_id = urllib.parse.quote_plus(sp_entity_id)
188+
encoded_idp_entity_id = urllib.parse.quote_plus(data.auth_info.issuer)
189+
url = "{}?sp={}&idp={}".format(on_ldap_search_result_empty, encoded_sp_entity_id, encoded_idp_entity_id)
190+
satosa_logging(logger, logging.INFO, "Redirecting to {}".format(url), context.state)
191+
return Redirect(url)
192+
193+
satosa_logging(logger, logging.DEBUG, "Returning data.attributes {}".format(str(data.attributes)), context.state)
194+
return ResponseMicroService.process(self, context, data)

0 commit comments

Comments
 (0)