Skip to content

Commit

Permalink
merge last_activity and last_check updates
Browse files Browse the repository at this point in the history
the debounce for updating last_activity is changed so it always updates if another field of the token has been updated, this ensures that last_check if updated even if last_activity is still in the debounce period.

Signed-off-by: Robin Appelman <[email protected]>
  • Loading branch information
icewind1991 committed Jun 21, 2024
1 parent f56bca3 commit 568c4b4
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 29 deletions.
26 changes: 15 additions & 11 deletions lib/private/Authentication/Token/PublicKeyTokenMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ public function hasExpiredTokens(string $uid): bool {
}

/**
* Update the last activity timestamp
* Update the last activity timestamp and save all saved fields
*
* In highly concurrent setups it can happen that two parallel processes
* trigger the update at (nearly) the same time. In that special case it's
Expand All @@ -192,7 +192,7 @@ public function hasExpiredTokens(string $uid): bool {
*
* Example:
* - process 1 (P1) reads the token at timestamp 1500
* - process 1 (P2) reads the token at timestamp 1501
* - process 2 (P2) reads the token at timestamp 1501
* - activity update interval is 100
*
* This means
Expand All @@ -206,17 +206,21 @@ public function hasExpiredTokens(string $uid): bool {
* but the comparison on last_activity will still not be truthy and the
* token row is not updated a second time
*
* @param IToken $token
* @param PublicKeyToken $token
* @param int $now
*/
public function updateActivity(IToken $token, int $now): void {
$qb = $this->db->getQueryBuilder();
$update = $qb->update($this->getTableName())
->set('last_activity', $qb->createNamedParameter($now, IQueryBuilder::PARAM_INT))
->where(
$qb->expr()->eq('id', $qb->createNamedParameter($token->getId(), IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT),
$qb->expr()->lt('last_activity', $qb->createNamedParameter($now - 15, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)
);
public function updateActivity(PublicKeyToken $token, int $now): void {
$token->setLastActivity($now);
$update = $this->createUpdateQuery($token);

$updatedFields = $token->getUpdatedFields();
unset($updatedFields['lastActivity']);

// if no other fields are updated, we add the extra filter to prevent duplicate updates
if (count($updatedFields) === 0) {
$update->andWhere($update->expr()->lt('last_activity', $update->createNamedParameter($now - 15, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
}

$update->executeStatement();
}

Expand Down
6 changes: 4 additions & 2 deletions lib/private/Authentication/Token/PublicKeyTokenProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -299,10 +299,12 @@ public function updateTokenActivity(OCPIToken $token) {
$activityInterval = $this->config->getSystemValueInt('token_auth_activity_update', 60);
$activityInterval = min(max($activityInterval, 0), 300);

$updatedFields = $token->getUpdatedFields();
unset($updatedFields['lastActivity']);

/** @var PublicKeyToken $token */
$now = $this->time->getTime();
if ($token->getLastActivity() < ($now - $activityInterval)) {
$token->setLastActivity($now);
if ($token->getLastActivity() < ($now - $activityInterval) || count($updatedFields)) {
$this->mapper->updateActivity($token, $now);
$this->cacheToken($token);
}
Expand Down
1 change: 0 additions & 1 deletion lib/private/User/Session.php
Original file line number Diff line number Diff line change
Expand Up @@ -757,7 +757,6 @@ private function checkTokenCredentials(IToken $dbToken, $token) {
if ($dbToken instanceof PublicKeyToken) {
$dbToken->setLastActivity($now);
}
$this->tokenProvider->updateToken($dbToken);
return true;
}

Expand Down
42 changes: 29 additions & 13 deletions lib/public/AppFramework/Db/QBMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,22 +146,15 @@ public function insertOrUpdate(Entity $entity): Entity {
}

/**
* Updates an entry in the db from an entity
* Create an update query that saves all updated fields
*
* @param Entity $entity the entity that should be created
* @psalm-param T $entity the entity that should be created
* @return Entity the saved entity with the set id
* @psalm-return T the saved entity with the set id
* @throws Exception
* @throws \InvalidArgumentException if entity has no id
* @since 14.0.0
* @param Entity $entity the entity that should be updated
* @psalm-param T $entity the entity that should be updated
* @return IQueryBuilder
* @since 25.0.0
*/
public function update(Entity $entity): Entity {
// if entity wasn't changed it makes no sense to run a db query
protected function createUpdateQuery(Entity $entity): IQueryBuilder {
$properties = $entity->getUpdatedFields();
if (\count($properties) === 0) {
return $entity;
}

// entity needs an id
$id = $entity->getId();
Expand Down Expand Up @@ -193,6 +186,29 @@ public function update(Entity $entity): Entity {
$qb->where(
$qb->expr()->eq('id', $qb->createNamedParameter($id, $idType))
);

return $qb;
}

/**
* Updates an entry in the db from an entity
*
* @param Entity $entity the entity that should be created
* @psalm-param T $entity the entity that should be created
* @return Entity the saved entity with the set id
* @psalm-return T the saved entity with the set id
* @throws Exception
* @throws \InvalidArgumentException if entity has no id
* @since 14.0.0
*/
public function update(Entity $entity): Entity {
// if entity wasn't changed it makes no sense to run a db query
$properties = $entity->getUpdatedFields();
if (\count($properties) === 0) {
return $entity;
}

$qb = $this->createUpdateQuery($entity);
$qb->executeStatement();

return $entity;
Expand Down
49 changes: 49 additions & 0 deletions tests/lib/Authentication/Token/PublicKeyTokenMapperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -261,4 +261,53 @@ public function testHasExpiredTokens() {
$this->assertFalse($this->mapper->hasExpiredTokens('user1'));
$this->assertTrue($this->mapper->hasExpiredTokens('user3'));
}

public function testUpdateTokenActivity() {
$token = '6d9a290d239d09f2cc33a03cc54cccd46f7dc71630dcc27d39214824bd3e093f1feb4e2b55eb159d204caa15dee9556c202a5aa0b9d67806c3f4ec2cde11af67';
$dbToken = $this->mapper->getToken($token);

$this->assertEquals($dbToken->getLastActivity(), $this->time - 120);
$this->assertEquals($this->time - 60 * 10, $dbToken->getLastCheck());

$this->mapper->updateActivity($dbToken, $this->time);

$updatedDbToken = $this->mapper->getToken($token);

$this->assertEquals($this->time, $updatedDbToken->getLastActivity());
$this->assertEquals($this->time - 60 * 10, $dbToken->getLastCheck());
$this->assertEquals($this->time, $dbToken->getLastActivity());
}

public function testUpdateTokenActivityDebounce() {
$token = '6d9a290d239d09f2cc33a03cc54cccd46f7dc71630dcc27d39214824bd3e093f1feb4e2b55eb159d204caa15dee9556c202a5aa0b9d67806c3f4ec2cde11af67';
$dbToken = $this->mapper->getToken($token);

$this->assertEquals($dbToken->getLastActivity(), $this->time - 120);
$this->assertEquals($this->time - 60 * 10, $dbToken->getLastCheck());

$this->mapper->updateActivity($dbToken, $this->time - 110);

$updatedDbToken = $this->mapper->getToken($token);

$this->assertEquals($this->time - 120, $updatedDbToken->getLastActivity());
$this->assertEquals($this->time - 60 * 10, $dbToken->getLastCheck());
$this->assertEquals($this->time - 110, $dbToken->getLastActivity());
}

public function testUpdateTokenActivityDebounceUpdate() {
$token = '6d9a290d239d09f2cc33a03cc54cccd46f7dc71630dcc27d39214824bd3e093f1feb4e2b55eb159d204caa15dee9556c202a5aa0b9d67806c3f4ec2cde11af67';
$dbToken = $this->mapper->getToken($token);

$this->assertEquals($this->time - 120, $dbToken->getLastActivity());
$this->assertEquals($this->time - 60 * 10, $dbToken->getLastCheck());

$dbToken->setLastCheck($this->time - 100);
$this->mapper->updateActivity($dbToken, $this->time - 110);

$updatedDbToken = $this->mapper->getToken($token);

$this->assertEquals($this->time - 110, $updatedDbToken->getLastActivity());
$this->assertEquals($this->time - 100, $dbToken->getLastCheck());
$this->assertEquals($this->time - 110, $dbToken->getLastActivity());
}
}
18 changes: 16 additions & 2 deletions tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,6 @@ public function testUpdateToken() {
]);

$this->tokenProvider->updateTokenActivity($tk);

$this->assertEquals($this->time, $tk->getLastActivity());
}

public function testUpdateTokenDebounce() {
Expand All @@ -210,6 +208,22 @@ public function testUpdateTokenDebounce() {
$this->tokenProvider->updateTokenActivity($tk);
}

public function testUpdateTokenDebounceWithUpdatedFields() {
$tk = new PublicKeyToken();
$this->config->method('getSystemValueInt')
->willReturnCallback(function ($value, $default) {
return $default;
});
$tk->setLastActivity($this->time - 30);
$tk->setLastCheck($this->time - 30);

$this->mapper->expects($this->once())
->method('updateActivity')
->with($tk, $this->time);

$this->tokenProvider->updateTokenActivity($tk);
}

public function testGetTokenByUser() {
$this->mapper->expects($this->once())
->method('getTokenByUser')
Expand Down

0 comments on commit 568c4b4

Please sign in to comment.