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
2 changes: 2 additions & 0 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,7 @@ Those groups of people can then be used by any other app for sharing purpose.

<settings>
<admin>OCA\Circles\Settings\Admin</admin>
<admin>OCA\Circles\Settings\TeamsAdmin</admin>
<admin-section>OCA\Circles\Settings\Section</admin-section>
</settings>
</info>
2 changes: 2 additions & 0 deletions lib/ConfigLexicon.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class ConfigLexicon implements ILexicon {
public const FEDERATED_TEAMS_ENABLED = 'federated_teams_enabled';
public const FEDERATED_TEAMS_FRONTAL = 'federated_teams_frontal';
public const REMOVE_SHARE_TOKENS_DONE = 'remove_share_tokens_done';
public const TEAM_CREATION_ALLOWED_GROUPS = 'team_creation_allowed_groups';

public function getStrictness(): Strictness {
return Strictness::IGNORE;
Expand All @@ -34,6 +35,7 @@ public function getAppConfigs(): array {
new Entry(key: self::FEDERATED_TEAMS_ENABLED, type: ValueType::BOOL, defaultRaw: false, definition: 'disable/enable Federated Teams', lazy: true),
new Entry(key: self::FEDERATED_TEAMS_FRONTAL, type: ValueType::STRING, defaultRaw: '', definition: 'domain name used to auth public request', lazy: true),
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_CREATION_ALLOWED_GROUPS, type: ValueType::STRING, defaultRaw: '[]', definition: 'JSON array of group GIDs allowed to create teams (empty = all users)', lazy: true),
];
}

Expand Down
6 changes: 6 additions & 0 deletions lib/Controller/PageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@

use OCA\Circles\AppInfo\Application;
use OCA\Circles\Service\ConfigService;
use OCA\Circles\Service\PermissionService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\FrontpageRoute;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\NotFoundResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\IRequest;
use OCP\Util;

Expand All @@ -27,6 +29,8 @@ class PageController extends Controller {
public function __construct(
IRequest $request,
private ConfigService $configService,
private PermissionService $permissionService,
private IInitialState $initialState,
) {
parent::__construct(Application::APP_ID, $request);
}
Expand All @@ -41,6 +45,8 @@ public function index(): TemplateResponse|NotFoundResponse {
return new NotFoundResponse();
}

$this->initialState->provideInitialState('canCreateTeam', $this->permissionService->canUserCreateTeams());

Util::addScript(Application::APP_ID, 'teams-main');
Util::addStyle(Application::APP_ID, 'teams-main');

Expand Down
30 changes: 29 additions & 1 deletion lib/Controller/SettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,44 @@ public function setValue(string $key, string $value): DataResponse {
return $this->getValues();
}

if ($key === ConfigLexicon::TEAM_CREATION_ALLOWED_GROUPS) {
if (!$this->isValidAllowedGroupsValue($value)) {
return new DataResponse(['data' => ['message' => 'allowed groups must be a JSON array of group ids']], Http::STATUS_BAD_REQUEST);
}

$this->appConfig->setAppValueString(ConfigLexicon::TEAM_CREATION_ALLOWED_GROUPS, $value);
return $this->getValues();
}

return new DataResponse(['data' => ['message' => 'unsupported key']], Http::STATUS_BAD_REQUEST);
}

public function getValues(): DataResponse {
return new DataResponse([
ConfigLexicon::FEDERATED_TEAMS_FRONTAL => $this->getFrontalValue() ?? '',
ConfigLexicon::FEDERATED_TEAMS_ENABLED => $this->appConfig->getAppValueBool(ConfigLexicon::FEDERATED_TEAMS_ENABLED),
ConfigLexicon::TEAM_CREATION_ALLOWED_GROUPS => $this->appConfig->getAppValueString(
ConfigLexicon::TEAM_CREATION_ALLOWED_GROUPS,
'[]',
),
]);
}

private function isValidAllowedGroupsValue(string $value): bool {
$decoded = json_decode($value, true);
if (!is_array($decoded)) {
return false;
}

foreach ($decoded as $groupId) {
if (!is_string($groupId) || $groupId === '') {
return false;
}
}

return true;
}

private function setFrontalValue(string $url): bool {
[$scheme, $cloudId, $path] = $this->parseFrontalAddress($url);
if (is_null($scheme)) {
Expand All @@ -66,7 +94,7 @@ private function setFrontalValue(string $url): bool {

private function getFrontalValue(): ?string {
if ($this->appConfig->hasAppKey(ConfigLexicon::FEDERATED_TEAMS_FRONTAL)) {
return $this->appConfig->getAppValueString(ConfigLExicon::FEDERATED_TEAMS_FRONTAL);
return $this->appConfig->getAppValueString(ConfigLexicon::FEDERATED_TEAMS_FRONTAL);
}

if (!$this->appConfig->hasAppKey(ConfigService::FRONTAL_CLOUD_SCHEME)
Expand Down
27 changes: 23 additions & 4 deletions lib/Dashboard/TeamDashboardWidget.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,27 @@

use OCA\Circles\AppInfo\Application;
use OCA\Circles\Service\ConfigService;
use OCA\Circles\Service\PermissionService;
use OCP\AppFramework\Services\IInitialState;
use OCP\Dashboard\IButtonWidget;
use OCP\Dashboard\IConditionalWidget;
use OCP\Dashboard\IIconWidget;
use OCP\Dashboard\Model\WidgetButton;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Util;

class TeamDashboardWidget implements IIconWidget, IButtonWidget, IConditionalWidget {
public function __construct(
private readonly IURLGenerator $urlGenerator,
private readonly IL10N $l10n,
private readonly ConfigService $configService,
private readonly PermissionService $permissionService,
private readonly IUserManager $userManager,
private readonly IUserSession $userSession,
private readonly IInitialState $initialState,
) {
}

Expand Down Expand Up @@ -64,23 +72,34 @@ public function getUrl(): ?string {
* @inheritDoc
*/
public function load(): void {
$this->initialState->provideInitialState(
'canCreateTeam',
$this->permissionService->canUserCreateTeams($this->userSession->getUser()),
);

Util::addScript(Application::APP_ID, 'teams-dashboard');
Util::addStyle(Application::APP_ID, 'teams-dashboard');
}

public function getWidgetButtons(string $userId): array {
return [
$buttons = [
new WidgetButton(
WidgetButton::TYPE_MORE,
$this->getTeamPage(),
$this->l10n->t('Show all teams')
),
new WidgetButton(
];

$user = $this->userManager->get($userId);
if ($this->permissionService->canUserCreateTeams($user)) {
$buttons[] = new WidgetButton(
WidgetButton::TYPE_SETUP,
$this->getTeamPage(),
$this->l10n->t('Create a new team')
),
];
);
}

return $buttons;
}

public function getIconUrl(): string {
Expand Down
84 changes: 84 additions & 0 deletions lib/Service/PermissionService.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace OCA\Circles\Service;

use OCA\Circles\ConfigLexicon;
use OCA\Circles\Db\MemberRequest;
use OCA\Circles\Db\MembershipRequest;
use OCA\Circles\Exceptions\InitiatorNotFoundException;
Expand All @@ -21,7 +22,10 @@
use OCA\Circles\Model\Circle;
use OCA\Circles\Model\Helpers\MemberHelper;
use OCA\Circles\Model\Member;
use OCP\IGroupManager;
use OCP\IL10N;
use OCP\IUser;
use OCP\IUserSession;

class PermissionService {

Expand All @@ -31,15 +35,78 @@ public function __construct(
private readonly ConfigService $configService,
private readonly MemberRequest $memberRequest,
private readonly MembershipRequest $membershipRequest,
private readonly IGroupManager $groupManager,
private readonly IUserSession $userSession,
) {
}

/**
* @return string[]
*/
public function getAllowedCreationGroups(): array {
$raw = $this->configService->getAppValue(ConfigLexicon::TEAM_CREATION_ALLOWED_GROUPS);
if ($raw === '') {
return [];
}

$decoded = json_decode($raw, true);
if (!is_array($decoded)) {
return [];
}

return array_values(array_filter($decoded, static fn ($groupId): bool => is_string($groupId) && $groupId !== ''));
}

public function canUserCreateTeams(?IUser $user = null): bool {
$user ??= $this->userSession->getUser();
if ($user === null) {
return false;
}

if ($this->groupManager->isAdmin($user->getUID())) {
return true;
}

$allowedGroups = $this->getAllowedCreationGroups();
if ($allowedGroups === []) {
return $this->canPassLegacyCircleCreationLimit();
}

$userGroups = $this->groupManager->getUserGroupIds($user);
if (array_intersect($allowedGroups, $userGroups) === []) {
return false;
}

return $this->canPassLegacyCircleCreationLimit();
}

/**
* @throws RequestBuilderException
* @throws InitiatorNotFoundException
* @throws InsufficientPermissionException
*/
public function confirmCircleCreation(): void {
$user = $this->userSession->getUser();
if ($user !== null && $this->groupManager->isAdmin($user->getUID())) {
return;
}

$allowedGroups = $this->getAllowedCreationGroups();
if ($allowedGroups !== []) {
if ($user === null) {
throw new InsufficientPermissionException(
$this->l10n->t('You have no permission to create a new team')
);
}

$userGroups = $this->groupManager->getUserGroupIds($user);
if (array_intersect($allowedGroups, $userGroups) === []) {
throw new InsufficientPermissionException(
$this->l10n->t('You have no permission to create a new team')
);
}
}

try {
$this->confirm(ConfigService::LIMIT_CIRCLE_CREATION);
} catch (InsufficientPermissionException) {
Expand All @@ -49,6 +116,23 @@ public function confirmCircleCreation(): void {
}
}

private function canPassLegacyCircleCreationLimit(): bool {
$singleId = $this->configService->getAppValue(ConfigService::LIMIT_CIRCLE_CREATION);
if ($singleId === '') {
return true;
}

try {
$this->federatedUserService->mustHaveCurrentUser();
$federatedUser = $this->federatedUserService->getCurrentUser();
$federatedUser->getLink($singleId);

return true;
} catch (InitiatorNotFoundException|MembershipNotFoundException|RequestBuilderException) {
return false;
}
}

/**
* @param string $config
*
Expand Down
43 changes: 43 additions & 0 deletions lib/Settings/Section.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

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

namespace OCA\Circles\Settings;

use OCA\Circles\AppInfo\Application;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Settings\IIconSection;

class Section implements IIconSection {
public function __construct(
private readonly IL10N $l,
private readonly IURLGenerator $url,
) {
}

#[\Override]
public function getID(): string {
return 'teams';
}

#[\Override]
public function getName(): string {
return $this->l->t('Teams');
}

#[\Override]
public function getPriority(): int {
return 85;
}

#[\Override]
public function getIcon(): string {
return $this->url->imagePath(Application::APP_ID, 'circles.svg');
}
}
Loading