mirror of
https://github.com/chenasraf/nextcloud-forum.git
synced 2026-05-18 01:28:58 +00:00
Implement opt-in guest access system allowing unauthenticated users to view forum content with configurable permissions. Features Added: - Guest access toggle in admin settings for forum-wide control - Guest role with configurable category-level permissions (view/post/reply) - Role type system (admin, moderator, default, guest, custom) for enhanced access control - Public page support in routing with automatic redirect to login when disabled - Public settings API endpoint for unauthenticated access to forum metadata - Guest role permissions UI in admin panel with clear capability restrictions - Database migration with automatic role type assignment and guest role seeding Security & Permission Improvements: - Permission middleware now validates permissions on public pages instead of skipping checks - Admin/moderator roles have full access; guest/default roles restricted from moderation - Guest role cannot be assigned to authenticated users Breaking Changes: - Forum title/subtitle moved from system config to app config (auto-migrated) - Permission middleware behavior changed for PublicPage routes (now checks permissions)
407 lines
14 KiB
PHP
407 lines
14 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace OCA\Forum\Tests\Middleware;
|
|
|
|
use OCA\Forum\Attribute\RequirePermission;
|
|
use OCA\Forum\Middleware\PermissionMiddleware;
|
|
use OCA\Forum\Service\PermissionService;
|
|
use OCP\AppFramework\Controller;
|
|
use OCP\AppFramework\OCS\OCSForbiddenException;
|
|
use OCP\AppFramework\Services\IAppConfig;
|
|
use OCP\IRequest;
|
|
use OCP\IUser;
|
|
use OCP\IUserSession;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
/**
|
|
* Test controller with permission attributes
|
|
*/
|
|
class TestPermissionController extends Controller {
|
|
public function methodWithoutPermissions(): void {
|
|
}
|
|
|
|
#[RequirePermission('canEditRoles')]
|
|
public function methodWithGlobalPermission(): void {
|
|
}
|
|
|
|
#[RequirePermission('canView', resourceType: 'category', resourceIdParam: 'categoryId')]
|
|
public function methodWithCategoryPermission(): void {
|
|
}
|
|
|
|
#[RequirePermission('canPost', resourceType: 'category', resourceIdParam: 'categoryId')]
|
|
public function methodWithCategoryPostPermission(): void {
|
|
}
|
|
|
|
#[RequirePermission('canView', resourceType: 'category', resourceIdFromThreadId: 'threadId')]
|
|
public function methodWithThreadPermission(): void {
|
|
}
|
|
|
|
#[RequirePermission('canReply', resourceType: 'category', resourceIdFromPostId: 'postId')]
|
|
public function methodWithPostPermission(): void {
|
|
}
|
|
|
|
#[RequirePermission('canView', resourceType: 'invalid', resourceIdParam: 'id')]
|
|
public function methodWithInvalidResource(): void {
|
|
}
|
|
}
|
|
|
|
class PermissionMiddlewareTest extends TestCase {
|
|
private PermissionMiddleware $middleware;
|
|
private IRequest $request;
|
|
private IUserSession $userSession;
|
|
private PermissionService $permissionService;
|
|
private IAppConfig $config;
|
|
private LoggerInterface $logger;
|
|
private TestPermissionController $controller;
|
|
|
|
protected function setUp(): void {
|
|
$this->request = $this->createMock(IRequest::class);
|
|
$this->userSession = $this->createMock(IUserSession::class);
|
|
$this->permissionService = $this->createMock(PermissionService::class);
|
|
$this->config = $this->createMock(IAppConfig::class);
|
|
$this->logger = $this->createMock(LoggerInterface::class);
|
|
|
|
$this->middleware = new PermissionMiddleware(
|
|
$this->request,
|
|
$this->userSession,
|
|
$this->permissionService,
|
|
$this->config,
|
|
$this->logger
|
|
);
|
|
|
|
$this->controller = new TestPermissionController('forum', $this->request);
|
|
}
|
|
|
|
public function testAuthenticatedUserWithNoPermissionAttributesIsAllowed(): void {
|
|
$user = $this->createMock(IUser::class);
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
$this->config->method('getAppValueBool')
|
|
->with('allow_guest_access', false, true)
|
|
->willReturn(false);
|
|
|
|
// Should not throw exception
|
|
$this->middleware->beforeController($this->controller, 'methodWithoutPermissions');
|
|
$this->assertTrue(true); // If we get here, test passed
|
|
}
|
|
|
|
public function testAuthenticatedUserWithGlobalPermissionIsAllowed(): void {
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn('user1');
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
$this->config->method('getAppValueBool')
|
|
->with('allow_guest_access', false, true)
|
|
->willReturn(false);
|
|
|
|
$this->permissionService->expects($this->once())
|
|
->method('hasGlobalPermission')
|
|
->with('user1', 'canEditRoles')
|
|
->willReturn(true);
|
|
|
|
// Should not throw exception
|
|
$this->middleware->beforeController($this->controller, 'methodWithGlobalPermission');
|
|
$this->assertTrue(true);
|
|
}
|
|
|
|
public function testAuthenticatedUserWithoutGlobalPermissionIsDenied(): void {
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn('user1');
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
$this->config->method('getAppValueBool')
|
|
->with('allow_guest_access', false, true)
|
|
->willReturn(false);
|
|
|
|
$this->permissionService->expects($this->once())
|
|
->method('hasGlobalPermission')
|
|
->with('user1', 'canEditRoles')
|
|
->willReturn(false);
|
|
|
|
$this->expectException(OCSForbiddenException::class);
|
|
$this->middleware->beforeController($this->controller, 'methodWithGlobalPermission');
|
|
}
|
|
|
|
public function testUnauthenticatedUserWithGuestAccessDisabledIsDenied(): void {
|
|
$this->userSession->method('getUser')->willReturn(null);
|
|
$this->config->method('getAppValueBool')
|
|
->with('allow_guest_access', false, true)
|
|
->willReturn(false);
|
|
|
|
$this->expectException(OCSForbiddenException::class);
|
|
$this->expectExceptionMessage('User not authenticated');
|
|
$this->middleware->beforeController($this->controller, 'methodWithoutPermissions');
|
|
}
|
|
|
|
public function testUnauthenticatedUserWithGuestAccessEnabledAndGetMethodIsAllowed(): void {
|
|
$this->userSession->method('getUser')->willReturn(null);
|
|
$this->config->method('getAppValueBool')
|
|
->with('allow_guest_access', false, true)
|
|
->willReturn(true);
|
|
$this->request->method('getMethod')->willReturn('GET');
|
|
|
|
// Should not throw exception
|
|
$this->middleware->beforeController($this->controller, 'methodWithoutPermissions');
|
|
$this->assertTrue(true);
|
|
}
|
|
|
|
public function testUnauthenticatedUserWithGuestAccessEnabledAndHeadMethodIsAllowed(): void {
|
|
$this->userSession->method('getUser')->willReturn(null);
|
|
$this->config->method('getAppValueBool')
|
|
->with('allow_guest_access', false, true)
|
|
->willReturn(true);
|
|
$this->request->method('getMethod')->willReturn('HEAD');
|
|
|
|
// Should not throw exception
|
|
$this->middleware->beforeController($this->controller, 'methodWithoutPermissions');
|
|
$this->assertTrue(true);
|
|
}
|
|
|
|
public function testUnauthenticatedUserWithGuestAccessEnabledAndOptionsMethodIsAllowed(): void {
|
|
$this->userSession->method('getUser')->willReturn(null);
|
|
$this->config->method('getAppValueBool')
|
|
->with('allow_guest_access', false, true)
|
|
->willReturn(true);
|
|
$this->request->method('getMethod')->willReturn('OPTIONS');
|
|
|
|
// Should not throw exception
|
|
$this->middleware->beforeController($this->controller, 'methodWithoutPermissions');
|
|
$this->assertTrue(true);
|
|
}
|
|
|
|
public function testUnauthenticatedUserWithGuestAccessEnabledAndPostMethodIsDenied(): void {
|
|
$this->userSession->method('getUser')->willReturn(null);
|
|
$this->config->method('getAppValueBool')
|
|
->with('allow_guest_access', false, true)
|
|
->willReturn(true);
|
|
$this->request->method('getMethod')->willReturn('POST');
|
|
|
|
$this->expectException(OCSForbiddenException::class);
|
|
$this->expectExceptionMessage('User not authenticated');
|
|
$this->middleware->beforeController($this->controller, 'methodWithoutPermissions');
|
|
}
|
|
|
|
public function testUnauthenticatedUserWithGuestAccessEnabledAndPutMethodIsDenied(): void {
|
|
$this->userSession->method('getUser')->willReturn(null);
|
|
$this->config->method('getAppValueBool')
|
|
->with('allow_guest_access', false, true)
|
|
->willReturn(true);
|
|
$this->request->method('getMethod')->willReturn('PUT');
|
|
|
|
$this->expectException(OCSForbiddenException::class);
|
|
$this->expectExceptionMessage('User not authenticated');
|
|
$this->middleware->beforeController($this->controller, 'methodWithoutPermissions');
|
|
}
|
|
|
|
public function testUnauthenticatedUserWithGuestAccessEnabledAndDeleteMethodIsDenied(): void {
|
|
$this->userSession->method('getUser')->willReturn(null);
|
|
$this->config->method('getAppValueBool')
|
|
->with('allow_guest_access', false, true)
|
|
->willReturn(true);
|
|
$this->request->method('getMethod')->willReturn('DELETE');
|
|
|
|
$this->expectException(OCSForbiddenException::class);
|
|
$this->expectExceptionMessage('User not authenticated');
|
|
$this->middleware->beforeController($this->controller, 'methodWithoutPermissions');
|
|
}
|
|
|
|
public function testGuestUserWithGlobalPermissionCheckUsesNullUserId(): void {
|
|
$this->userSession->method('getUser')->willReturn(null);
|
|
$this->config->method('getAppValueBool')
|
|
->with('allow_guest_access', false, true)
|
|
->willReturn(true);
|
|
$this->request->method('getMethod')->willReturn('GET');
|
|
|
|
$this->permissionService->expects($this->once())
|
|
->method('hasGlobalPermission')
|
|
->with(null, 'canEditRoles')
|
|
->willReturn(false);
|
|
|
|
$this->expectException(OCSForbiddenException::class);
|
|
$this->middleware->beforeController($this->controller, 'methodWithGlobalPermission');
|
|
}
|
|
|
|
public function testGuestUserWithCategoryPermissionCheckUsesNullUserId(): void {
|
|
$this->userSession->method('getUser')->willReturn(null);
|
|
$this->config->method('getAppValueBool')
|
|
->with('allow_guest_access', false, true)
|
|
->willReturn(true);
|
|
$this->request->method('getMethod')->willReturn('GET');
|
|
$this->request->method('getParam')
|
|
->with('categoryId')
|
|
->willReturn('1');
|
|
|
|
$this->permissionService->expects($this->once())
|
|
->method('hasCategoryPermission')
|
|
->with(null, 1, 'canView')
|
|
->willReturn(true);
|
|
|
|
// Should not throw exception
|
|
$this->middleware->beforeController($this->controller, 'methodWithCategoryPermission');
|
|
$this->assertTrue(true);
|
|
}
|
|
|
|
public function testGuestUserWithoutCategoryPermissionIsDenied(): void {
|
|
$this->userSession->method('getUser')->willReturn(null);
|
|
$this->config->method('getAppValueBool')
|
|
->with('allow_guest_access', false, true)
|
|
->willReturn(true);
|
|
$this->request->method('getMethod')->willReturn('GET');
|
|
$this->request->method('getParam')
|
|
->with('categoryId')
|
|
->willReturn('1');
|
|
|
|
$this->permissionService->expects($this->once())
|
|
->method('hasCategoryPermission')
|
|
->with(null, 1, 'canPost')
|
|
->willReturn(false);
|
|
|
|
$this->expectException(OCSForbiddenException::class);
|
|
$this->middleware->beforeController($this->controller, 'methodWithCategoryPostPermission');
|
|
}
|
|
|
|
public function testMultiplePermissionAttributesAllMustPass(): void {
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn('user1');
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
$this->config->method('getAppValueBool')
|
|
->with('allow_guest_access', false, true)
|
|
->willReturn(false);
|
|
|
|
// First permission check passes
|
|
$this->permissionService->expects($this->once())
|
|
->method('hasGlobalPermission')
|
|
->with('user1', 'canEditRoles')
|
|
->willReturn(true);
|
|
|
|
// Should not throw exception when permission passes
|
|
$this->middleware->beforeController($this->controller, 'methodWithGlobalPermission');
|
|
$this->assertTrue(true);
|
|
}
|
|
|
|
public function testCategoryPermissionFromThreadId(): void {
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn('user1');
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
$this->config->method('getAppValueBool')
|
|
->with('allow_guest_access', false, true)
|
|
->willReturn(false);
|
|
|
|
$this->request->method('getParam')
|
|
->with('threadId')
|
|
->willReturn('10');
|
|
|
|
$this->permissionService->expects($this->once())
|
|
->method('getCategoryIdFromThread')
|
|
->with(10)
|
|
->willReturn(5);
|
|
|
|
$this->permissionService->expects($this->once())
|
|
->method('hasCategoryPermission')
|
|
->with('user1', 5, 'canView')
|
|
->willReturn(true);
|
|
|
|
// Should not throw exception
|
|
$this->middleware->beforeController($this->controller, 'methodWithThreadPermission');
|
|
$this->assertTrue(true);
|
|
}
|
|
|
|
public function testCategoryPermissionFromPostId(): void {
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn('user1');
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
$this->config->method('getAppValueBool')
|
|
->with('allow_guest_access', false, true)
|
|
->willReturn(false);
|
|
|
|
$this->request->method('getParam')
|
|
->with('postId')
|
|
->willReturn('20');
|
|
|
|
$this->permissionService->expects($this->once())
|
|
->method('getCategoryIdFromPost')
|
|
->with(20)
|
|
->willReturn(7);
|
|
|
|
$this->permissionService->expects($this->once())
|
|
->method('hasCategoryPermission')
|
|
->with('user1', 7, 'canReply')
|
|
->willReturn(true);
|
|
|
|
// Should not throw exception
|
|
$this->middleware->beforeController($this->controller, 'methodWithPostPermission');
|
|
$this->assertTrue(true);
|
|
}
|
|
|
|
public function testInvalidResourceTypeThrowsException(): void {
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn('user1');
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
$this->config->method('getAppValueBool')
|
|
->with('allow_guest_access', false, true)
|
|
->willReturn(false);
|
|
|
|
$this->expectException(OCSForbiddenException::class);
|
|
$this->middleware->beforeController($this->controller, 'methodWithInvalidResource');
|
|
}
|
|
|
|
public function testMissingResourceIdParameterThrowsException(): void {
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn('user1');
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
$this->config->method('getAppValueBool')
|
|
->with('allow_guest_access', false, true)
|
|
->willReturn(false);
|
|
|
|
$this->request->method('getParam')
|
|
->with('categoryId')
|
|
->willReturn(null);
|
|
|
|
$this->expectException(OCSForbiddenException::class);
|
|
$this->middleware->beforeController($this->controller, 'methodWithCategoryPermission');
|
|
}
|
|
|
|
public function testGuestAccessWithMultipleReadMethods(): void {
|
|
$this->userSession->method('getUser')->willReturn(null);
|
|
$this->config->method('getAppValueBool')
|
|
->with('allow_guest_access', false, true)
|
|
->willReturn(true);
|
|
|
|
// Test with GET
|
|
$this->request->method('getMethod')->willReturn('GET');
|
|
$this->middleware->beforeController($this->controller, 'methodWithoutPermissions');
|
|
|
|
// Test with HEAD
|
|
$this->request = $this->createMock(IRequest::class);
|
|
$this->request->method('getMethod')->willReturn('HEAD');
|
|
$this->middleware = new PermissionMiddleware(
|
|
$this->request,
|
|
$this->userSession,
|
|
$this->permissionService,
|
|
$this->config,
|
|
$this->logger
|
|
);
|
|
$this->middleware->beforeController($this->controller, 'methodWithoutPermissions');
|
|
|
|
$this->assertTrue(true);
|
|
}
|
|
|
|
public function testAuthenticatedUserBypassesGuestRestrictions(): void {
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn('user1');
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
|
|
// Guest access is disabled, but authenticated users should still work
|
|
$this->config->method('getAppValueBool')
|
|
->with('allow_guest_access', false, true)
|
|
->willReturn(false);
|
|
|
|
// POST method, which would be blocked for guests
|
|
$this->request->method('getMethod')->willReturn('POST');
|
|
|
|
// Should not throw exception for authenticated user
|
|
$this->middleware->beforeController($this->controller, 'methodWithoutPermissions');
|
|
$this->assertTrue(true);
|
|
}
|
|
}
|