Skip to content

Commit 901f9bd

Browse files
authored
Import the conditional authproc-filters (#1836)
* Feature: add optional precondition to authproc-filters to be able to run them conditionally * Import rule-inserter code from cirrusidentity/simplesamlphp-module-general to accomodate for more complex scenarios
1 parent de98fc5 commit 901f9bd

File tree

5 files changed

+253
-1
lines changed

5 files changed

+253
-1
lines changed

docs/simplesamlphp-authproc.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,20 @@ $metadata['https://example.org/saml-idp'] = [
111111

112112
The example above is in `saml20-idp-hosted`.
113113

114+
## Preconditional filters
115+
116+
Any filter can be configured with a precondition that will determine whether or not a filter should run.
117+
The condition is represented as a string that will be evaluated, similar to the `core:PHP` filter. It also has
118+
the `$attributes` and `$state` variable available for use.
119+
The code must return either `true` to run the filter, or `false` to skip it.
120+
121+
```php
122+
'authproc' => [
123+
40 => 'core:TargetedID',
124+
'%precondition' => 'return $attributes["displayName"] === "John Doe";',
125+
],
126+
```
127+
114128
## Auth Proc Filters included in the SimpleSAMLphp distribution
115129

116130
The following filters are included in the SimpleSAMLphp distribution:

src/SimpleSAML/Auth/ProcessingChain.php

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use function count;
1818
use function is_array;
1919
use function is_string;
20+
use function sprintf;
2021
use function str_replace;
2122
use function var_export;
2223

@@ -204,7 +205,9 @@ public function processState(array &$state): void
204205
try {
205206
while (count($state[self::FILTERS_INDEX]) > 0) {
206207
$filter = array_shift($state[self::FILTERS_INDEX]);
207-
$filter->process($state);
208+
if ($filter->checkPrecondition($state) === true) {
209+
$filter->process($state);
210+
}
208211
}
209212
} catch (Error\Exception $e) {
210213
// No need to convert the exception
@@ -319,4 +322,40 @@ public static function fetchProcessedState(string $id): ?array
319322
{
320323
return State::loadState($id, self::COMPLETED_STAGE);
321324
}
325+
326+
327+
/**
328+
* @param array $state
329+
* @psalm-param array{"\\\SimpleSAML\\\Auth\\\ProcessingChain.filters": array} $state
330+
* @param ProcessingFilter[] $authProcs
331+
*/
332+
public static function insertFilters(array &$state, array $authProcs): void
333+
{
334+
if (count($authProcs) === 0) {
335+
return;
336+
}
337+
338+
Logger::debug(sprintf(
339+
'ProcessingChainRuleInserter: Adding %d additional filters before remaining %d',
340+
count($authProcs),
341+
count($state[self::FILTERS_INDEX]),
342+
));
343+
344+
array_splice($state[self::FILTERS_INDEX], 0, 0, $authProcs);
345+
}
346+
347+
348+
/**
349+
* @param array $state
350+
* @psalm-param array{"\\\SimpleSAML\\\Auth\\\ProcessingChain.filters": array} $state
351+
* @param array $authProcConfigs
352+
* @return \SimpleSAML\Auth\ProcessingFilter[]
353+
*/
354+
public static function createAndInsertFilters(array &$state, array $authProcConfigs): array
355+
{
356+
$filters = self::parseFilterList($authProcConfigs);
357+
self::insertFilters($state, $filters);
358+
359+
return $filters;
360+
}
322361
}

src/SimpleSAML/Auth/ProcessingFilter.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ abstract class ProcessingFilter
3838
*/
3939
public int $priority = 50;
4040

41+
/**
42+
* The condition to run this filter.
43+
*
44+
* Used to control whether or not to run this filter.
45+
*/
46+
public string $precondition = 'return true;';
47+
4148

4249
/**
4350
* Constructor for a processing filter.
@@ -54,6 +61,30 @@ public function __construct(array &$config, /** @scrutinizer ignore-unused */ mi
5461
$this->priority = $config['%priority'];
5562
unset($config['%priority']);
5663
}
64+
65+
if (array_key_exists('%precondition', $config)) {
66+
$this->precondition = $config['%precondition'];
67+
unset($config['%precondition']);
68+
}
69+
}
70+
71+
72+
/**
73+
* Test whether or not this filter must run.
74+
*
75+
* @param array &$state The request we are currently processing.
76+
* @return bool
77+
*/
78+
public function checkPrecondition(array &$state): bool
79+
{
80+
$function = /** @return bool */ function (
81+
array &$attributes,
82+
array &$state
83+
) {
84+
return eval($this->precondition);
85+
};
86+
87+
return $function($state['Attributes'], $state) === true;
5788
}
5889

5990

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Test\SimpleSAML\Auth;
6+
7+
use PHPUnit\Framework\TestCase;
8+
use SimpleSAML\Auth\ProcessingChain;
9+
use SimpleSAML\Configuration;
10+
use SimpleSAML\Module\core\Auth\Process\AttributeAdd;
11+
use SimpleSAML\Module\core\Auth\Process\AttributeLimit;
12+
use SimpleSAML\Module\core\Auth\Process\AttributeMap;
13+
14+
class ProcessingChainTest extends TestCase
15+
{
16+
/**
17+
*/
18+
protected function setUp(): void
19+
{
20+
Configuration::loadFromArray([], '[ARRAY]', 'simplesaml');
21+
}
22+
23+
24+
public function testInsertAuthProcs(): void
25+
{
26+
$config = [];
27+
$authProcs = [
28+
new AttributeAdd($config, []),
29+
new AttributeMap($config, []),
30+
];
31+
$state = [
32+
ProcessingChain::FILTERS_INDEX => [
33+
new AttributeLimit($config, [])
34+
]
35+
];
36+
$this->assertCount(1, $state[ProcessingChain::FILTERS_INDEX], 'Unexpected number of filters preinsert');
37+
38+
ProcessingChain::insertFilters($state, $authProcs);
39+
40+
$filterInChain = $state[ProcessingChain::FILTERS_INDEX];
41+
$this->assertCount(3, $filterInChain);
42+
$this->assertInstanceOf(AttributeAdd::class, $filterInChain[0]);
43+
$this->assertInstanceOf(AttributeMap::class, $filterInChain[1]);
44+
$this->assertInstanceOf(AttributeLimit::class, $filterInChain[2]);
45+
}
46+
47+
public function testInsertAuthFromConfigs(): void
48+
{
49+
$config = [];
50+
$authProcsConfigs = [
51+
[
52+
'class' => 'core:AttributeAdd',
53+
'source' => ['myidp'],
54+
],
55+
];
56+
$state = [
57+
ProcessingChain::FILTERS_INDEX => [
58+
new AttributeLimit($config, [])
59+
]
60+
];
61+
$this->assertCount(1, $state[ProcessingChain::FILTERS_INDEX], 'Unexpected number of filters preinsert');
62+
63+
ProcessingChain::createAndInsertFilters($state, $authProcsConfigs);
64+
65+
$filterInChain = $state[ProcessingChain::FILTERS_INDEX];
66+
$this->assertCount(2, $filterInChain);
67+
$this->assertInstanceOf(AttributeAdd::class, $filterInChain[0]);
68+
$this->assertInstanceOf(AttributeLimit::class, $filterInChain[1]);
69+
}
70+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleSAML\Test\Auth\ProcessingFilter;
6+
7+
use Exception;
8+
use PHPUnit\Framework\TestCase;
9+
use SimpleSAML\Auth\ProcessingFilter as AuthProcFilter;
10+
use SimpleSAML\Module\core\Auth\Process\AttributeAlter;
11+
12+
/**
13+
* Test for the ProccessingFilter.
14+
*
15+
* @covers \SimpleSAML\Auth\ProcessingFilter
16+
*/
17+
class AttributeAlterTest extends TestCase
18+
{
19+
/**
20+
* Helper function to run the filter with a given configuration.
21+
*
22+
* @param array $config The filter configuration.
23+
* @param array $request The request state.
24+
* @return \SimpleSAML\Auth\ProcessFilter.
25+
*/
26+
private static function processFilter(array $config, array $request): AuthProcFilter
27+
{
28+
return new AttributeAlter($config, null);
29+
}
30+
31+
32+
/**
33+
* Test that a filter without precondition will run.
34+
*/
35+
public function testWithoutPrecondition(): void
36+
{
37+
$config = [
38+
'subject' => 'test',
39+
'pattern' => '/wrong/',
40+
'replacement' => 'right',
41+
];
42+
43+
$request = [
44+
'Attributes' => [
45+
'test' => ['somethingiswrong'],
46+
],
47+
];
48+
49+
$filter = self::processFilter($config, $request);
50+
$this->assertTrue($filter->checkPrecondition($request));
51+
}
52+
53+
54+
/**
55+
* Test that a filter with a precondition evaluating to true will run.
56+
*/
57+
public function testWithPreconditionTrue(): void
58+
{
59+
$config = [
60+
'%precondition' => 'return true;',
61+
'subject' => 'test',
62+
'pattern' => '/wrong/',
63+
'replacement' => 'right',
64+
];
65+
66+
$request = [
67+
'Attributes' => [
68+
'test' => ['somethingiswrong'],
69+
],
70+
];
71+
72+
$filter = self::processFilter($config, $request);
73+
$this->assertTrue($filter->checkPrecondition($request));
74+
}
75+
76+
77+
/**
78+
* Test that a filter with a precondition evaluating to false will not run.
79+
*/
80+
public function testWithPreconditionFalse(): void
81+
{
82+
$config = [
83+
'%precondition' => 'return false;',
84+
'subject' => 'test',
85+
'pattern' => '/wrong/',
86+
'replacement' => 'right',
87+
];
88+
89+
$request = [
90+
'Attributes' => [
91+
'test' => ['somethingiswrong'],
92+
],
93+
];
94+
95+
$filter = self::processFilter($config, $request);
96+
$this->assertFalse($filter->checkPrecondition($request));
97+
}
98+
}

0 commit comments

Comments
 (0)