Skip to content

Commit 58c7411

Browse files
committed
Add Service Provider entity ID based filtering
Allow restricting authorization rules to specific SPs by adding spEntityIDs arrays to attribute configurations. Rules with spEntityIDs only apply when the current SP matches the allowed list, enabling fine-grained access control per service provider.
1 parent 84b4664 commit 58c7411

File tree

3 files changed

+185
-2
lines changed

3 files changed

+185
-2
lines changed

docs/authorize.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,23 @@ Additionally, some helpful instructions are shown.
155155
],
156156
],
157157
],
158+
159+
You can restrict an attribute allowance to a list of Service Providers.
160+
161+
```php
162+
'authproc.sp' => [
163+
60 => array[
164+
'class' => 'authorize:Authorize',
165+
'uid' => [
166+
'/.*@students.example.edu$/',
167+
'/^(stu1|stu2|stu3)@example.edu$/',
168+
'spEntityIDs' => [
169+
'https://example.com/sp1',
170+
'https://example.com/sp2'
171+
]
172+
]
173+
]
174+
]
175+
```
176+
158177
```

src/Auth/Process/Authorize.php

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ class Authorize extends Auth\ProcessingFilter
6161
/**
6262
* Array of valid users. Each element is a regular expression. You should
6363
* use \ to escape special chars, like '.' etc.
64+
* can also contain 'spEntityIDs' arrays to restrict rules to specific SPs.
6465
*
6566
* @var array<mixed>
6667
*/
@@ -137,6 +138,19 @@ public function __construct(array $config, $reserved)
137138
));
138139
}
139140

141+
// Extract spEntityIDs if present
142+
$spEntityIDs = null;
143+
if (isset($values['spEntityIDs'])) {
144+
if (!is_array($values['spEntityIDs'])) {
145+
throw new Exception(sprintf(
146+
'Filter Authorize: spEntityIDs must be an array for attribute: %s',
147+
var_export($attribute, true),
148+
));
149+
}
150+
$spEntityIDs = $values['spEntityIDs'];
151+
unset($values['spEntityIDs']);
152+
}
153+
140154
foreach ($values as $value) {
141155
if (!is_string($value)) {
142156
throw new Exception(sprintf(
@@ -147,7 +161,20 @@ public function __construct(array $config, $reserved)
147161
));
148162
}
149163
}
150-
$this->valid_attribute_values[$attribute] = $values;
164+
165+
// For backward compatibility, if no spEntityIDs were found,
166+
// store the values directly in the old format
167+
if ($spEntityIDs === null) {
168+
$this->valid_attribute_values[$attribute] = [
169+
'values' => $values,
170+
'spEntityIDs' => null,
171+
];
172+
} else {
173+
$this->valid_attribute_values[$attribute] = [
174+
'values' => $values,
175+
'spEntityIDs' => $spEntityIDs,
176+
];
177+
}
151178
}
152179
}
153180

@@ -171,9 +198,27 @@ public function process(array &$state): void
171198
}
172199
$state['authprocAuthorize_errorURL'] = $this->errorURL;
173200
$state['authprocAuthorize_allow_reauthentication'] = $this->allow_reauthentication;
201+
// Get current SP EntityID from state
202+
$currentSpEntityId = null;
203+
if (isset($state['saml:sp:State']['core:SP'])) {
204+
$currentSpEntityId = $state['saml:sp:State']['core:SP'];
205+
} elseif (isset($state['Destination']['entityid'])) {
206+
$currentSpEntityId = $state['Destination']['entityid'];
207+
}
208+
174209
$arrayUtils = new Utils\Arrays();
175-
foreach ($this->valid_attribute_values as $name => $patterns) {
210+
foreach ($this->valid_attribute_values as $name => $ruleConfig) {
176211
if (array_key_exists($name, $attributes)) {
212+
$patterns = $ruleConfig['values'];
213+
$spEntityIDs = $ruleConfig['spEntityIDs'];
214+
215+
// If spEntityIDs is specified, check if current SP is in the list
216+
if ($spEntityIDs !== null) {
217+
if ($currentSpEntityId === null || !in_array($currentSpEntityId, $spEntityIDs, true)) {
218+
continue; // Skip this rule if SP is not specified or not in allowed list
219+
}
220+
}
221+
177222
foreach ($patterns as $pattern) {
178223
$values = $arrayUtils->arrayize($attributes[$name]);
179224
foreach ($values as $value) {

tests/src/Auth/Process/AuthorizeTest.php

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,4 +240,123 @@ public static function showUserAttributeScenarioProvider(): array
240240
[['uid' => 'stu3@example.edu', 'mail' => 'user@example.edu'], false, true, 'user@example.edu'],
241241
];
242242
}
243+
244+
/**
245+
* Test SP restriction functionality
246+
*
247+
* @param array $userAttributes The attributes to test
248+
* @param string|null $spEntityId The SP Entity ID in the state
249+
* @param bool $isAuthorized Should the user be authorized
250+
*/
251+
#[DataProvider('spRestrictionScenarioProvider')]
252+
public function testSpRestriction(array $userAttributes, ?string $spEntityId, bool $isAuthorized): void
253+
{
254+
$attributeUtils = new Utils\Attributes();
255+
$userAttributes = $attributeUtils->normalizeAttributesArray($userAttributes);
256+
$config = [
257+
'uid' => [
258+
'/.*@example.com$/',
259+
'spEntityIDs' => [
260+
'https://sp1.example.com',
261+
'https://sp2.example.com',
262+
],
263+
],
264+
'group' => [
265+
'/^admins$/',
266+
'spEntityIDs' => [
267+
'https://admin.example.com',
268+
],
269+
],
270+
];
271+
272+
$state = ['Attributes' => $userAttributes];
273+
if ($spEntityId !== null) {
274+
$state['saml:sp:State']['core:SP'] = $spEntityId;
275+
}
276+
277+
$resultState = $this->processFilter($config, $state);
278+
$resultAuthorized = isset($resultState['NOT_AUTHORIZED']) ? false : true;
279+
$this->assertEquals($isAuthorized, $resultAuthorized);
280+
}
281+
282+
/**
283+
* @return array
284+
*/
285+
public static function spRestrictionScenarioProvider(): array
286+
{
287+
return [
288+
// Should be allowed - matching attribute and SP
289+
[['uid' => 'user@example.com'], 'https://sp1.example.com', true],
290+
[['uid' => 'user@example.com'], 'https://sp2.example.com', true],
291+
[['group' => 'admins'], 'https://admin.example.com', true],
292+
293+
// Should be denied - matching attribute but wrong SP
294+
[['uid' => 'user@example.com'], 'https://wrong.example.com', false],
295+
[['group' => 'admins'], 'https://sp1.example.com', false],
296+
297+
// Should be denied - no SP specified but attribute would match
298+
[['uid' => 'user@example.com'], null, false],
299+
[['group' => 'admins'], null, false],
300+
301+
// Should be denied - wrong attribute regardless of SP
302+
[['uid' => 'user@wrong.com'], 'https://sp1.example.com', false],
303+
[['group' => 'users'], 'https://admin.example.com', false],
304+
];
305+
}
306+
307+
/**
308+
* Test mixed SP and non-SP rules
309+
*
310+
* @param array $userAttributes The attributes to test
311+
* @param string|null $spEntityId The SP Entity ID in the state
312+
* @param bool $isAuthorized Should the user be authorized
313+
*/
314+
#[DataProvider('mixedRulesScenarioProvider')]
315+
public function testMixedSpAndNonSpRules(array $userAttributes, ?string $spEntityId, bool $isAuthorized): void
316+
{
317+
$attributeUtils = new Utils\Attributes();
318+
$userAttributes = $attributeUtils->normalizeAttributesArray($userAttributes);
319+
$config = [
320+
// Rule with SP restriction
321+
'uid' => [
322+
'/.*@restricted.com$/',
323+
'spEntityIDs' => ['https://restricted.example.com'],
324+
],
325+
// Rule without SP restriction (should work for all SPs)
326+
'role' => [
327+
'/^admin$/',
328+
'/^superuser$/',
329+
],
330+
];
331+
332+
$state = ['Attributes' => $userAttributes];
333+
if ($spEntityId !== null) {
334+
$state['saml:sp:State']['core:SP'] = $spEntityId;
335+
}
336+
337+
$resultState = $this->processFilter($config, $state);
338+
$resultAuthorized = isset($resultState['NOT_AUTHORIZED']) ? false : true;
339+
$this->assertEquals($isAuthorized, $resultAuthorized);
340+
}
341+
342+
/**
343+
* @return array
344+
*/
345+
public static function mixedRulesScenarioProvider(): array
346+
{
347+
return [
348+
// Should be allowed - role rule matches (no SP restriction)
349+
[['role' => 'admin'], 'https://any.example.com', true],
350+
[['role' => 'superuser'], null, true],
351+
352+
// Should be allowed - uid rule matches and SP is correct
353+
[['uid' => 'user@restricted.com'], 'https://restricted.example.com', true],
354+
355+
// Should be denied - uid rule matches but SP is wrong
356+
[['uid' => 'user@restricted.com'], 'https://other.example.com', false],
357+
358+
// Should be denied - no matching rules
359+
[['uid' => 'user@other.com', 'role' => 'user'], 'https://any.example.com', false],
360+
];
361+
}
243362
}

0 commit comments

Comments
 (0)