Skip to content

Commit dc6fa38

Browse files
committed
Merge #4 Add event based provisioning
2 parents f098432 + 42b21af commit dc6fa38

File tree

10 files changed

+1064
-4
lines changed

10 files changed

+1064
-4
lines changed

lib/AppInfo/Application.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
use OCA\UserOIDC\Listener\TimezoneHandlingListener;
3434
use OCA\UserOIDC\Service\ID4MeService;
3535
use OCA\UserOIDC\Service\SettingsService;
36+
use OCA\UserOIDC\Service\ProvisioningService;
37+
use OCA\UserOIDC\Service\ProvisioningEventService;
3638
use OCA\UserOIDC\User\Backend;
3739
use OCP\AppFramework\App;
3840
use OCP\AppFramework\Bootstrap\IBootContext;
@@ -57,6 +59,12 @@ public function __construct(array $urlParams = []) {
5759
}
5860

5961
public function register(IRegistrationContext $context): void {
62+
/**
63+
* MagentaCLOUD replaces the normal provisioning behavior with the event based one
64+
* as a decoupled way to force event based approach
65+
*/
66+
$context->registerServiceAlias(ProvisioningService::class, ProvisioningEventService::class);
67+
6068
/** @var IUserManager $userManager */
6169
$userManager = $this->getContainer()->get(IUserManager::class);
6270

lib/Controller/LoginController.php

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
use OCA\UserOIDC\Service\DiscoveryService;
3737
use OCA\UserOIDC\Service\LdapService;
3838
use OCA\UserOIDC\Service\ProviderService;
39+
use OCA\UserOIDC\Service\EventProvisioningService;
40+
use OCA\UserOIDC\Service\ProvisioningDeniedException;
3941
use OCA\UserOIDC\Service\ProvisioningService;
4042
use OCA\UserOIDC\Vendor\Firebase\JWT\JWT;
4143
use OCA\UserOIDC\AppInfo\Application;
@@ -118,6 +120,9 @@ class LoginController extends BaseOidcController {
118120
/** @var SessionMapper */
119121
private $sessionMapper;
120122

123+
/** @var EventProvisioningService */
124+
private $eventProvisioningService;
125+
121126
/** @var ProvisioningService */
122127
private $provisioningService;
123128

@@ -475,7 +480,23 @@ public function code(string $state = '', string $code = '', string $scope = '',
475480

476481
// Provisioning
477482
if ($autoProvisionAllowed) {
478-
$user = $this->provisioningService->provisionUser($userId, $providerId, $idTokenPayload);
483+
// TODO: (proposal) refactor all provisioning strategies into event handlers
484+
$user = null;
485+
try {
486+
$user = $this->provisioningService->provisionUser($userId, $providerId, $idTokenPayload);
487+
} catch (ProvisioningDeniedException $denied) {
488+
// TODO MagentaCLOUD should upstream the exception handling
489+
$redirectUrl = $denied->getRedirectUrl();
490+
if ($redirectUrl === null) {
491+
$message = $this->l10n->t('Failed to provision user');
492+
return $this->build403TemplateResponse($message, Http::STATUS_BAD_REQUEST, ['reason' => $denied->getMessage()]);
493+
} else {
494+
// error response is a redirect, e.g. to a booking site
495+
// so that you can immediately get the registration page
496+
return new RedirectResponse($redirectUrl);
497+
}
498+
}
499+
// no default exception handling to pass on unittest assertion failures
479500
} else {
480501
// in case user is provisioned by user_ldap, userManager->search() triggers an ldap search which syncs the results
481502
// so new users will be directly available even if they were not synced before this login attempt
@@ -566,7 +587,8 @@ public function singleLogoutService() {
566587
$endSessionEndpoint .= '&client_id=' . $provider->getClientId();
567588
$shouldSendIdToken = $this->providerService->getSetting(
568589
$provider->getId(),
569-
ProviderService::SETTING_SEND_ID_TOKEN_HINT, '0'
590+
ProviderService::SETTING_SEND_ID_TOKEN_HINT,
591+
'0'
570592
) === '1';
571593
$idToken = $this->session->get(self::ID_TOKEN);
572594
if ($shouldSendIdToken && $idToken) {
@@ -715,8 +737,12 @@ public function backChannelLogout(string $providerIdentifier, string $logout_tok
715737
* @param bool|null $throttle
716738
* @return JSONResponse
717739
*/
718-
private function getBackchannelLogoutErrorResponse(string $error, string $description,
719-
array $throttleMetadata = [], ?bool $throttle = null): JSONResponse {
740+
private function getBackchannelLogoutErrorResponse(
741+
string $error,
742+
string $description,
743+
array $throttleMetadata = [],
744+
?bool $throttle = null
745+
): JSONResponse {
720746
$this->logger->debug('Backchannel logout error. ' . $error . ' ; ' . $description);
721747
$response = new JSONResponse(
722748
[
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
/*
3+
* @copyright Copyright (c) 2023 T-Systems International
4+
*
5+
* @author B. Rederlechner <bernd.rederlechner@t-systems.com>
6+
*
7+
* @license GNU AGPL version 3 or any later version
8+
*
9+
*/
10+
11+
declare(strict_types=1);
12+
13+
namespace OCA\UserOIDC\Event;
14+
15+
use OCP\EventDispatcher\Event;
16+
17+
/**
18+
* Event to provide custom mapping logic based on the OIDC token data
19+
* In order to avoid further processing the event propagation should be stopped
20+
* in the listener after processing as the value might get overwritten afterwards
21+
* by other listeners through $event->stopPropagation();
22+
*/
23+
class UserAccountChangeEvent extends Event {
24+
private $uid;
25+
private $displayname;
26+
private $mainEmail;
27+
private $quota;
28+
private $claims;
29+
private $result;
30+
31+
32+
public function __construct(string $uid, ?string $displayname, ?string $mainEmail, ?string $quota, object $claims, bool $accessAllowed = false) {
33+
parent::__construct();
34+
$this->uid = $uid;
35+
$this->displayname = $displayname;
36+
$this->mainEmail = $mainEmail;
37+
$this->quota = $quota;
38+
$this->claims = $claims;
39+
$this->result = new UserAccountChangeResult($accessAllowed, 'default');
40+
}
41+
42+
/**
43+
* @return get event username (uid)
44+
*/
45+
public function getUid(): string {
46+
return $this->uid;
47+
}
48+
49+
/**
50+
* @return get event displayname
51+
*/
52+
public function getDisplayName(): ?string {
53+
return $this->displayname;
54+
}
55+
56+
/**
57+
* @return get event main email
58+
*/
59+
public function getMainEmail(): ?string {
60+
return $this->mainEmail;
61+
}
62+
63+
/**
64+
* @return get event quota
65+
*/
66+
public function getQuota(): ?string {
67+
return $this->quota;
68+
}
69+
70+
/**
71+
* @return array the array of claim values associated with the event
72+
*/
73+
public function getClaims(): object {
74+
return $this->claims;
75+
}
76+
77+
/**
78+
* @return value for the logged in user attribute
79+
*/
80+
public function getResult(): UserAccountChangeResult {
81+
return $this->result;
82+
}
83+
84+
public function setResult(bool $accessAllowed, string $reason = '', ?string $redirectUrl = null) : void {
85+
$this->result = new UserAccountChangeResult($accessAllowed, $reason, $redirectUrl);
86+
}
87+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
/*
3+
* @copyright Copyright (c) 2023 T-Systems International
4+
*
5+
* @author B. Rederlechner <bernd.rederlechner@t-systems.com>
6+
*
7+
* @license GNU AGPL version 3 or any later version
8+
*
9+
*/
10+
11+
declare(strict_types=1);
12+
13+
namespace OCA\UserOIDC\Event;
14+
15+
/**
16+
* Event to provide custom mapping logic based on the OIDC token data
17+
* In order to avoid further processing the event propagation should be stopped
18+
* in the listener after processing as the value might get overwritten afterwards
19+
* by other listeners through $event->stopPropagation();
20+
*/
21+
class UserAccountChangeResult {
22+
23+
/** @var bool */
24+
private $accessAllowed;
25+
/** @var string */
26+
private $reason;
27+
/** @var string */
28+
private $redirectUrl;
29+
30+
public function __construct(bool $accessAllowed, string $reason = '', ?string $redirectUrl = null) {
31+
$this->accessAllowed = $accessAllowed;
32+
$this->redirectUrl = $redirectUrl;
33+
$this->reason = $reason;
34+
}
35+
36+
/**
37+
* @return value for the logged in user attribute
38+
*/
39+
public function isAccessAllowed(): bool {
40+
return $this->accessAllowed;
41+
}
42+
43+
public function setAccessAllowed(bool $accessAllowed): void {
44+
$this->accessAllowed = $accessAllowed;
45+
}
46+
47+
/**
48+
* @return get optional alternate redirect address
49+
*/
50+
public function getRedirectUrl(): ?string {
51+
return $this->redirectUrl;
52+
}
53+
54+
/**
55+
* @return set optional alternate redirect address
56+
*/
57+
public function setRedirectUrl(?string $redirectUrl): void {
58+
$this->redirectUrl = $redirectUrl;
59+
}
60+
61+
/**
62+
* @return get decision reason
63+
*/
64+
public function getReason(): string {
65+
return $this->reason;
66+
}
67+
68+
/**
69+
* @return set decision reason
70+
*/
71+
public function setReason(string $reason): void {
72+
$this->reason = $reason;
73+
}
74+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* @copyright Copyright (c) 2023, T-Systems International
7+
*
8+
* @author B. Rederlechner <bernd.rederlechner@t-Systems.com>
9+
*
10+
* @license AGPL-3.0
11+
*
12+
* This code is free software: you can redistribute it and/or modify
13+
* it under the terms of the GNU Affero General Public License, version 3,
14+
* as published by the Free Software Foundation.
15+
*
16+
* This program is distributed in the hope that it will be useful,
17+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
18+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+
* GNU Affero General Public License for more details.
20+
*
21+
* You should have received a copy of the GNU Affero General Public License, version 3,
22+
* along with this program. If not, see <http://www.gnu.org/licenses/>
23+
*
24+
*/
25+
26+
namespace OCA\UserOIDC\Service;
27+
28+
/**
29+
* Exception if the precondition of the config update method isn't met
30+
* @since 1.4.0
31+
*/
32+
class ProvisioningDeniedException extends \Exception {
33+
private $redirectUrl;
34+
35+
/**
36+
* Exception constructor including an option redirect url.
37+
*
38+
* @param string $message The error message. It will be not revealed to the
39+
* the user (unless the hint is empty) and thus
40+
* should be not translated.
41+
* @param string $hint A useful message that is presented to the end
42+
* user. It should be translated, but must not
43+
* contain sensitive data.
44+
* @param int $code Set default to 403 (Forbidden)
45+
* @param \Exception|null $previous
46+
*/
47+
public function __construct(string $message, ?string $redirectUrl = null, int $code = 403, \Exception $previous = null) {
48+
parent::__construct($message, $code, $previous);
49+
$this->redirectUrl = $redirectUrl;
50+
}
51+
52+
/**
53+
* Read optional failure redirect if available
54+
* @return string|null
55+
*/
56+
public function getRedirectUrl(): ?string {
57+
return $this->redirectUrl;
58+
}
59+
60+
/**
61+
* Include redirect in string serialisation.
62+
*
63+
* @return string
64+
*/
65+
public function __toString(): string {
66+
$redirect = $this->redirectUrl ?? '<no redirect>';
67+
return __CLASS__ . ": [{$this->code}]: {$this->message} ({$redirect})\n";
68+
}
69+
}

0 commit comments

Comments
 (0)