@@ -28,48 +28,104 @@ def __init__(self, config, *args, **kwargs):
28
28
super ().__init__ (* args , ** kwargs )
29
29
self .config = config
30
30
31
- def constructFilterValue (self , identifier , data ):
31
+ def constructFilterValue (self , candidate , data ):
32
32
"""
33
33
Construct and return a LDAP directory search filter value from the
34
- data asserted by the IdP based on the input identifier.
34
+ candidate identifier.
35
35
36
- If the input identifier is a list of identifiers then this
37
- method is called recursively and the values concatenated together.
36
+ Argument 'canidate' is a dictionary with one required key and
37
+ two optional keys:
38
+
39
+ key required value
40
+ --------------- -------- ---------------------------------
41
+ attribute_names Y list of identifier names
38
42
39
- If the input identifier is a dictionary with 'name_id' as the key
40
- and a NameID format as value than the NameID value (if any) asserted
41
- by the IdP for that format is used as the value.
43
+ name_id_format N NameID format (string)
44
+
45
+ add_scope N "issuer_entityid" or other string
46
+
47
+ Argument 'data' is that object passed into the microservice
48
+ method process().
49
+
50
+ If the attribute_names list consists of more than one identifier
51
+ name then the values of the identifiers will be concatenated together
52
+ to create the filter value.
53
+
54
+ If one of the identifier names in the attribute_names is the string
55
+ 'name_id' then the NameID value with format name_id_format
56
+ will be concatenated to the filter value.
57
+
58
+ If the add_scope key is present with value 'issuer_entityid' then the
59
+ entityID for the IdP will be concatenated to "scope" the value. If the
60
+ string is any other value it will be directly concatenated.
42
61
"""
43
- value = ""
44
-
45
- # If the identifier is a list of identifiers then loop over them
46
- # calling ourself recursively and concatenate the values from
47
- # the identifiers together.
48
- if isinstance (identifier , list ):
49
- for i in identifier :
50
- value += self .constructFilterValue (i , data )
51
-
52
- # If the identifier is a dictionary with key 'name_id' then the value
53
- # is a NameID format. Look for a NameID asserted by the IdP with that
54
- # format and if found use its value.
55
- elif isinstance (identifier , dict ):
56
- if 'name_id' in identifier :
57
- nameIdFormat = identifier ['name_id' ]
58
- if 'name_id' in data .to_dict ():
59
- if nameIdFormat in data .to_dict ()['name_id' ]:
60
- value += data .to_dict ()['name_id' ][nameIdFormat ]
61
-
62
- # The identifier is not a list or dictionary so just consume the asserted values
63
- # for this single identifier to create the value.
64
- else :
65
- if identifier in data .attributes :
66
- for v in data .attributes [identifier ]:
67
- value += v
62
+ logprefix = self .logprefix
63
+ context = self .context
64
+
65
+ attributes = data .attributes
66
+ satosa_logging (logger , logging .DEBUG , "{} Input attributes {}" .format (logprefix , attributes ), context .state )
67
+
68
+ # Get the values configured list of identifier names for this candidate
69
+ # and substitute None if there are no values for a configured identifier.
70
+ values = []
71
+ for identifier_name in candidate ['attribute_names' ]:
72
+ v = attributes .get (identifier_name , None )
73
+ if isinstance (v , list ):
74
+ v = v [0 ]
75
+ values .append (v )
76
+ satosa_logging (logger , logging .DEBUG , "{} Found candidate values {}" .format (logprefix , values ), context .state )
77
+
78
+ # If one of the configured identifier names is name_id then if there is also a configured
79
+ # name_id_format add the value for the NameID of that format if it was asserted by the IdP
80
+ # or else add the value None.
81
+ if 'name_id' in candidate ['attribute_names' ]:
82
+ nameid_value = None
83
+ if 'name_id' in data .to_dict ():
84
+ name_id = data .to_dict ()['name_id' ]
85
+ satosa_logging (logger , logging .DEBUG , "{} IdP asserted NameID {}" .format (logprefix , name_id ), context .state )
86
+ if 'name_id_format' in candidate :
87
+ if candidate ['name_id_format' ] in name_id :
88
+ nameid_value = name_id [candidate ['name_id_format' ]]
89
+
90
+ # Only add the NameID value asserted by the IdP if it is not already
91
+ # in the list of values. This is necessary because some non-compliant IdPs
92
+ # have been known, for example, to assert the value of eduPersonPrincipalName
93
+ # in the value for SAML2 persistent NameID as well as asserting
94
+ # eduPersonPrincipalName.
95
+ if nameid_value not in values :
96
+ satosa_logging (logger , logging .DEBUG , "{} Added NameID {} to candidate values" .format (logprefix , nameid_value ), context .state )
97
+ values .append (nameid_value )
98
+ else :
99
+ satosa_logging (logger , logging .WARN , "{} NameID {} value also asserted as attribute value" .format (logprefix , nameid_value ), context .state )
100
+
101
+ # If no value was asserted by the IdP for one of the configured list of identifier names
102
+ # for this candidate then go onto the next candidate.
103
+ if None in values :
104
+ satosa_logging (logger , logging .DEBUG , "{} Candidate is missing value so skipping" .format (logprefix ), context .state )
105
+ return None
106
+
107
+ # All values for the configured list of attribute names are present
108
+ # so we can create a value. Add a scope if configured
109
+ # to do so.
110
+ if 'add_scope' in candidate :
111
+ if candidate ['add_scope' ] == 'issuer_entityid' :
112
+ scope = data .to_dict ()['auth_info' ]['issuer' ]
113
+ else :
114
+ scope = candidate ['add_scope' ]
115
+ satosa_logging (logger , logging .DEBUG , "{} Added scope {} to values" .format (logprefix , scope ), context .state )
116
+ values .append (scope )
117
+
118
+ # Concatenate all values to create the filter value.
119
+ value = '' .join (values )
120
+
121
+ satosa_logging (logger , logging .DEBUG , "{} Constructed filter value {}" .format (logprefix , value ), context .state )
68
122
69
123
return value
70
124
71
125
def process (self , context , data ):
72
126
logprefix = LdapAttributeStore .logprefix
127
+ self .logprefix = logprefix
128
+ self .context = context
73
129
74
130
# Initialize the configuration to use as the default configuration
75
131
# that is passed during initialization.
@@ -119,10 +175,10 @@ def process(self, context, data):
119
175
search_return_attributes = config ['search_return_attributes' ]
120
176
else :
121
177
search_return_attributes = self .config ['search_return_attributes' ]
122
- if 'idp_identifiers ' in config :
123
- idp_identifiers = config ['idp_identifiers ' ]
178
+ if 'ordered_identifier_candidates ' in config :
179
+ ordered_identifier_candidates = config ['ordered_identifier_candidates ' ]
124
180
else :
125
- idp_identifiers = self .config ['idp_identifiers ' ]
181
+ ordered_identifier_candidates = self .config ['ordered_identifier_candidates ' ]
126
182
if 'ldap_identifier_attribute' in config :
127
183
ldap_identifier_attribute = config ['ldap_identifier_attribute' ]
128
184
else :
@@ -156,14 +212,14 @@ def process(self, context, data):
156
212
157
213
# Loop over the configured list of identifiers from the IdP to consider and find
158
214
# asserted values to construct the ordered list of values for the LDAP search filters.
159
- for identifier in idp_identifiers :
160
- value = self .constructFilterValue (identifier , data )
215
+ for candidate in ordered_identifier_candidates :
216
+ value = self .constructFilterValue (candidate , data )
161
217
162
218
# If we have constructed a non empty value then add it as the next filter value
163
219
# to use when searching for the user record.
164
220
if value :
165
221
filterValues .append (value )
166
- satosa_logging (logger , logging .DEBUG , "{} Added identifier {} with value {} to list of search filters" .format (logprefix , identifier , value ), context .state )
222
+ satosa_logging (logger , logging .DEBUG , "{} Added search filter value {} to list of search filters" .format (logprefix , value ), context .state )
167
223
168
224
# Initialize an empty LDAP record. The first LDAP record found using the ordered
169
225
# list of search filter values will be the record used.
@@ -199,7 +255,7 @@ def process(self, context, data):
199
255
break
200
256
201
257
except Exception as err :
202
- satosa_logging (logger , logging .ERROR , "{} Caught exception: {0 }" .format (logprefix , err ), None )
258
+ satosa_logging (logger , logging .ERROR , "{} Caught exception: {}" .format (logprefix , err ), context . state )
203
259
return super ().process (context , data )
204
260
205
261
else :
0 commit comments