mirror of
https://github.com/appwrite/appwrite
synced 2026-05-23 08:58:35 +00:00
Merge branch '1.8.x' into lazy-load-relationships
This commit is contained in:
commit
7e026b11b4
10 changed files with 291 additions and 4 deletions
|
|
@ -39,7 +39,8 @@ $console = [
|
|||
'invites' => System::getEnv('_APP_CONSOLE_INVITES', 'enabled') === 'enabled',
|
||||
'limit' => (System::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled') === 'enabled') ? 1 : 0, // limit signup to 1 user
|
||||
'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds
|
||||
'sessionAlerts' => System::getEnv('_APP_CONSOLE_SESSION_ALERTS', 'disabled') === 'enabled'
|
||||
'sessionAlerts' => System::getEnv('_APP_CONSOLE_SESSION_ALERTS', 'disabled') === 'enabled',
|
||||
'invalidateSessions' => true
|
||||
],
|
||||
'authWhitelistEmails' => (!empty(System::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null))) ? \explode(',', System::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null)) : [],
|
||||
'authWhitelistIPs' => (!empty(System::getEnv('_APP_CONSOLE_WHITELIST_IPS', null))) ? \explode(',', System::getEnv('_APP_CONSOLE_WHITELIST_IPS', null)) : [],
|
||||
|
|
|
|||
|
|
@ -2843,6 +2843,18 @@ App::patch('/v1/account/password')
|
|||
->setAttribute('hash', Auth::DEFAULT_ALGO)
|
||||
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS);
|
||||
|
||||
$sessions = $user->getAttribute('sessions', []);
|
||||
$current = Auth::sessionVerify($sessions, Auth::$secret);
|
||||
$invalidate = $project->getAttribute('auths', default: [])['invalidateSessions'] ?? false;
|
||||
if ($invalidate && !empty($current)) {
|
||||
foreach ($sessions as $session) {
|
||||
/** @var Document $session */
|
||||
if ($session->getId() !== $current) {
|
||||
$dbForProject->deleteDocument('sessions', $session->getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
|
||||
$queueForEvents->setParam('userId', $user->getId());
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ App::post('/v1/projects')
|
|||
'membershipsUserName' => false,
|
||||
'membershipsUserEmail' => false,
|
||||
'membershipsMfa' => false,
|
||||
'invalidateSessions' => true
|
||||
];
|
||||
|
||||
foreach ($auth as $method) {
|
||||
|
|
@ -2499,3 +2500,40 @@ App::delete('/v1/projects/:projectId/templates/email/:type/:locale')
|
|||
'message' => $template['message']
|
||||
]), Response::MODEL_EMAIL_TEMPLATE);
|
||||
});
|
||||
|
||||
App::patch('/v1/projects/:projectId/auth/session-invalidation')
|
||||
->desc('Update invalidate session option of the project')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'auth',
|
||||
name: 'updateSessionInvalidation',
|
||||
description: '/docs/references/projects/update-session-invalidation.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||
->param('enabled', false, new Boolean(), 'Update authentication session invalidation status. Use this endpoint to enable or disable session invalidation on password change')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, bool $invalidateSessions, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$auths = $project->getAttribute('auths', []);
|
||||
$auths['invalidateSessions'] = $invalidateSessions;
|
||||
$dbForPlatform->updateDocument('projects', $project->getId(), $project
|
||||
->setAttribute('auths', $auths));
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1351,6 +1351,17 @@ App::patch('/v1/users/:userId/password')
|
|||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
|
||||
$sessions = $user->getAttribute('sessions', []);
|
||||
$invalidate = $project->getAttribute('auths', default: [])['invalidateSessions'] ?? false;
|
||||
if ($invalidate) {
|
||||
foreach ($sessions as $session) {
|
||||
/** @var Document $session */
|
||||
$dbForProject->deleteDocument('sessions', $session->getId());
|
||||
}
|
||||
}
|
||||
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$queueForEvents->setParam('userId', $user->getId());
|
||||
|
||||
$response->dynamic($user, Response::MODEL_USER);
|
||||
|
|
|
|||
1
docs/references/projects/update-session-invalidation.md
Normal file
1
docs/references/projects/update-session-invalidation.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
Invalidate all existing sessions. An optional auth security setting for projects, and enabled by default for console project.
|
||||
|
|
@ -169,6 +169,12 @@ class Project extends Model
|
|||
'default' => false,
|
||||
'example' => true,
|
||||
])
|
||||
->addRule('authInvalidateSessions', [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => 'Whether or not all existing sessions should be invalidated on password change',
|
||||
'default' => false,
|
||||
'example' => true,
|
||||
])
|
||||
->addRule('oAuthProviders', [
|
||||
'type' => Response::MODEL_AUTH_PROVIDER,
|
||||
'description' => 'List of Auth Providers.',
|
||||
|
|
@ -376,6 +382,7 @@ class Project extends Model
|
|||
$document->setAttribute('authMembershipsUserName', $authValues['membershipsUserName'] ?? true);
|
||||
$document->setAttribute('authMembershipsUserEmail', $authValues['membershipsUserEmail'] ?? true);
|
||||
$document->setAttribute('authMembershipsMfa', $authValues['membershipsMfa'] ?? true);
|
||||
$document->setAttribute('authInvalidateSessions', $authValues['invalidateSessions'] ?? false);
|
||||
|
||||
foreach ($auth as $index => $method) {
|
||||
$key = $method['key'];
|
||||
|
|
|
|||
|
|
@ -200,4 +200,17 @@ trait ProjectCustom
|
|||
|
||||
return $key['body']['secret'];
|
||||
}
|
||||
public function updateProjectinvalidateSessionsProperty(bool $value)
|
||||
{
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . self::$project['$id'] . '/auth/session-invalidation', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
|
||||
'x-appwrite-project' => 'console',
|
||||
]), [
|
||||
'enabled' => $value,
|
||||
]);
|
||||
|
||||
return $response['headers']['status-code'];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -480,6 +480,29 @@ class AccountCustomClientTest extends Scope
|
|||
$password = $data['password'] ?? '';
|
||||
$session = $data['session'] ?? '';
|
||||
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
]), [
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/account/sessions', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]));
|
||||
|
||||
$allSessions = array_map(fn ($sessionDetails) => $sessionDetails['$id'], $response['body']['sessions']);
|
||||
|
||||
/**
|
||||
* Test for SUCCESS
|
||||
*/
|
||||
|
|
@ -500,17 +523,140 @@ class AccountCustomClientTest extends Scope
|
|||
$this->assertTrue((new DatetimeValidator())->isValid($response['body']['registration']));
|
||||
$this->assertEquals($response['body']['email'], $email);
|
||||
|
||||
$currentSessionId = $data['sessionId'] ?? '';
|
||||
$response = $this->client->call(Client::METHOD_GET, '/account/sessions', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]));
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(1, $response['body']['total']);
|
||||
// checking the current session or not
|
||||
$this->assertEquals($currentSessionId, $response['body']['sessions'][0]['$id']);
|
||||
$this->assertTrue($response['body']['sessions'][0]['current']);
|
||||
|
||||
// checking for all non active sessions are cleared
|
||||
foreach ($allSessions as $sessionId) {
|
||||
if ($currentSessionId === $sessionId) {
|
||||
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/current', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]));
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
} else {
|
||||
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/'.$sessionId, array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]));
|
||||
|
||||
$this->assertEquals(404, $response['headers']['status-code']);
|
||||
}
|
||||
}
|
||||
|
||||
$newPassword = 'new-password';
|
||||
// updating the invalidateSession to false to check sessions are not invalidated
|
||||
$this->updateProjectinvalidateSessionsProperty(false);
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
]), [
|
||||
'email' => $email,
|
||||
'password' => $newPassword,
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/account/sessions', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]));
|
||||
|
||||
$allSessions = array_map(fn ($sessionDetails) => $sessionDetails['$id'], $response['body']['sessions']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/account/password', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]), [
|
||||
'password' => $newPassword,
|
||||
'oldPassword' => $newPassword,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
foreach ($allSessions as $sessionId) {
|
||||
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/'.$sessionId, headers: array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]));
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
// setting invalidateSession to true to check the sessions are cleared or not
|
||||
$this->updateProjectinvalidateSessionsProperty(true);
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/account/password', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]), [
|
||||
'password' => $newPassword,
|
||||
'oldPassword' => $newPassword,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/account/sessions', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]));
|
||||
|
||||
$allSessions = array_map(fn ($sessionDetails) => $sessionDetails['$id'], $response['body']['sessions']);
|
||||
|
||||
foreach ($allSessions as $sessionId) {
|
||||
if ($currentSessionId !== $sessionId) {
|
||||
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/'.$sessionId, array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]));
|
||||
|
||||
$this->assertEquals(404, $response['headers']['status-code']);
|
||||
}
|
||||
}
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
]), [
|
||||
'email' => $email,
|
||||
'password' => 'new-password',
|
||||
'password' => $newPassword,
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
|
||||
/**
|
||||
* Test for FAILURE
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -951,6 +951,55 @@ class ProjectsConsoleClientTest extends Scope
|
|||
return ['projectId' => $projectId];
|
||||
}
|
||||
|
||||
/** @depends testCreateProject */
|
||||
public function testUpdateProjectInvalidateSessions($data): array
|
||||
{
|
||||
$id = $data['projectId'];
|
||||
|
||||
// Check defaults
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => 'console',
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertTrue($response['body']['authInvalidateSessions']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/session-invalidation', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'enabled' => false,
|
||||
]);
|
||||
$this->assertFalse($response['body']['authInvalidateSessions']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => 'console',
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertFalse($response['body']['authInvalidateSessions']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/session-invalidation', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'enabled' => true,
|
||||
]);
|
||||
$this->assertTrue($response['body']['authInvalidateSessions']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => 'console',
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertTrue($response['body']['authInvalidateSessions']);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testCreateProject
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1117,7 +1117,7 @@ trait UsersBase
|
|||
]);
|
||||
|
||||
$this->assertEquals(401, $session['headers']['status-code']);
|
||||
|
||||
$this->updateProjectinvalidateSessionsProperty(true);
|
||||
$user = $this->client->call(Client::METHOD_PATCH, '/users/' . $data['userId'] . '/password', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
|
|
@ -1129,6 +1129,15 @@ trait UsersBase
|
|||
$this->assertNotEmpty($user['body']['$id']);
|
||||
$this->assertNotEmpty($user['body']['password']);
|
||||
|
||||
$sessions = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/sessions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals($sessions['headers']['status-code'], 200);
|
||||
$this->assertIsArray($sessions['body']);
|
||||
$this->assertEmpty($sessions['body']['sessions']);
|
||||
|
||||
$session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
|
|
@ -1138,7 +1147,7 @@ trait UsersBase
|
|||
]);
|
||||
|
||||
$this->assertEquals($session['headers']['status-code'], 201);
|
||||
|
||||
$this->updateProjectinvalidateSessionsProperty(false);
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue