Skip to content

Commit f0712d8

Browse files
authored
Merge pull request #962 from guerby/patch-user-add-fix-939
feat: add saml:user:add command to pre-provision users
2 parents f26fd82 + f549ed3 commit f0712d8

File tree

7 files changed

+104
-15
lines changed

7 files changed

+104
-15
lines changed

appinfo/info.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ While theoretically any other authentication provider implementing either one of
5555
<command>OCA\User_SAML\Command\ConfigSet</command>
5656
<command>OCA\User_SAML\Command\GetMetadata</command>
5757
<command>OCA\User_SAML\Command\GroupMigrationCopyIncomplete</command>
58+
<command>OCA\User_SAML\Command\UserAdd</command>
5859
</commands>
5960
<settings>
6061
<admin>OCA\User_SAML\Settings\Admin</admin>

lib/AppInfo/Application.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,9 @@ public function boot(IBootContext $context): void {
148148

149149
// All requests that are not authenticated and match against the "/login" route are
150150
// redirected to the SAML login endpoint
151-
if (!$isCLI &&
152-
!$userSession->isLoggedIn() &&
153-
($request->getPathInfo() === '/login')) {
151+
if (!$isCLI
152+
&& !$userSession->isLoggedIn()
153+
&& ($request->getPathInfo() === '/login')) {
154154
try {
155155
$params = $request->getParams();
156156
} catch (\LogicException) {

lib/Command/UserAdd.php

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
namespace OCA\User_SAML\Command;
9+
10+
use OC\Core\Command\Base;
11+
use OCA\User_SAML\UserBackend;
12+
use OCP\IUserManager;
13+
use Psr\Log\LoggerInterface;
14+
use Symfony\Component\Console\Input\InputArgument;
15+
use Symfony\Component\Console\Input\InputInterface;
16+
use Symfony\Component\Console\Input\InputOption;
17+
use Symfony\Component\Console\Output\OutputInterface;
18+
19+
class UserAdd extends Base {
20+
public function __construct(
21+
protected IUserManager $userManager,
22+
protected UserBackend $backend,
23+
private LoggerInterface $logger,
24+
) {
25+
parent::__construct();
26+
}
27+
protected function configure(): void {
28+
$this
29+
->setName('saml:user:add')
30+
->setDescription('Add a SAML account')
31+
->addArgument(
32+
'uid',
33+
InputArgument::REQUIRED,
34+
'Account ID as provided by the IdP (must only contain a-z, A-Z, 0-9, -, _ and @)'
35+
)
36+
->addOption(
37+
'display-name',
38+
null,
39+
InputOption::VALUE_REQUIRED,
40+
'Name as presented in the web interface (can contain any characters)'
41+
)
42+
->addOption(
43+
'email',
44+
null,
45+
InputOption::VALUE_OPTIONAL,
46+
'Set user default email in user profile'
47+
);
48+
}
49+
50+
protected function execute(InputInterface $input, OutputInterface $output): int {
51+
$uid = $input->getArgument('uid');
52+
53+
if ($this->userManager->userExists($uid)) {
54+
$output->writeln('<error>The account "' . $uid . '" already exists.</error>');
55+
return 1;
56+
}
57+
58+
if (!$output->isQuiet()) {
59+
$output->writeln('<info>The account "' . $uid . '" is to be added to the SAML backend.</info>');
60+
}
61+
62+
try {
63+
$this->backend->createUserIfNotExists($uid);
64+
} catch (\Exception $e) {
65+
$output->writeln('<error>SAML create user ' . $e->getMessage() . '</error>');
66+
return 1;
67+
}
68+
69+
try {
70+
$this->backend->setDisplayName($uid, $input->getOption('display-name'));
71+
$email = $input->getOption('email');
72+
if (!empty($email)) {
73+
$user = $this->userManager->get($uid);
74+
$user->setSystemEMailAddress($email);
75+
}
76+
} catch (\Exception $e) {
77+
$output->writeln('<error>SAML create user Email and DisplayName ' . $e->getMessage() . '</error>');
78+
return 1;
79+
}
80+
81+
if (!$output->isQuiet()) {
82+
$output->writeln('<info>SAML user "' . $uid . '" added.</info>');
83+
}
84+
85+
return 0;
86+
}
87+
88+
}

lib/Controller/SAMLController.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -431,8 +431,8 @@ public function assertionConsumerService(): Http\RedirectResponse {
431431
* @throws Error
432432
*/
433433
public function singleLogoutService(): Http\RedirectResponse {
434-
$isFromGS = ($this->config->getSystemValueBool('gs.enabled', false) &&
435-
$this->config->getSystemValueString('gss.mode', '') === 'master');
434+
$isFromGS = ($this->config->getSystemValueBool('gs.enabled', false)
435+
&& $this->config->getSystemValueString('gss.mode', '') === 'master');
436436

437437
// Some IDPs send the SLO request via POST, but OneLogin php-saml only handles GET.
438438
// To hack around this issue we copy the request from _POST to _GET.

lib/DavPlugin.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ public function initialize(Server $server) {
3535

3636
public function beforeMethod(RequestInterface $request, ResponseInterface $response) {
3737
if (
38-
$this->config->getAppValue('user_saml', 'type') === 'environment-variable' &&
39-
!$this->session->exists('user_saml.samlUserData')
38+
$this->config->getAppValue('user_saml', 'type') === 'environment-variable'
39+
&& !$this->session->exists('user_saml.samlUserData')
4040
) {
4141
$uidMapping = $this->samlSettings->get(1)['general-uid_mapping'];
4242
if (isset($this->auth[$uidMapping])) {

lib/GroupManager.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,8 @@ protected function hasGroupForeignMembers(IGroup $group): bool {
273273
* allowed only for groups owned by the SAML backend.
274274
*/
275275
protected function mayModifyGroup(?IGroup $group): bool {
276-
$isInTransition =
277-
$group !== null
276+
$isInTransition
277+
= $group !== null
278278
&& $group->getGID() !== 'admin'
279279
&& in_array('Database', $group->getBackendNames())
280280
&& $this->isGroupInTransitionList($group->getGID());

tests/unit/UserBackendTest.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,9 @@ public function testUpdateAttributesWithoutAttributes() {
106106
$user = $this->createMock(IUser::class);
107107

108108
$this->config->method('getAppValue')
109-
->willReturnCallback(fn (string $appId, string $key, string $default) =>
109+
->willReturnCallback(fn (string $appId, string $key, string $default)
110110
// Unused parameters are intentionally kept for clarity
111-
$default);
111+
=> $default);
112112

113113
$this->userManager
114114
->expects($this->once())
@@ -138,9 +138,9 @@ public function testUpdateAttributesWithoutValidUser() {
138138
$this->getMockedBuilder();
139139

140140
$this->config->method('getAppValue')
141-
->willReturnCallback(fn (string $appId, string $key, string $default) =>
141+
->willReturnCallback(fn (string $appId, string $key, string $default)
142142
// Unused parameters are intentionally kept for clarity
143-
$default);
143+
=> $default);
144144

145145
$this->userManager
146146
->expects($this->once())
@@ -227,9 +227,9 @@ public function testUpdateAttributesQuotaDefaultFallback() {
227227
$attributes = ['email' => '[email protected]', 'displayname' => 'New Displayname', 'quota' => ''];
228228

229229
$this->config->method('getAppValue')
230-
->willReturnCallback(fn (string $appId, string $key, string $default) =>
230+
->willReturnCallback(fn (string $appId, string $key, string $default)
231231
// Unused $appId parameter is intentionally kept for clarity
232-
match ($key) {
232+
=> match ($key) {
233233
'saml-attribute-mapping-email_mapping' => 'email',
234234
'saml-attribute-mapping-displayName_mapping' => 'displayname',
235235
'saml-attribute-mapping-quota_mapping' => 'quota',

0 commit comments

Comments
 (0)