Skip to content
This repository was archived by the owner on Sep 19, 2022. It is now read-only.

Commit 61dc7ce

Browse files
author
Dominik František Bučík
committed
feat: 🎸 IsEligible authProc filter
1 parent 602421b commit 61dc7ce

File tree

7 files changed

+383
-0
lines changed

7 files changed

+383
-0
lines changed

config-templates/processFilterConfigurations-example.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,3 +338,55 @@ Configuration options:
338338
'interface' => 'ldap',
339339
],
340340
```
341+
342+
## IsEligible
343+
344+
Checks the eligibility timestamp. If the value is not older than `validity_period_months`, it lets the user in. Otherwise, user is forced to reauthenticate using other identity.
345+
346+
Configuration options:
347+
* `trigger_attribute`: attribute name which has to be consumed by the service (in the SP remote metadata part attributes) to run this filter. If the attribute specified by the option is not released, filter is skipped.
348+
* `eligible_last_seen_timestamp_attribute`: attribute containing the timestamp of last eligible login (timestamp of this event). Has to be available in `$state[PerunConstants::Attributes]`.
349+
* `validity_period_months`: if the timestamp is older that this number of months, user won't be let in.
350+
* `translations`: inline translations for the unauthorized page.
351+
```php
352+
25 => [
353+
'class' => 'perun:IsEligible',
354+
'trigger_attribute' => 'isCesnetEligible',
355+
'eligible_last_seen_timestamp_attribute' => 'isCesnetEligibleLastSeen',
356+
'validity_period_months' => 12,
357+
'translations' => [
358+
'old_value_header'=> [
359+
'en' => '...',
360+
'cs' => '...',
361+
],
362+
'old_value_text'=> [
363+
'en' => '...',
364+
'cs' => '...',
365+
],
366+
'old_value_button'=> [
367+
'en' => '...',
368+
'cs' => '...',
369+
],
370+
'old_value_contact'=> [
371+
'en' => '...',
372+
'cs' => '...',
373+
],
374+
'no_value_header'=> [
375+
'en' => '...',
376+
'cs' => '...',
377+
],
378+
'no_value_text'=> [
379+
'en' => '...',
380+
'cs' => '...',
381+
],
382+
'no_value_button'=> [
383+
'en' => '...',
384+
'cs' => '...',
385+
],
386+
'no_value_contact'=> [
387+
'en' => '...',
388+
'cs' => '...',
389+
],
390+
]
391+
],
392+
```

dictionaries/perun.definition.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,5 +162,21 @@
162162
"sp_authorize_notify_button": {
163163
"en": "Proceed to registration",
164164
"cs": "Pokračovat na registrační stránku"
165+
},
166+
"403_is_eligible_default_header": {
167+
"en": "Access denied",
168+
"cs": "Přístup zamítnut"
169+
},
170+
"403_is_eligible_default_text": {
171+
"en": "Your account does not meet the criteria for accessing the service. Please log in with other account.",
172+
"cs": "Přístup ke službě byl zamítnut, protože Váš účet nesplňuje pomdínky přístupu. Přihlaste se, prosíme, pomocí jiného účtu."
173+
},
174+
"403_is_eligible_default_button": {
175+
"en": "Continue with other account",
176+
"cs": "Pokračovat"
177+
},
178+
"403_is_eligible_default_contact": {
179+
"en": "If you think you have used an account which meets the criteria, and you are still prevented from logging in to the service, please contact us at",
180+
"cs": "Pokud si myslíte, že používáte správný účet a přístup je Vám odmítnut neprávem, prosíme kontakujte nás na"
165181
}
166182
}

lib/Auth/Process/IsEligible.php

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleSAML\Module\perun\Auth\Process;
6+
7+
use DateTime;
8+
use SimpleSAML\Auth\ProcessingFilter;
9+
use SimpleSAML\Auth\State;
10+
use SimpleSAML\Configuration;
11+
use SimpleSAML\Error\Exception;
12+
use SimpleSAML\Locale\Translate;
13+
use SimpleSAML\Logger;
14+
use SimpleSAML\Module;
15+
use SimpleSAML\Module\perun\PerunConstants;
16+
use SimpleSAML\Session;
17+
use SimpleSAML\Utils\HTTP;
18+
19+
class IsEligible extends ProcessingFilter
20+
{
21+
public const STAGE = 'perun:IsEligible';
22+
23+
public const DEBUG_PREFIX = self::STAGE . ' - ';
24+
25+
public const REDIRECT = 'perun/403_is_eligible.php';
26+
27+
public const TEMPLATE = 'perun:403-is-eligible-tpl.php';
28+
29+
public const DEFAULT_VALIDITY_PERIOD_MONTHS = 12;
30+
31+
public const PARAM_STATE_ID = PerunConstants::STATE_ID;
32+
33+
public const PARAM_RESTART_URL = 'restart_url';
34+
35+
public const TRIGGER_ATTRIBUTE = 'trigger_attribute';
36+
37+
public const ELIGIBLE_LAST_SEEN_TIMESTAMP_ATTRIBUTE = 'eligible_last_seen_timestamp_attribute';
38+
39+
public const VALIDITY_PERIOD_MONTHS = 'validity_period_months';
40+
41+
public const TRANSLATIONS = 'translations';
42+
43+
public const OLD_VALUE_HEADER_TRANSLATION = 'old_value_header';
44+
45+
public const OLD_VALUE_TEXT_TRANSLATION = 'old_value_text';
46+
47+
public const OLD_VALUE_BUTTON_TRANSLATION = 'old_value_button';
48+
49+
public const OLD_VALUE_CONTACT_TRANSLATION = 'old_value_contact';
50+
51+
public const NO_VALUE_HEADER_TRANSLATION = 'no_value_header';
52+
53+
public const NO_VALUE_TEXT_TRANSLATION = 'no_value_text';
54+
55+
public const NO_VALUE_BUTTON_TRANSLATION = 'no_value_button';
56+
57+
public const NO_VALUE_CONTACT_TRANSLATION = 'no_value_contact';
58+
59+
public const HEADER_TRANSLATION = 'header_translation';
60+
61+
public const TEXT_TRANSLATION = 'text_translation';
62+
63+
public const BUTTON_TRANSLATION = 'button_translation';
64+
65+
public const CONTACT_TRANSLATION = 'contact_translation';
66+
67+
private $triggerAttribute;
68+
69+
private $timestampAttribute;
70+
71+
private $validityPeriodMonths;
72+
73+
private $filterConfig;
74+
75+
private $translations;
76+
77+
public function __construct($config, $reserved)
78+
{
79+
parent::__construct($config, $reserved);
80+
$this->filterConfig = Configuration::loadFromArray($config);
81+
82+
$this->triggerAttribute = $this->filterConfig->getString(self::TRIGGER_ATTRIBUTE);
83+
$this->timestampAttribute = $this->filterConfig->getString(self::ELIGIBLE_LAST_SEEN_TIMESTAMP_ATTRIBUTE);
84+
85+
$this->validityPeriodMonths = $this->filterConfig->getInteger(
86+
self::VALIDITY_PERIOD_MONTHS,
87+
self::DEFAULT_VALIDITY_PERIOD_MONTHS
88+
);
89+
90+
$this->translations = $this->filterConfig->getArray(self::TRANSLATIONS, []);
91+
}
92+
93+
public function process(&$request)
94+
{
95+
assert(is_array($request));
96+
assert(!empty($request[PerunConstants::DESTINATION]));
97+
assert(!empty($request[PerunConstants::ATTRIBUTES]));
98+
99+
$attributesReleasedToSp = [];
100+
if (!empty($request[PerunConstants::DESTINATION][PerunConstants::DESTINATION_ATTRIBUTES])) {
101+
$attributesReleasedToSp = $request[PerunConstants::DESTINATION][PerunConstants::DESTINATION_ATTRIBUTES];
102+
}
103+
104+
if (!in_array($this->triggerAttribute, $attributesReleasedToSp, true)) {
105+
Logger::info(
106+
self::DEBUG_PREFIX . 'SP does not consume the trigger attribute \'' . $this->triggerAttribute . '\'. Terminating execution of this filter.'
107+
);
108+
return;
109+
}
110+
111+
$lastSeenEligibleTimestampString = null;
112+
if (!empty($request[PerunConstants::ATTRIBUTES][$this->timestampAttribute])) {
113+
$lastSeenEligibleTimestampString = $request[PerunConstants::ATTRIBUTES][$this->timestampAttribute][0];
114+
} else {
115+
Logger::info(
116+
self::DEBUG_PREFIX . 'Timestamp of the last seen eligibility is empty, cannot let user go through. Redirecting to unauthorized explanation page.'
117+
);
118+
$this->unauthorized($request, false);
119+
}
120+
121+
$lastSeenEligibleTimestamp = DateTime::createFromFormat('Y-m-d H:i:s', $lastSeenEligibleTimestampString);
122+
$lastSeenEligibleTimestamp = $lastSeenEligibleTimestamp->modify('+' . $this->validityPeriodMonths . 'months');
123+
$now = new DateTime();
124+
125+
if ($lastSeenEligibleTimestamp < $now) {
126+
Logger::info(
127+
self::DEBUG_PREFIX . 'Last seen eligibility timestamp value \'' . $lastSeenEligibleTimestampString . '\' is out of the defined period of ' . $this->validityPeriodMonths . ' months until now. Redirecting to unauthorized explanation page.'
128+
);
129+
$this->unauthorized($request, true);
130+
}
131+
Logger::info(
132+
self::DEBUG_PREFIX . 'Last seen eligibility timestamp value \'' . $lastSeenEligibleTimestampString . '\' is inside the defined period of ' . $this->validityPeriodMonths . ' months until now. Continue to next filter.'
133+
);
134+
}
135+
136+
public function unauthorized(&$state, $hasValue)
137+
{
138+
$translations = $this->loadLocalTranslations($hasValue);
139+
$state[self::TRANSLATIONS] = $translations;
140+
141+
$state = State::saveState($state, self::STAGE);
142+
143+
try {
144+
$session = Session::getSessionFromRequest();
145+
$session->doLogout('default-sp');
146+
} catch (Exception|\Exception $exception) {
147+
Logger::warning(self::DEBUG_PREFIX . 'Error when logging user out. Logout has failed!');
148+
Logger::debug(
149+
self::DEBUG_PREFIX . 'Details about the logout failure \'' . $exception->getMessage() . '\'.'
150+
);
151+
}
152+
$url = Module::getModuleURL(self::REDIRECT);
153+
$params = [
154+
self::PARAM_STATE_ID => $state,
155+
];
156+
157+
HTTP::redirectTrustedURL($url, $params);
158+
}
159+
160+
public static function loadLocalTranslation(
161+
Translate $translator,
162+
string $key,
163+
array $translations,
164+
string $defaultTranslationKey
165+
): string {
166+
if (!empty($translations[$key])) {
167+
$translation = $translations[$key];
168+
$translationKey = '{' . self::STAGE . '_' . $key . '}';
169+
$translator->includeInlineTranslation($translationKey, $translation);
170+
return $translationKey;
171+
}
172+
return $defaultTranslationKey;
173+
}
174+
175+
private function loadLocalTranslations($hasValue): array
176+
{
177+
if ($hasValue) {
178+
$header = $this->loadTranslation(self::OLD_VALUE_HEADER_TRANSLATION, $this->translations);
179+
$text = $this->loadTranslation(self::OLD_VALUE_TEXT_TRANSLATION, $this->translations);
180+
$button = $this->loadTranslation(self::OLD_VALUE_BUTTON_TRANSLATION, $this->translations);
181+
$contact = $this->loadTranslation(self::OLD_VALUE_CONTACT_TRANSLATION, $this->translations);
182+
} else {
183+
$header = $this->loadTranslation(self::NO_VALUE_HEADER_TRANSLATION, $this->translations);
184+
$text = $this->loadTranslation(self::NO_VALUE_TEXT_TRANSLATION, $this->translations);
185+
$button = $this->loadTranslation(self::NO_VALUE_BUTTON_TRANSLATION, $this->translations);
186+
$contact = $this->loadTranslation(self::NO_VALUE_CONTACT_TRANSLATION, $this->translations);
187+
}
188+
return [
189+
self::HEADER_TRANSLATION => $header,
190+
self::TEXT_TRANSLATION => $text,
191+
self::BUTTON_TRANSLATION => $button,
192+
self::CONTACT_TRANSLATION => $contact,
193+
];
194+
}
195+
196+
private function loadTranslation(string $key, array $translations): array
197+
{
198+
if (array_key_exists($key, $translations)) {
199+
return $translations[$key];
200+
}
201+
return [];
202+
}
203+
}

lib/PerunConstants.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,8 @@ class PerunConstants
4141
public const SP_ADMINISTRATION_CONTACT = 'administrationContact';
4242

4343
public const SP_NAME = 'name';
44+
45+
public const DESTINATION = 'Destination';
46+
47+
public const DESTINATION_ATTRIBUTES = 'attributes';
4448
}

templates/403-is-eligible-tpl.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php declare(strict_types=1);
2+
3+
use SimpleSAML\Configuration;
4+
use SimpleSAML\Module;
5+
use SimpleSAML\Module\perun\Auth\Process\IsEligible;
6+
7+
header('HTTP/1.0 403 Forbidden');
8+
9+
$headerKey = $this->data[IsEligible::HEADER_TRANSLATION];
10+
$textKey = $this->data[IsEligible::TEXT_TRANSLATION];
11+
$buttonKey = $this->data[IsEligible::BUTTON_TRANSLATION];
12+
$contactKey = $this->data[IsEligible::CONTACT_TRANSLATION];
13+
14+
$restartUrl = $this->data[IsEligible::PARAM_RESTART_URL] ?: null;
15+
$config = Configuration::getInstance();
16+
$supportAddress = $config->getString('technicalcontact_email', 'N/A');
17+
18+
$this->data['head'] = '<link rel="stylesheet" media="screen" type="text/css" href="' .
19+
Module::getModuleUrl('perun/res/css/perun.css') . '" />';
20+
21+
$this->data['header'] = $this->t($headerKey);
22+
23+
24+
25+
$this->includeAtTemplateBase('includes/header.php');
26+
27+
?>
28+
29+
<div class="row">
30+
<div>
31+
<p><?php echo $this->t($textKey); ?></p>
32+
<?php if (!empty($restartUrl)): ?>
33+
<p>
34+
<a class="btn btn-lg btn-block btn-primary" href="<?php echo $restartUrl ?>">
35+
<?php echo $this->t($buttonKey); ?>
36+
</a>
37+
</p>
38+
<?php endif ?>
39+
<p><?php echo $this->t(
40+
$contactKey
41+
); ?> <a href="mailto:<?php echo $supportAddress; ?>"><?php echo $supportAddress; ?></a></p>
42+
</div>
43+
</div>
44+
45+
46+
<?php
47+
48+
$this->includeAtTemplateBase('includes/footer.php');

www/403_is_eligible.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use SimpleSAML\Auth\State;
6+
use SimpleSAML\Configuration;
7+
use SimpleSAML\Error\BadRequest;
8+
use SimpleSAML\Module\perun\Auth\Process\IsEligible;
9+
use SimpleSAML\XHTML\Template;
10+
11+
if (empty($_REQUEST[IsEligible::PARAM_STATE_ID])) {
12+
throw new BadRequest('Missing required \'' . IsEligible::PARAM_STATE_ID . '\' query parameter.');
13+
}
14+
15+
$state = State::loadState($_REQUEST[IsEligible::PARAM_STATE_ID], IsEligible::STAGE);
16+
17+
$config = Configuration::getInstance();
18+
$t = new Template($config, IsEligible::TEMPLATE);
19+
20+
$restartUrl = $state[State::RESTART] ?: null;
21+
22+
$headerKey = '{perun:perun:403_is_eligible_default_header}';
23+
$textKey = '{perun:perun:403_is_eligible_default_text}';
24+
$buttonKey = '{perun:perun:403_is_eligible_default_button}';
25+
$contactKey = '{perun:perun:403_is_eligible_default_contact}';
26+
27+
$translations = $state[IsEligible::TRANSLATIONS] ?: [];
28+
if (!empty($translations)) {
29+
$translator = $t->getTranslator();
30+
$headerKey = IsEligible::loadLocalTranslation(
31+
$translator,
32+
IsEligible::HEADER_TRANSLATION,
33+
$translations,
34+
$headerKey
35+
);
36+
$textKey = IsEligible::loadLocalTranslation($translator, IsEligible::TEXT_TRANSLATION, $translations, $textKey);
37+
$buttonKey = IsEligible::loadLocalTranslation(
38+
$translator,
39+
IsEligible::BUTTON_TRANSLATION,
40+
$translations,
41+
$buttonKey
42+
);
43+
$contactKey = IsEligible::loadLocalTranslation(
44+
$translator,
45+
IsEligible::CONTACT_TRANSLATION,
46+
$translations,
47+
$contactKey
48+
);
49+
}
50+
51+
$t->data[IsEligible::PARAM_RESTART_URL] = $restartUrl;
52+
$t->data[IsEligible::HEADER_TRANSLATION] = $headerKey;
53+
$t->data[IsEligible::TEXT_TRANSLATION] = $textKey;
54+
$t->data[IsEligible::BUTTON_TRANSLATION] = $buttonKey;
55+
$t->data[IsEligible::CONTACT_TRANSLATION] = $contactKey;
56+
57+
$t->show();

0 commit comments

Comments
 (0)