feat(Roles): admin always has full permissions

This commit is contained in:
2025-11-22 23:37:05 +02:00
parent 94787052ef
commit c9a76e5cd9
7 changed files with 161 additions and 23 deletions

View File

@@ -14,6 +14,7 @@ use OCA\Forum\Db\CategoryPermMapper;
use OCA\Forum\Db\CatHeaderMapper;
use OCA\Forum\Db\ThreadMapper;
use OCA\Forum\Db\UserRoleMapper;
use OCA\Forum\Service\UserRoleService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\ApiRoute;
@@ -384,7 +385,8 @@ class CategoryController extends OCSController {
#[ApiRoute(verb: 'GET', url: '/api/categories/{id}/permissions')]
public function getPermissions(int $id): DataResponse {
try {
$permissions = $this->categoryPermMapper->findByCategoryId($id);
// Exclude Admin role - it has hardcoded full access to all categories
$permissions = $this->categoryPermMapper->findByCategoryIdExcludingAdmin($id);
return new DataResponse(array_map(fn ($perm) => $perm->jsonSerialize(), $permissions));
} catch (\Exception $e) {
$this->logger->error('Error fetching category permissions: ' . $e->getMessage());
@@ -412,8 +414,13 @@ class CategoryController extends OCSController {
// Delete existing permissions for this category
$this->categoryPermMapper->deleteByCategoryId($id);
// Filter out Admin role - it has hardcoded full access
$filteredPermissions = array_filter($permissions, fn ($perm)
=> ($perm['roleId'] ?? null) !== UserRoleService::ROLE_ADMIN
);
// Insert new permissions
foreach ($permissions as $perm) {
foreach ($filteredPermissions as $perm) {
$categoryPerm = new CategoryPerm();
$categoryPerm->setCategoryId($id);
$categoryPerm->setRoleId($perm['roleId']);

View File

@@ -164,14 +164,22 @@ class RoleController extends OCSController {
if ($colorDark !== null) {
$role->setColorDark($colorDark);
}
if ($canAccessAdminTools !== null) {
$role->setCanAccessAdminTools($canAccessAdminTools);
}
if ($canEditRoles !== null) {
$role->setCanEditRoles($canEditRoles);
}
if ($canEditCategories !== null) {
$role->setCanEditCategories($canEditCategories);
// Admin role always has all permissions - cannot be changed
if ($id === UserRoleService::ROLE_ADMIN) {
$role->setCanAccessAdminTools(true);
$role->setCanEditRoles(true);
$role->setCanEditCategories(true);
} else {
if ($canAccessAdminTools !== null) {
$role->setCanAccessAdminTools($canAccessAdminTools);
}
if ($canEditRoles !== null) {
$role->setCanEditRoles($canEditRoles);
}
if ($canEditCategories !== null) {
$role->setCanEditCategories($canEditCategories);
}
}
/** @var \OCA\Forum\Db\Role */

View File

@@ -8,6 +8,7 @@ declare(strict_types=1);
namespace OCA\Forum\Db;
use OCA\Forum\AppInfo\Application;
use OCA\Forum\Service\UserRoleService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
@@ -67,6 +68,26 @@ class CategoryPermMapper extends QBMapper {
return $this->findEntities($qb);
}
/**
* Find permissions for a category, excluding Admin role (which has implicit full access)
*
* @param int $categoryId Category ID
* @return array<CategoryPerm>
*/
public function findByCategoryIdExcludingAdmin(int $categoryId): array {
/* @var $qb IQueryBuilder */
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
->where(
$qb->expr()->eq('category_id', $qb->createNamedParameter($categoryId, IQueryBuilder::PARAM_INT))
)
->andWhere(
$qb->expr()->neq('role_id', $qb->createNamedParameter(UserRoleService::ROLE_ADMIN, IQueryBuilder::PARAM_INT))
);
return $this->findEntities($qb);
}
/**
* Find permission for specific category and role
*

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
// SPDX-FileCopyrightText: Chen Asraf <contact@casraf.dev>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\Forum\Migration;
use Closure;
use OCA\Forum\AppInfo\Application;
use OCA\Forum\Service\UserRoleService;
use OCP\DB\ISchemaWrapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version6Date20251122233018 extends SimpleMigrationStep {
public function __construct(
private IDBConnection $db,
) {
}
/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
*/
public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
}
/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
// TODO add migration logic
return $schema;
}
/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
*/
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
// Remove Admin role permissions from categories
// Admin role now has hardcoded full access to all categories
$qb = $this->db->getQueryBuilder();
$qb->delete(Application::tableName('forum_category_perms'))
->where(
$qb->expr()->eq('role_id', $qb->createNamedParameter(UserRoleService::ROLE_ADMIN, IQueryBuilder::PARAM_INT))
);
$deletedCount = $qb->executeStatement();
$output->info("Removed $deletedCount Admin role permission entries from categories (Admin has hardcoded full access)");
}
}

View File

@@ -28,11 +28,34 @@ class PermissionService {
) {
}
/**
* Check if user has Admin role
*
* @param string $userId Nextcloud user ID
* @return bool True if user has Admin role
*/
private function hasAdminRole(string $userId): bool {
try {
$userRoles = $this->userRoleMapper->findByUserId($userId);
foreach ($userRoles as $userRole) {
if ($userRole->getRoleId() === UserRoleService::ROLE_ADMIN) {
return true;
}
}
return false;
} catch (\Exception $e) {
$this->logger->error("Error checking admin role for user $userId: " . $e->getMessage());
return false;
}
}
/**
* Check if user has Admin or Moderator role
*
* @param string $userId Nextcloud user ID
* @return bool True if user has Admin (roleId 1) or Moderator (roleId 2) role
* @return bool True if user has Admin or Moderator role
*/
public function hasAdminOrModeratorRole(string $userId): bool {
try {
@@ -40,8 +63,7 @@ class PermissionService {
foreach ($userRoles as $userRole) {
$roleId = $userRole->getRoleId();
// Admin role = 1, Moderator role = 2
if ($roleId === 1 || $roleId === 2) {
if ($roleId === UserRoleService::ROLE_ADMIN || $roleId === UserRoleService::ROLE_MODERATOR) {
return true;
}
}
@@ -125,6 +147,12 @@ class PermissionService {
* @return bool True if user has the permission
*/
public function hasCategoryPermission(string $userId, int $categoryId, string $permission): bool {
// Admin role has hardcoded full access to all categories
if ($this->hasAdminRole($userId)) {
$this->logger->debug("User $userId has Admin role - granting category permission '$permission' on category $categoryId");
return true;
}
try {
$userRoles = $this->userRoleMapper->findByUserId($userId);

View File

@@ -323,10 +323,13 @@ export default defineComponent({
}))
},
roleOptions(): Array<{ id: number; label: string }> {
return this.roles.map((role) => ({
id: role.id,
label: role.name,
}))
// Filter out Admin role - it has implicit full access to all categories
return this.roles
.filter((role) => role.id !== SystemRole.ADMIN)
.map((role) => ({
id: role.id,
label: role.name,
}))
},
},
watch: {

View File

@@ -115,21 +115,21 @@
<div class="permissions-checkboxes">
<div class="checkbox-group">
<NcCheckboxRadioSwitch v-model="formData.canAccessAdminTools">
<NcCheckboxRadioSwitch v-model="formData.canAccessAdminTools" :disabled="isAdmin">
<strong>{{ strings.canAccessAdminTools }}</strong>
<span class="checkbox-desc muted">{{ strings.canAccessAdminToolsDesc }}</span>
</NcCheckboxRadioSwitch>
</div>
<div class="checkbox-group">
<NcCheckboxRadioSwitch v-model="formData.canEditRoles">
<NcCheckboxRadioSwitch v-model="formData.canEditRoles" :disabled="isAdmin">
<strong>{{ strings.canEditRoles }}</strong>
<span class="checkbox-desc muted">{{ strings.canEditRolesDesc }}</span>
</NcCheckboxRadioSwitch>
</div>
<div class="checkbox-group">
<NcCheckboxRadioSwitch v-model="formData.canEditCategories">
<NcCheckboxRadioSwitch v-model="formData.canEditCategories" :disabled="isAdmin">
<strong>{{ strings.canEditCategories }}</strong>
<span class="checkbox-desc muted">{{ strings.canEditCategoriesDesc }}</span>
</NcCheckboxRadioSwitch>
@@ -399,6 +399,13 @@ export default defineComponent({
this.formData.canEditRoles = role.canEditRoles || false
this.formData.canEditCategories = role.canEditCategories || false
// Admin role always has all permissions
if (this.isAdmin) {
this.formData.canAccessAdminTools = true
this.formData.canEditRoles = true
this.formData.canEditCategories = true
}
// If colors are different, mark dark as modified
if (role.colorLight && role.colorDark && role.colorLight !== role.colorDark) {
this.darkColorModified = true
@@ -452,9 +459,9 @@ export default defineComponent({
description: this.formData.description.trim() || null,
colorLight: this.formData.colorLight || null,
colorDark: this.formData.colorDark || null,
canAccessAdminTools: this.formData.canAccessAdminTools,
canEditRoles: this.formData.canEditRoles,
canEditCategories: this.formData.canEditCategories,
canAccessAdminTools: this.isAdmin ? true : this.formData.canAccessAdminTools,
canEditRoles: this.isAdmin ? true : this.formData.canEditRoles,
canEditCategories: this.isAdmin ? true : this.formData.canEditCategories,
}
let roleId: number