Skip to content

Commit b4e8b0a

Browse files
committed
Merge #14 Add event based provisioning V31
2 parents 65c5bab + 75da158 commit b4e8b0a

12 files changed

+1175
-19
lines changed

lib/AppInfo/Application.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ public function register(IRegistrationContext $context): void {
6060

6161
public function boot(IBootContext $context): void {
6262
$context->injectFn(\Closure::fromCallable([$this->backend, 'injectSession']));
63-
$context->injectFn(\Closure::fromCallable([$this, 'checkLoginToken']));
6463
/** @var IUserSession $userSession */
6564
$userSession = $this->getContainer()->get(IUserSession::class);
6665
if ($userSession->isLoggedIn()) {

lib/Controller/LoginController.php

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use OCA\UserOIDC\Service\DiscoveryService;
2424
use OCA\UserOIDC\Service\LdapService;
2525
use OCA\UserOIDC\Service\ProviderService;
26+
use OCA\UserOIDC\Service\ProvisioningDeniedException;
2627
use OCA\UserOIDC\Service\ProvisioningService;
2728
use OCA\UserOIDC\Service\TokenService;
2829
use OCA\UserOIDC\Vendor\Firebase\JWT\JWT;
@@ -480,26 +481,51 @@ public function code(string $state = '', string $code = '', string $scope = '',
480481
}
481482

482483
$autoProvisionAllowed = (!isset($oidcSystemConfig['auto_provision']) || $oidcSystemConfig['auto_provision']);
484+
$softAutoProvisionAllowed = (!isset($oidcSystemConfig['soft_auto_provision']) || $oidcSystemConfig['soft_auto_provision']);
485+
486+
$shouldDoUserLookup = !$autoProvisionAllowed || ($softAutoProvisionAllowed && !$this->provisioningService->hasOidcUserProvisitioned($userId));
487+
if ($shouldDoUserLookup && $this->ldapService->isLDAPEnabled()) {
488+
// in case user is provisioned by user_ldap, userManager->search() triggers an ldap search which syncs the results
489+
// so new users will be directly available even if they were not synced before this login attempt
490+
$this->userManager->search($userId, 1, 0);
491+
$this->ldapService->syncUser($userId);
492+
}
483493

484-
// in case user is provisioned by user_ldap, userManager->search() triggers an ldap search which syncs the results
485-
// so new users will be directly available even if they were not synced before this login attempt
486-
$this->userManager->search($userId);
487-
$this->ldapService->syncUser($userId);
488494
$userFromOtherBackend = $this->userManager->get($userId);
489495
if ($userFromOtherBackend !== null && $this->ldapService->isLdapDeletedUser($userFromOtherBackend)) {
490496
$userFromOtherBackend = null;
491497
}
492498

493499
if ($autoProvisionAllowed) {
494-
$softAutoProvisionAllowed = (!isset($oidcSystemConfig['soft_auto_provision']) || $oidcSystemConfig['soft_auto_provision']);
495-
if (!$softAutoProvisionAllowed && $userFromOtherBackend !== null) {
496-
// if soft auto-provisioning is disabled,
497-
// we refuse login for a user that already exists in another backend
498-
$message = $this->l10n->t('User conflict');
499-
return $this->build403TemplateResponse($message, Http::STATUS_BAD_REQUEST, ['reason' => 'non-soft auto provision, user conflict'], false);
500+
// $softAutoProvisionAllowed = (!isset($oidcSystemConfig['soft_auto_provision']) || $oidcSystemConfig['soft_auto_provision']);
501+
// if (!$softAutoProvisionAllowed && $userFromOtherBackend !== null) {
502+
// if soft auto-provisioning is disabled,
503+
// we refuse login for a user that already exists in another backend
504+
// $message = $this->l10n->t('User conflict');
505+
// return $this->build403TemplateResponse($message, Http::STATUS_BAD_REQUEST, ['reason' => 'non-soft auto provision, user conflict'], false);
506+
// }
507+
508+
// TODO: (proposal) refactor all provisioning strategies into event handlers
509+
$user = null;
510+
511+
try {
512+
$user = $this->provisioningService->provisionUser($userId, $providerId, $idTokenPayload, $userFromOtherBackend);
513+
} catch (ProvisioningDeniedException $denied) {
514+
// TODO MagentaCLOUD should upstream the exception handling
515+
$redirectUrl = $denied->getRedirectUrl();
516+
if ($redirectUrl === null) {
517+
$message = $this->l10n->t('Failed to provision user');
518+
return $this->build403TemplateResponse($message, Http::STATUS_BAD_REQUEST, ['reason' => $denied->getMessage()]);
519+
} else {
520+
// error response is a redirect, e.g. to a booking site
521+
// so that you can immediately get the registration page
522+
return new RedirectResponse($redirectUrl);
523+
}
500524
}
525+
501526
// use potential user from other backend, create it in our backend if it does not exist
502-
$user = $this->provisioningService->provisionUser($userId, $providerId, $idTokenPayload, $userFromOtherBackend);
527+
// $user = $this->provisioningService->provisionUser($userId, $providerId, $idTokenPayload, $userFromOtherBackend);
528+
// no default exception handling to pass on unittest assertion failures
503529
} else {
504530
// when auto provision is disabled, we assume the user has been created by another user backend (or manually)
505531
$user = $userFromOtherBackend;
@@ -526,12 +552,12 @@ public function code(string $state = '', string $code = '', string $scope = '',
526552
$this->session->remove(self::NONCE);
527553

528554
// store all token information for potential token exchange requests
529-
$tokenData = array_merge(
530-
$data,
531-
['provider_id' => $providerId],
532-
);
533-
$this->tokenService->storeToken($tokenData);
534-
$this->config->setUserValue($user->getUID(), Application::APP_ID, 'had_token_once', '1');
555+
// $tokenData = array_merge(
556+
// $data,
557+
// ['provider_id' => $providerId],
558+
// );
559+
// $this->tokenService->storeToken($tokenData);
560+
// $this->config->setUserValue($user->getUID(), Application::APP_ID, 'had_token_once', '1');
535561

536562
// Set last password confirm to the future as we don't have passwords to confirm against with SSO
537563
$this->session->set('last-password-confirm', strtotime('+4 year', time()));
@@ -758,7 +784,7 @@ public function backChannelLogout(string $providerIdentifier, string $logout_tok
758784
* @return JSONResponse
759785
*/
760786
private function getBackchannelLogoutErrorResponse(
761-
string $error, string $description, array $throttleMetadata = [],
787+
string $error, string $description, array $throttleMetadata = [], ?bool $throttle = null,
762788
): JSONResponse {
763789
$this->logger->debug('Backchannel logout error. ' . $error . ' ; ' . $description);
764790
return new JSONResponse(

lib/Db/UserMapper.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,25 @@
1414
use OCP\Cache\CappedMemoryCache;
1515
use OCP\IConfig;
1616
use OCP\IDBConnection;
17+
use Psr\Log\LoggerInterface;
1718

1819
/**
1920
* @extends QBMapper<User>
2021
*/
2122
class UserMapper extends QBMapper {
2223

2324
private CappedMemoryCache $userCache;
25+
private LoggerInterface $logger;
2426

2527
public function __construct(
2628
IDBConnection $db,
29+
LoggerInterface $logger,
2730
private LocalIdService $idService,
2831
private IConfig $config,
2932
) {
3033
parent::__construct($db, 'user_oidc', User::class);
3134
$this->userCache = new CappedMemoryCache();
35+
$this->logger = $logger;
3236
}
3337

3438
/**
@@ -59,6 +63,29 @@ public function getUser(string $uid): User {
5963
public function find(string $search, $limit = null, $offset = null): array {
6064
$qb = $this->db->getQueryBuilder();
6165

66+
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
67+
$stack = [];
68+
69+
foreach ($backtrace as $index => $trace) {
70+
$class = $trace['class'] ?? '';
71+
$type = $trace['type'] ?? '';
72+
$function = $trace['function'] ?? '';
73+
$file = $trace['file'] ?? 'unknown file';
74+
$line = $trace['line'] ?? 'unknown line';
75+
76+
$stack[] = sprintf(
77+
"#%d %s%s%s() called at [%s:%s]",
78+
$index,
79+
$class,
80+
$type,
81+
$function,
82+
$file,
83+
$line
84+
);
85+
}
86+
87+
$this->logger->debug("Find user by string: " . $search . " -- Call Stack:\n" . implode("\n", $stack));
88+
6289
$oidcSystemConfig = $this->config->getSystemValue('user_oidc', []);
6390
$matchEmails = !isset($oidcSystemConfig['user_search_match_emails']) || $oidcSystemConfig['user_search_match_emails'] === true;
6491
if ($matchEmails) {
@@ -91,6 +118,29 @@ public function find(string $search, $limit = null, $offset = null): array {
91118
public function findDisplayNames(string $search, $limit = null, $offset = null): array {
92119
$qb = $this->db->getQueryBuilder();
93120

121+
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
122+
$stack = [];
123+
124+
foreach ($backtrace as $index => $trace) {
125+
$class = $trace['class'] ?? '';
126+
$type = $trace['type'] ?? '';
127+
$function = $trace['function'] ?? '';
128+
$file = $trace['file'] ?? 'unknown file';
129+
$line = $trace['line'] ?? 'unknown line';
130+
131+
$stack[] = sprintf(
132+
"#%d %s%s%s() called at [%s:%s]",
133+
$index,
134+
$class,
135+
$type,
136+
$function,
137+
$file,
138+
$line
139+
);
140+
}
141+
142+
$this->logger->debug("Find user display names by string: " . $search . " -- Call Stack:\n" . implode("\n", $stack));
143+
94144
$oidcSystemConfig = $this->config->getSystemValue('user_oidc', []);
95145
$matchEmails = !isset($oidcSystemConfig['user_search_match_emails']) || $oidcSystemConfig['user_search_match_emails'] === true;
96146
if ($matchEmails) {
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+
}

lib/Service/LdapService.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
namespace OCA\UserOIDC\Service;
1010

11+
use OCP\App\IAppManager;
1112
use OCP\AppFramework\QueryException;
1213
use OCP\IUser;
1314
use Psr\Log\LoggerInterface;
@@ -16,16 +17,25 @@ class LdapService {
1617

1718
public function __construct(
1819
private LoggerInterface $logger,
20+
private IAppManager $appManager,
1921
) {
2022
}
2123

24+
public function isLDAPEnabled(): bool {
25+
return $this->appManager->isEnabledForUser('user_ldap');
26+
}
27+
2228
/**
2329
* @param IUser $user
2430
* @return bool
2531
* @throws \Psr\Container\ContainerExceptionInterface
2632
* @throws \Psr\Container\NotFoundExceptionInterface
2733
*/
2834
public function isLdapDeletedUser(IUser $user): bool {
35+
if ($this->isLDAPEnabled()) {
36+
return false;
37+
}
38+
2939
$className = $user->getBackendClassName();
3040
if ($className !== 'LDAP') {
3141
return false;

0 commit comments

Comments
 (0)