mirror of
https://github.com/appwrite/appwrite
synced 2026-05-24 09:28:40 +00:00
Merge pull request #9615 from appwrite/fix-owner-downgrade
Restrict role change for sole org owner
This commit is contained in:
commit
2d7b01aa35
2 changed files with 48 additions and 2 deletions
|
|
@ -1034,7 +1034,6 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
|
||||||
->param('membershipId', '', new UID(), 'Membership ID.')
|
->param('membershipId', '', new UID(), 'Membership ID.')
|
||||||
->param('roles', [], function (Document $project) {
|
->param('roles', [], function (Document $project) {
|
||||||
if ($project->getId() === 'console') {
|
if ($project->getId() === 'console') {
|
||||||
;
|
|
||||||
$roles = array_keys(Config::getParam('roles', []));
|
$roles = array_keys(Config::getParam('roles', []));
|
||||||
array_filter($roles, function ($role) {
|
array_filter($roles, function ($role) {
|
||||||
return !in_array($role, [Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS, Auth::USER_ROLE_USERS]);
|
return !in_array($role, [Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS, Auth::USER_ROLE_USERS]);
|
||||||
|
|
@ -1046,9 +1045,10 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
|
||||||
->inject('request')
|
->inject('request')
|
||||||
->inject('response')
|
->inject('response')
|
||||||
->inject('user')
|
->inject('user')
|
||||||
|
->inject('project')
|
||||||
->inject('dbForProject')
|
->inject('dbForProject')
|
||||||
->inject('queueForEvents')
|
->inject('queueForEvents')
|
||||||
->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) {
|
->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents) {
|
||||||
|
|
||||||
$team = $dbForProject->getDocument('teams', $teamId);
|
$team = $dbForProject->getDocument('teams', $teamId);
|
||||||
if ($team->isEmpty()) {
|
if ($team->isEmpty()) {
|
||||||
|
|
@ -1069,6 +1069,21 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
|
||||||
$isAppUser = Auth::isAppUser(Authorization::getRoles());
|
$isAppUser = Auth::isAppUser(Authorization::getRoles());
|
||||||
$isOwner = Authorization::isRole('team:' . $team->getId() . '/owner');
|
$isOwner = Authorization::isRole('team:' . $team->getId() . '/owner');
|
||||||
|
|
||||||
|
if ($project->getId() === 'console') {
|
||||||
|
// Quick check: fetch up to 2 owners to determine if only one exists
|
||||||
|
$ownersCount = $dbForProject->count(
|
||||||
|
collection: 'memberships',
|
||||||
|
queries: [Query::contains('roles', ['owner'])],
|
||||||
|
max: 2
|
||||||
|
);
|
||||||
|
|
||||||
|
// Prevent role change if there's only one owner left,
|
||||||
|
// the requester is that owner, and the new `$roles` no longer include 'owner'!
|
||||||
|
if ($ownersCount === 1 && $isOwner && !\in_array('owner', $roles)) {
|
||||||
|
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'There must be at least one owner in the organization.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!$isOwner && !$isPrivilegedUser && !$isAppUser) { // Not owner, not admin, not app (server)
|
if (!$isOwner && !$isPrivilegedUser && !$isAppUser) { // Not owner, not admin, not app (server)
|
||||||
throw new Exception(Exception::USER_UNAUTHORIZED, 'User is not allowed to modify roles');
|
throw new Exception(Exception::USER_UNAUTHORIZED, 'User is not allowed to modify roles');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,37 @@ trait TeamsBase
|
||||||
$teamUid = $response1['body']['$id'];
|
$teamUid = $response1['body']['$id'];
|
||||||
$teamName = $response1['body']['name'];
|
$teamName = $response1['body']['name'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test: Attempt to downgrade the only OWNER in an organization (should fail)
|
||||||
|
*/
|
||||||
|
if ($this->getProject()['$id'] === 'console') {
|
||||||
|
// Step 1: Fetch all team memberships — only one exists at this point
|
||||||
|
$response = $this->client->call(Client::METHOD_GET, '/teams/' . $teamUid . '/memberships', array_merge([
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $this->getProject()['$id'],
|
||||||
|
], $this->getHeaders()), [
|
||||||
|
'queries' => [
|
||||||
|
Query::limit(1)->toString(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Step 2: Extract the membership ID of the only member (also the only OWNER)
|
||||||
|
$membershipID = $response['body']['memberships'][0]['$id'];
|
||||||
|
|
||||||
|
// Step 3: Attempt to downgrade the member's role to 'developer'
|
||||||
|
$response = $this->client->call(Client::METHOD_PATCH, '/teams/' . $teamUid . '/memberships/' . $membershipID, array_merge([
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $this->getProject()['$id'],
|
||||||
|
], $this->getHeaders()), [
|
||||||
|
'roles' => ['developer']
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Step 4: Assert failure — cannot remove the only OWNER from a team
|
||||||
|
$this->assertEquals(400, $response['headers']['status-code']);
|
||||||
|
$this->assertEquals('general_argument_invalid', $response['body']['type']);
|
||||||
|
$this->assertEquals('There must be at least one owner in the organization.', $response['body']['message']);
|
||||||
|
}
|
||||||
|
|
||||||
$teamId = ID::unique();
|
$teamId = ID::unique();
|
||||||
$response2 = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
|
$response2 = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
|
||||||
'content-type' => 'application/json',
|
'content-type' => 'application/json',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue