Merge pull request #9006 from appwrite/fix-team-invite-session-creation

fix: team invites with existing session
This commit is contained in:
Christy Jacob 2024-11-22 09:38:40 +01:00 committed by GitHub
commit 7ccc74bae2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 130 additions and 45 deletions

View file

@ -1060,7 +1060,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
throw new Exception(Exception::TEAM_INVITE_MISMATCH, 'Invite does not belong to current user (' . $user->getAttribute('email') . ')');
}
if ($user->isEmpty()) {
$hasSession = !$user->isEmpty();
if (!$hasSession) {
$user->setAttributes($dbForProject->getDocument('users', $userId)->getArrayCopy()); // Get user
}
@ -1079,39 +1080,64 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
Authorization::skip(fn () => $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', true)));
// Log user in
// Create session for the user if not logged in
if (!$hasSession) {
Authorization::setRole(Role::user($user->getId())->toString());
Authorization::setRole(Role::user($user->getId())->toString());
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$expire = DateTime::addSeconds(new \DateTime(), $authDuration);
$secret = Auth::tokenGenerator();
$session = new Document(array_merge([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
],
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'providerUid' => $user->getAttribute('email'),
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
'factors' => ['email'],
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
'expire' => DateTime::addSeconds(new \DateTime(), $authDuration)
], $detector->getOS(), $detector->getClient(), $detector->getDevice()));
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$expire = DateTime::addSeconds(new \DateTime(), $authDuration);
$secret = Auth::tokenGenerator();
$session = new Document(array_merge([
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'providerUid' => $user->getAttribute('email'),
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
'factors' => ['email'],
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
'expire' => DateTime::addSeconds(new \DateTime(), $authDuration)
], $detector->getOS(), $detector->getClient(), $detector->getDevice()));
$session = $dbForProject->createDocument('sessions', $session);
$session = $dbForProject->createDocument('sessions', $session
->setAttribute('$permissions', [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
]));
Authorization::setRole(Role::user($userId)->toString());
$dbForProject->purgeCachedDocument('users', $user->getId());
if (!Config::getParam('domainVerification')) {
$response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]));
}
Authorization::setRole(Role::user($userId)->toString());
$response
->addCookie(
name: Auth::$cookieName . '_legacy',
value: Auth::encodeSession($user->getId(), $secret),
expire: (new \DateTime($expire))->getTimestamp(),
path: '/',
domain: Config::getParam('cookieDomain'),
secure: ('https' === $protocol),
httponly: true
)
->addCookie(
name: Auth::$cookieName,
value: Auth::encodeSession($user->getId(), $secret),
expire: (new \DateTime($expire))->getTimestamp(),
path: '/',
domain: Config::getParam('cookieDomain'),
secure: ('https' === $protocol),
httponly: true,
sameSite: Config::getParam('cookieSamesite')
)
;
}
$membership = $dbForProject->updateDocument('memberships', $membership->getId(), $membership);
@ -1125,22 +1151,11 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
->setParam('membershipId', $membership->getId())
;
if (!Config::getParam('domainVerification')) {
$response
->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]))
;
}
$response
->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
;
$response->dynamic(
$membership
->setAttribute('teamName', $team->getAttribute('name'))
->setAttribute('userName', $user->getAttribute('name'))
->setAttribute('userEmail', $user->getAttribute('email')),
->setAttribute('teamName', $team->getAttribute('name'))
->setAttribute('userName', $user->getAttribute('name'))
->setAttribute('userEmail', $user->getAttribute('email')),
Response::MODEL_MEMBERSHIP
);
});

View file

@ -559,6 +559,76 @@ trait TeamsBaseClient
return $data;
}
/**
* @depends testCreateTeam
*/
public function testUpdateMembershipWithSession(array $data): void
{
$teamUid = $data['teamUid'] ?? '';
// create user
$response = $this->client->call(Client::METHOD_POST, '/account', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'userId' => 'unique()',
'email' => uniqid() . 'foe@localhost.test',
'password' => 'password',
'name' => 'test'
]);
$this->assertEquals(201, $response['headers']['status-code']);
$user = $response['body'];
// create session
$response = $this->client->call(Client::METHOD_POST, '/account/sessions', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'email' => $user['email'],
'password' => 'password'
]);
$this->assertEquals(201, $response['headers']['status-code']);
$session = $response['cookies']['a_session_' . $this->getProject()['$id']];
$response = $this->client->call(Client::METHOD_POST, '/teams/' . $teamUid . '/memberships', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'email' => $user['email'],
'roles' => ['developer'],
'url' => 'http://localhost:5000/join-us#title'
]);
$this->assertEquals(201, $response['headers']['status-code']);
$lastEmail = $this->getLastEmail();
$secret = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256);
$membershipUid = substr($lastEmail['text'], strpos($lastEmail['text'], '?membershipId=', 0) + 14, 20);
$userUid = substr($lastEmail['text'], strpos($lastEmail['text'], '&userId=', 0) + 8, 20);
$response = $this->client->call(Client::METHOD_PATCH, '/teams/' . $teamUid . '/memberships/' . $membershipUid . '/status', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
], [
'secret' => $secret,
'userId' => $userUid,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertNotEmpty($response['body']['userId']);
$this->assertNotEmpty($response['body']['teamId']);
$this->assertCount(1, $response['body']['roles']);
$this->assertEmpty($response['cookies']);
}
/**
* @depends testUpdateTeamMembership
*/
@ -648,7 +718,7 @@ trait TeamsBaseClient
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(3, $response['body']['total']);
$this->assertEquals(4, $response['body']['total']);
$ownerMembershipUid = $response['body']['memberships'][0]['$id'];
@ -703,7 +773,7 @@ trait TeamsBaseClient
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(2, $response['body']['total']);
$this->assertEquals(3, $response['body']['total']);
/**
* Test for when the owner tries to delete their membership