Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

namespace SP\Infrastructure\Adapter\In\Web\Controllers\ConfigAuth;

use SP\Application\User\Ports\UserProfileService;
use SP\Infrastructure\Adapter\In\Web\Controllers\Helpers\SimpleControllerHelper;
use SP\Application\Application;
use SP\Application\Config\Ports\ConfigBackupService;
Expand All @@ -27,7 +28,8 @@ final class SaveController extends SimpleControllerBase
public function __construct(
Application $application,
SimpleControllerHelper $simpleControllerHelper,
private readonly ConfigBackupService $configBackup
private readonly ConfigBackupService $configBackup,
private readonly UserProfileService $userProfileService
) {
parent::__construct($application, $simpleControllerHelper);
}
Expand Down Expand Up @@ -67,6 +69,20 @@ private function handleAuthConfig(ConfigDataInterface $configData, EventMessage
$configData->setAuthBasicEnabled(true);
$configData->setAuthBasicAutoLoginEnabled($authBasicAutologinEnabled);
$configData->setAuthBasicDomain($authBasicDomain);
// The same authorisation question ConfigLdap\SaveController asks about its own
// defaults, and this door asked neither half of it. These decide the group and profile
// every user auto-provisioned on their first SSO sign-in receives —
// User::createOnLogin() reads them — so setting them is a user-management decision
// reached here with isConfigGeneral(), an independent bit from the isMgmUsers() that
// USER_CREATE answers. Only when they change, so an administrator of the rest of this
// page can still save it.
if ($authSsoDefaultGroup !== $configData->getSsoDefaultGroup()
|| $authSsoDefaultProfile !== $configData->getSsoDefaultProfile()
) {
$this->checkAccess(AclActionsInterface::USER_CREATE);
$this->userProfileService->assertAssignableBy($authSsoDefaultProfile ?? 0);
}

$configData->setSsoDefaultGroup($authSsoDefaultGroup);
$configData->setSsoDefaultProfile($authSsoDefaultProfile);
} elseif ($configData->isAuthBasicEnabled()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

namespace SP\Infrastructure\Adapter\In\Web\Controllers\ConfigLdap;

use SP\Application\User\Ports\UserProfileService;
use SP\Application\Application;
use SP\Domain\Core\Events\Event;
use SP\Domain\Core\Events\EventMessage;
Expand Down Expand Up @@ -53,7 +54,8 @@ final class ImportController extends SimpleControllerBase
public function __construct(
Application $application,
SimpleControllerHelper $simpleControllerHelper,
private readonly LdapImportService $ldapImportService
private readonly LdapImportService $ldapImportService,
private readonly UserProfileService $userProfileService
) {
parent::__construct($application, $simpleControllerHelper);
}
Expand Down Expand Up @@ -149,6 +151,13 @@ protected function initialize(): void
// "may create a user holding any existing profile", including one with mgmUsers itself.
$this->checkAccess(AclActionsInterface::USER_CREATE);

// ...and the profile every imported user is given has to be one this administrator could
// have granted by hand. See ConfigLdap\SaveController: USER_CREATE is "may create users",
// not "may grant this much".
$this->userProfileService->assertAssignableBy(
$this->request->analyzeInt('ldap_defaultprofile') ?? 0
);

$this->extensionChecker->checkLdap(true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

namespace SP\Infrastructure\Adapter\In\Web\Controllers\ConfigLdap;

use SP\Application\User\Ports\UserProfileService;
use SP\Infrastructure\Adapter\In\Web\Controllers\Helpers\SimpleControllerHelper;
use SP\Application\Application;
use SP\Application\Config\Ports\ConfigBackupService;
Expand Down Expand Up @@ -56,7 +57,8 @@ final class SaveController extends SimpleControllerBase
public function __construct(
Application $application,
SimpleControllerHelper $simpleControllerHelper,
private readonly ConfigBackupService $configBackup
private readonly ConfigBackupService $configBackup,
private readonly UserProfileService $userProfileService
) {
parent::__construct($application, $simpleControllerHelper);
}
Expand Down Expand Up @@ -93,6 +95,14 @@ public function saveAction(): ActionResponse
|| $ldapDefaultProfile !== $configData->getLdapDefaultProfile()
) {
$this->checkAccess(AclActionsInterface::USER_CREATE);

// ...and the profile has to be one this administrator could have granted by hand.
// USER_CREATE answers "may create users at all"; it says nothing about how much a
// particular profile grants, which is what assertAssignableBy() is for and what
// every user create/edit door already asks. Without it, a delegated administrator
// holding isConfigGeneral() and isMgmUsers() could name a profile stronger than
// their own and have every LDAP user auto-provisioned into it.
$this->userProfileService->assertAssignableBy($ldapDefaultProfile ?? 0);
}

$ldapParams = LdapParams::fromRequest($this->request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

namespace SP\Tests\Unit\Infrastructure\Adapter\In\Web\Controllers\ConfigAuth;

use SP\Application\User\Ports\UserProfileService;
use SP\Application\Config\Ports\ConfigBackupService;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Test;
Expand Down Expand Up @@ -104,6 +105,8 @@ public function savingKeepsTheConfigurationItReplaces(): void
$this->signedInUserApplication(),
$this->simpleControllerHelper($this->aclThatAllows(), 'configAuth', 'save'),
$configBackup
,
self::createStub(UserProfileService::class)
))->saveAction();
}

Expand Down Expand Up @@ -142,6 +145,8 @@ static function () use (&$order): void {
new Application($config, new EventDispatcher(), $this->signedInUserSession()),
$this->simpleControllerHelper($this->aclThatAllows(), 'configAuth', 'save'),
$configBackup
,
self::createStub(UserProfileService::class)
))->saveAction();

self::assertSame(['backup', 'save'], $order);
Expand Down Expand Up @@ -169,6 +174,8 @@ public function aRefusedSaveKeepsNothing(): void
new Application($config, new EventDispatcher(), $this->signedInUserSession()),
$this->simpleControllerHelper($this->aclThatAllows(), 'configAuth', 'save'),
$configBackup
,
self::createStub(UserProfileService::class)
))->saveAction();

self::assertSame(ResponseStatus::WARNING, $response->status);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@

namespace SP\Tests\Unit\Infrastructure\Adapter\In\Web\Controllers\ConfigAuth;

use SP\Domain\Core\Acl\AclActionsInterface;
use SP\Domain\Common\Services\ServiceException;
use SP\Application\User\Ports\UserProfileService;
use SP\Application\Config\Ports\ConfigBackupService;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Test;
Expand Down Expand Up @@ -95,7 +98,8 @@ public function savingIsRefusedWhenTheAclDenies(): void
new SaveController(
$application,
$this->simpleControllerHelper($acl, 'configAuth', 'save'),
self::createStub(ConfigBackupService::class)
self::createStub(ConfigBackupService::class),
self::createStub(UserProfileService::class)
);
}

Expand All @@ -119,14 +123,90 @@ public function savingReportsAFailureBehindItRatherThanEscaping(): void
$response = (new SaveController(
$application,
$this->simpleControllerHelper($acl, 'configAuth', 'save'),
self::createStub(ConfigBackupService::class)
self::createStub(ConfigBackupService::class),
self::createStub(UserProfileService::class)
))->saveAction();

self::assertSame(ResponseStatus::ERROR, $response->status);
self::assertSame('Error while saving the configuration', $response->subject);
self::assertSame('the configuration file could not be written', $response->extra);
}

/**
* The SSO defaults decide who a single-sign-on user becomes, and this door asked nothing.
*
* `ConfigAuth\SaveController` is reached with `isConfigGeneral()`. The two settings below —
* read by `User::createOnLogin()` for every user auto-provisioned on their first SSO sign-in —
* are a user-management decision, and `ConfigLdap\SaveController` already guards its identical
* pair with `USER_CREATE` plus an assignability check. This one had neither half.
*
* @throws Exception
*/
#[Test]
public function changingTheProfileSsoUsersGetIsRefusedWithoutThatPermission(): void
{
$this->expectException(UnauthorizedPageException::class);

(new SaveController(
$this->signedInUserApplication(),
$this->simpleControllerHelper(
$this->aclThatAllowsAllBut(AclActionsInterface::USER_CREATE),
'configAuth',
'save',
enablingAuthBasic: true
),
self::createStub(ConfigBackupService::class),
self::createStub(UserProfileService::class)
))->saveAction();
}

/**
* ...and the profile named has to be one this administrator could have granted by hand.
*
* @throws Exception
*/
#[Test]
public function namingAnSsoProfileStrongerThanYourOwnIsRefused(): void
{
$userProfileService = $this->createStub(UserProfileService::class);
$userProfileService->method('assertAssignableBy')->willThrowException(
ServiceException::error(
'You cannot assign a profile with more permissions than your own',
'Please contact to the administrator'
)
);

$this->expectException(ServiceException::class);
$this->expectExceptionMessage('You cannot assign a profile with more permissions than your own');

(new SaveController(
$this->signedInUserApplication(),
$this->simpleControllerHelper($this->aclThatAllows(), 'configAuth', 'save', enablingAuthBasic: true),
self::createStub(ConfigBackupService::class),
$userProfileService
))->saveAction();
}

/**
* An ACL that allows everything except the one action named.
*
* @throws Exception
*/
private function aclThatAllowsAllBut(int $action): AclInterface
{
$acl = $this->createStub(AclInterface::class);
$acl->method('checkUserAccess')->willReturnCallback(
static fn(int $actionId): bool => $actionId !== $action
);

return $acl;
}

/**
* A profile id the stored config does not already hold, so the guarded comparison sees a change.
*/
private const A_DIFFERENT_PROFILE = 99;

/**
* `SimpleControllerBase` takes a `SimpleControllerHelper`, not the `WebControllerHelper` the
* shared harness builds for `ControllerBase` subclasses — this mirrors
Expand All @@ -137,14 +217,18 @@ public function savingReportsAFailureBehindItRatherThanEscaping(): void
private function simpleControllerHelper(
AclInterface $acl,
string $controller = 'controller',
string $action = 'action'
string $action = 'action',
bool $enablingAuthBasic = false
): SimpleControllerHelper {
$request = $this->createStub(RequestService::class);
$request->method('isAjax')->willReturn(false);
$request->method('getServer')->willReturn('0');
$request->method('analyzeString')->willReturn(null);
$request->method('analyzeArray')->willReturn(null);
$request->method('analyzeInt')->willReturn(null);

// The SSO defaults are only read, and only guarded, on the branch that enables auth basic.
$request->method('analyzeBool')->willReturn($enablingAuthBasic);
$request->method('analyzeInt')->willReturn($enablingAuthBasic ? self::A_DIFFERENT_PROFILE : null);

$theme = $this->createStub(ThemeInterface::class);
$theme->method('getUri')->willReturn('/theme');
Expand Down
Loading