Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ Those groups of people can then be used by any other app for sharing purpose.
<default_enable/>
<background-jobs>
<job>OCA\Circles\Cron\Maintenance</job>
<job>OCA\Circles\Cron\ScimSync</job>
<job>OCA\Circles\Cron\RemoteModDiscover</job>
<job>OCA\Circles\Cron\OidcSync</job>
<job>OCA\Circles\BackgroundJob\RemoteModSync</job>
<job>OCA\Circles\BackgroundJob\OidcSyncUser</job>
</background-jobs>

<repair-steps>
Expand Down Expand Up @@ -76,6 +81,11 @@ Those groups of people can then be used by any other app for sharing purpose.
<command>OCA\Circles\Command\MembersRemove</command>

<command>OCA\Circles\Command\MigrateCustomGroups</command>

<command>OCA\Circles\Command\CirclesScimSync</command>
<command>OCA\Circles\Command\CirclesRemoteModDiscover</command>
<command>OCA\Circles\Command\CirclesRemoteModSync</command>
<command>OCA\Circles\Command\CirclesOidcSync</command>
</commands>

<activity>
Expand All @@ -102,5 +112,7 @@ Those groups of people can then be used by any other app for sharing purpose.
<admin>OCA\Circles\Settings\Admin</admin>
<admin>OCA\Circles\Settings\AdminTeamFolders</admin>
<admin-section>OCA\Circles\Settings\AdminSection</admin-section>
<personal>OCA\Circles\Settings\Personal</personal>
<personal-section>OCA\Circles\Settings\PersonalSection</personal-section>
</settings>
</info>
4 changes: 4 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@
['name' => 'Remote#member', 'url' => '/member/{type}/{userId}/', 'verb' => 'GET'],
['name' => 'Remote#inherited', 'url' => '/inherited/{circleId}/', 'verb' => 'GET'],
['name' => 'Remote#memberships', 'url' => '/memberships/{circleId}/', 'verb' => 'GET'],
['name' => 'Remote#moderator', 'url' => '/moderator/', 'verb' => 'POST'],

['name' => 'Oidc#connect', 'url' => '/oidc/connect', 'verb' => 'GET'],
['name' => 'Oidc#callback', 'url' => '/oidc/callback', 'verb' => 'GET'],

['name' => 'Deprecated#listing', 'url' => '/listing', 'verb' => 'GET'],
]
Expand Down
3 changes: 3 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
use OCA\Circles\Listeners\TeamFolderLifecycleListener;
use OCA\Circles\Listeners\UserCreated;
use OCA\Circles\Listeners\UserDeleted;
use OCA\Circles\Listeners\UserLoggedIn;
use OCA\Circles\MountManager\CircleMountProvider;
use OCA\Circles\Notification\Notifier;
use OCA\Circles\Search\UnifiedSearchProvider;
Expand Down Expand Up @@ -71,6 +72,7 @@
use OCP\User\Events\UserChangedEvent;
use OCP\User\Events\UserCreatedEvent;
use OCP\User\Events\UserDeletedEvent;
use OCP\User\Events\UserLoggedInEvent;
use Psr\Container\ContainerInterface;
use Throwable;

Expand Down Expand Up @@ -105,6 +107,7 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(UserUpdatedEvent::class, AccountUpdated::class);
$context->registerEventListener(UserChangedEvent::class, AccountUpdated::class);
$context->registerEventListener(UserDeletedEvent::class, UserDeleted::class);
$context->registerEventListener(UserLoggedInEvent::class, UserLoggedIn::class);

// Circle Events
$context->registerEventListener(CircleMemberRemovedEvent::class, CircleMemberRemoved::class);
Expand Down
3 changes: 2 additions & 1 deletion lib/AppInfo/Capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ private function getCapabilitiesCircleConstants(): array {
Circle::CFG_CIRCLE_INVITE => $this->l10n->t('Team invite'),
Circle::CFG_FEDERATED => $this->l10n->t('Federated'),
Circle::CFG_MOUNTPOINT => $this->l10n->t('Mount point'),
Circle::CFG_APP => $this->l10n->t('App')
Circle::CFG_APP => $this->l10n->t('App'),
Circle::CFG_THIRD_PARTY => $this->l10n->t('Third party'),
],
'source'
=> [
Expand Down
39 changes: 39 additions & 0 deletions lib/BackgroundJob/OidcSyncUser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Circles\BackgroundJob;

use Exception;
use OCA\Circles\Service\OidcService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\QueuedJob;
use Psr\Log\LoggerInterface;

class OidcSyncUser extends QueuedJob {
public function __construct(
ITimeFactory $time,
private readonly OidcService $oidcService,
private readonly LoggerInterface $logger,
) {
parent::__construct($time);
}

protected function run($argument) {
$userId = $argument['userId'] ?? null;
if ($userId === null) {
return;
}

try {
$this->oidcService->syncMembershipsForUser($userId);
} catch (Exception $e) {
$this->logger->warning('could not sync OIDC memberships on login', ['userId' => $userId, 'exception' => $e]);
}
}
}
30 changes: 30 additions & 0 deletions lib/BackgroundJob/RemoteModSync.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Circles\BackgroundJob;

use OCA\Circles\Service\RemoteModCircleService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\QueuedJob;

class RemoteModSync extends QueuedJob {
public function __construct(
ITimeFactory $time,
private readonly RemoteModCircleService $remoteModCircleService,
) {
parent::__construct($time);

// only run one instance of this job at a time
$this->setAllowParallelRuns(false);
}

protected function run($argument) {
$this->remoteModCircleService->syncModeratorCircles();
}
}
37 changes: 37 additions & 0 deletions lib/Command/CirclesOidcSync.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Circles\Command;

use OC\Core\Command\Base;
use OCA\Circles\Service\OidcService;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class CirclesOidcSync extends Base {
public function __construct(
private readonly OidcService $oidcService,
) {
parent::__construct();
}

protected function configure() {
parent::configure();
$this->setName('circles:oidc:sync')
->setDescription('fetch memberships from OIDC server and add users to corresponding circles if not a member');
}

protected function execute(InputInterface $input, OutputInterface $output): int {
$this->oidcService->syncMemberships();

$output->writeln('<info>done</info>');

return 0;
}
}
37 changes: 37 additions & 0 deletions lib/Command/CirclesRemoteModDiscover.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Circles\Command;

use OC\Core\Command\Base;
use OCA\Circles\Service\RemoteModCircleService;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class CirclesRemoteModDiscover extends Base {
public function __construct(
private readonly RemoteModCircleService $remoteModCircleService,
) {
parent::__construct();
}

protected function configure() {
parent::configure();
$this->setName('circles:remotemod:discover')
->setDescription('discover the remote moderator circle id for each configured remote instance');
}

protected function execute(InputInterface $input, OutputInterface $output): int {
$this->remoteModCircleService->discoverModeratorCircles();

$output->writeln('<info>done</info>');

return 0;
}
}
37 changes: 37 additions & 0 deletions lib/Command/CirclesRemoteModSync.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Circles\Command;

use OC\Core\Command\Base;
use OCA\Circles\Service\RemoteModCircleService;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class CirclesRemoteModSync extends Base {
public function __construct(
private readonly RemoteModCircleService $remoteModCircleService,
) {
parent::__construct();
}

protected function configure() {
parent::configure();
$this->setName('circles:remotemod:sync')
->setDescription('ensure every discovered remote moderator circle is a member of every third-party circle');
}

protected function execute(InputInterface $input, OutputInterface $output): int {
$this->remoteModCircleService->syncModeratorCircles();

$output->writeln('<info>done</info>');

return 0;
}
}
37 changes: 37 additions & 0 deletions lib/Command/CirclesScimSync.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Circles\Command;

use OC\Core\Command\Base;
use OCA\Circles\Service\ScimService;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class CirclesScimSync extends Base {
public function __construct(
private readonly ScimService $scimService,
) {
parent::__construct();
}

protected function configure() {
parent::configure();
$this->setName('circles:scim:sync')
->setDescription('fetch circles from SCIM server and create the corresponding circles if missing');
}

protected function execute(InputInterface $input, OutputInterface $output): int {
$this->scimService->syncCircles();

$output->writeln('<info>done</info>');

return 0;
}
}
39 changes: 39 additions & 0 deletions lib/ConfigLexicon.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,27 @@ class ConfigLexicon implements ILexicon {
public const TEAM_FOLDER_AUTO_CREATE = 'team_folder_auto_create';
public const TEAM_FOLDER_DEFAULT_QUOTA = 'team_folder_default_quota';

// OIDC
public const OIDC_ENABLED = 'oidc_enabled';
public const OIDC_ISSUER = 'oidc_issuer';
public const OIDC_CLIENT_ID = 'oidc_client_id';
public const OIDC_CLIENT_SECRET = 'oidc_client_secret';
public const OIDC_AUTHORIZATION_ENDPOINT = 'oidc_authorization_endpoint';
public const OIDC_TOKEN_ENDPOINT = 'oidc_token_endpoint';
public const OIDC_USERINFO_ENDPOINT = 'oidc_userinfo_endpoint';
public const OIDC_SCOPE = 'oidc_scope';
public const OIDC_MEMBERSHIP_CLAIM = 'oidc_membership_claim';

// SCIM
public const SCIM_ENABLED = 'scim_enabled';
public const SCIM_ENDPOINT = 'scim_endpoint';
public const SCIM_TOKEN = 'scim_token';

// Remote moderator circle
public const REMOTE_MOD_CIRCLE_INSTANCES = 'remote_mod_circle_instances'; // without http/https
public const REMOTE_MOD_CIRCLE_MAPPING = 'remote_mod_circle_mapping';
public const REMOTE_MOD_CIRCLE_LOCAL_ID = 'remote_mod_circle_local_id';

public function getStrictness(): Strictness {
return Strictness::IGNORE;
}
Expand All @@ -38,6 +59,24 @@ public function getAppConfigs(): array {
new Entry(key: self::REMOVE_SHARE_TOKENS_DONE, type: ValueType::BOOL, defaultRaw: false, definition: 'whether the remove share tokens repair step has already been executed', lazy: true),
new Entry(key: self::TEAM_FOLDER_AUTO_CREATE, type: ValueType::BOOL, defaultRaw: true, definition: 'automatically create a team folder when a new team is created', lazy: true),
new Entry(key: self::TEAM_FOLDER_DEFAULT_QUOTA, type: ValueType::INT, defaultRaw: 0, definition: 'default quota in bytes for auto-created team folders (0 means unlimited)', lazy: true),
// OIDC
new Entry(key: self::OIDC_ENABLED, type: ValueType::BOOL, defaultRaw: false, definition: 'disable/enable OIDC integration', lazy: true),
new Entry(key: self::OIDC_ISSUER, type: ValueType::STRING, defaultRaw: '', definition: 'OIDC provider issuer URL', lazy: true),
new Entry(key: self::OIDC_CLIENT_ID, type: ValueType::STRING, defaultRaw: '', definition: 'OIDC client id', lazy: true),
new Entry(key: self::OIDC_CLIENT_SECRET, type: ValueType::STRING, defaultRaw: '', definition: 'OIDC client secret', lazy: true),
new Entry(key: self::OIDC_AUTHORIZATION_ENDPOINT, type: ValueType::STRING, defaultRaw: '', definition: 'OIDC authorization endpoint', lazy: true),
new Entry(key: self::OIDC_TOKEN_ENDPOINT, type: ValueType::STRING, defaultRaw: '', definition: 'OIDC token endpoint', lazy: true),
new Entry(key: self::OIDC_USERINFO_ENDPOINT, type: ValueType::STRING, defaultRaw: '', definition: 'OIDC userinfo endpoint', lazy: true),
new Entry(key: self::OIDC_SCOPE, type: ValueType::STRING, defaultRaw: 'openid', definition: 'OIDC scope(s) requested during authorization', lazy: true),
new Entry(key: self::OIDC_MEMBERSHIP_CLAIM, type: ValueType::STRING, defaultRaw: '', definition: 'claim name containing group membership information', lazy: true),
// SCIM
new Entry(key: self::SCIM_ENABLED, type: ValueType::BOOL, defaultRaw: false, definition: 'disable/enable SCIM integration', lazy: true),
new Entry(key: self::SCIM_ENDPOINT, type: ValueType::STRING, defaultRaw: '', definition: 'SCIM server endpoint for group discovery', lazy: true),
new Entry(key: self::SCIM_TOKEN, type: ValueType::STRING, defaultRaw: '', definition: 'bearer token used to authenticate against the SCIM server', lazy: true),
// Remote moderator circle
new Entry(key: self::REMOTE_MOD_CIRCLE_INSTANCES, type: ValueType::ARRAY, defaultRaw: [], definition: 'list of remote instances to sync a moderator circle from', lazy: true),
new Entry(key: self::REMOTE_MOD_CIRCLE_MAPPING, type: ValueType::ARRAY, defaultRaw: [], definition: 'map of instance => circle id for known remote moderator circles', lazy: true),
new Entry(key: self::REMOTE_MOD_CIRCLE_LOCAL_ID, type: ValueType::STRING, defaultRaw: '', definition: 'circle id of the local circle acting as a moderator in remote circles', lazy: true),
];
}

Expand Down
Loading
Loading