From 2b6787051993befb0789278d4f1d44b0fcc7126a Mon Sep 17 00:00:00 2001 From: Sein Coray Date: Mon, 10 Aug 2026 16:44:40 +0200 Subject: [PATCH] adding tokenName for api keys to allow setting a name --- ci/apiv2/utils.py | 2 ++ ci/phpunit/TestBase.php | 4 ++-- ci/phpunit/inc/utils/JwtTokenUtilsTest.php | 5 +++-- src/dba/models/JwtApiKey.php | 15 ++++++++++++++- src/dba/models/JwtApiKeyFactory.php | 4 ++-- src/dba/models/generator.php | 1 + src/inc/apiv2/model/ApiTokenAPI.php | 3 ++- src/inc/utils/JwtTokenUtils.php | 5 +++-- .../mysql/20260810120000_jwtapikey-tokenname.sql | 2 ++ .../20260810120000_jwtapikey-tokenname.sql | 2 ++ 10 files changed, 33 insertions(+), 10 deletions(-) create mode 100644 src/migrations/mysql/20260810120000_jwtapikey-tokenname.sql create mode 100644 src/migrations/postgres/20260810120000_jwtapikey-tokenname.sql diff --git a/ci/apiv2/utils.py b/ci/apiv2/utils.py index 1b05aba29..e584996db 100644 --- a/ci/apiv2/utils.py +++ b/ci/apiv2/utils.py @@ -123,6 +123,7 @@ def do_create_apitoken(extra_payload={}, **kwargs): extra_payload = dict(extra_payload or {}) extra_payload.setdefault('startValid', now) extra_payload.setdefault('endValid', now + 3600) + extra_payload.setdefault('tokenName', 'pytest-token') return _do_create_obj_from_file(ApiToken, 'create_apitoken', extra_payload, **kwargs) @@ -139,6 +140,7 @@ def create_apitoken_raw(test, auth, scopes): 'scopes': scopes, 'startValid': now, 'endValid': now + 3600, + 'tokenName': 'pytest-token', }, 'type': 'ApiToken', }, diff --git a/ci/phpunit/TestBase.php b/ci/phpunit/TestBase.php index 1669b008b..8c96c267a 100644 --- a/ci/phpunit/TestBase.php +++ b/ci/phpunit/TestBase.php @@ -234,10 +234,10 @@ protected function createTask(TaskWrapper $taskWrapper, CrackerBinary $crackerBi /** * @throws Exception */ - protected function createJwtApiKey(User $user, ?int $startValid = null, ?int $endValid = null, int $isRevoked = 0): JwtApiKey { + protected function createJwtApiKey(User $user, ?int $startValid = null, ?int $endValid = null, int $isRevoked = 0, string $tokenName = "test-token"): JwtApiKey { $key = $this->createDatabaseObject( Factory::getJwtApiKeyFactory(), - new JwtApiKey(null, $startValid ?? time(), $endValid ?? time() + 3600, $user->getId(), $isRevoked) + new JwtApiKey(null, $startValid ?? time(), $endValid ?? time() + 3600, $user->getId(), $isRevoked, $tokenName) ); $this->assertTrue($key instanceof JwtApiKey); return $key; diff --git a/ci/phpunit/inc/utils/JwtTokenUtilsTest.php b/ci/phpunit/inc/utils/JwtTokenUtilsTest.php index cdd0201bc..3d063df81 100644 --- a/ci/phpunit/inc/utils/JwtTokenUtilsTest.php +++ b/ci/phpunit/inc/utils/JwtTokenUtilsTest.php @@ -26,19 +26,20 @@ public function testCreateKeyCreatesValidKey(): void { $start = time(); $end = $start + 3600; - $key = JwtTokenUtils::createKey($this->user->getId(), $start, $end); + $key = JwtTokenUtils::createKey($this->user->getId(), $start, $end, 'test-token'); $this->assertInstanceOf(JwtApiKey::class, $key); $this->assertSame($start, $key->getStartValid()); $this->assertSame($end, $key->getEndValid()); $this->assertSame($this->user->getId(), $key->getUserId()); + $this->assertSame('test-token', $key->getTokenName()); $this->assertNotNull($key->getId()); $this->registerDatabaseObject(Factory::getJwtApiKeyFactory(), $key); } public function testCreateKeyThrowsForInvalidUser(): void { $this->expectException(HttpError::class); - JwtTokenUtils::createKey(-1, time(), time() + 3600); + JwtTokenUtils::createKey(-1, time(), time() + 3600, 'test-token'); } public function testDeleteKeyDeletesExpiredKey(): void { diff --git a/src/dba/models/JwtApiKey.php b/src/dba/models/JwtApiKey.php index 3beac6a98..339f4328b 100644 --- a/src/dba/models/JwtApiKey.php +++ b/src/dba/models/JwtApiKey.php @@ -10,13 +10,15 @@ class JwtApiKey extends AbstractModel { private ?int $endValid; private ?int $userId; private ?int $isRevoked; + private ?string $tokenName; - function __construct(?int $jwtApiKeyId, ?int $startValid, ?int $endValid, ?int $userId, ?int $isRevoked) { + function __construct(?int $jwtApiKeyId, ?int $startValid, ?int $endValid, ?int $userId, ?int $isRevoked, ?string $tokenName) { $this->jwtApiKeyId = $jwtApiKeyId; $this->startValid = $startValid; $this->endValid = $endValid; $this->userId = $userId; $this->isRevoked = $isRevoked; + $this->tokenName = $tokenName; } function getKeyValueDict(): array { @@ -26,6 +28,7 @@ function getKeyValueDict(): array { $dict['endValid'] = $this->endValid; $dict['userId'] = $this->userId; $dict['isRevoked'] = $this->isRevoked; + $dict['tokenName'] = $this->tokenName; return $dict; } @@ -37,6 +40,7 @@ static function getFeatures(): array { $dict['endValid'] = ['read_only' => True, "type" => "int64", "subtype" => "unset", "choices" => "unset", "null" => False, "pk" => False, "protected" => False, "private" => False, "alias" => "endValid", "public" => False, "dba_mapping" => False]; $dict['userId'] = ['read_only' => True, "type" => "int", "subtype" => "unset", "choices" => "unset", "null" => True, "pk" => False, "protected" => False, "private" => False, "alias" => "userId", "public" => False, "dba_mapping" => False]; $dict['isRevoked'] = ['read_only' => False, "type" => "bool", "subtype" => "unset", "choices" => "unset", "null" => True, "pk" => False, "protected" => False, "private" => False, "alias" => "isRevoked", "public" => False, "dba_mapping" => False]; + $dict['tokenName'] = ['read_only' => False, "type" => "str(100)", "subtype" => "unset", "choices" => "unset", "null" => False, "pk" => False, "protected" => False, "private" => False, "alias" => "tokenName", "public" => False, "dba_mapping" => False]; return $dict; } @@ -97,11 +101,20 @@ function setIsRevoked(?int $isRevoked): void { $this->isRevoked = $isRevoked; } + function getTokenName(): ?string { + return $this->tokenName; + } + + function setTokenName(?string $tokenName): void { + $this->tokenName = $tokenName; + } + const JWT_API_KEY_ID = "jwtApiKeyId"; const START_VALID = "startValid"; const END_VALID = "endValid"; const USER_ID = "userId"; const IS_REVOKED = "isRevoked"; + const TOKEN_NAME = "tokenName"; const PERM_CREATE = "permJwtApiKeyCreate"; const PERM_READ = "permJwtApiKeyRead"; diff --git a/src/dba/models/JwtApiKeyFactory.php b/src/dba/models/JwtApiKeyFactory.php index 0cba3e2c3..c84ae7a4c 100644 --- a/src/dba/models/JwtApiKeyFactory.php +++ b/src/dba/models/JwtApiKeyFactory.php @@ -32,7 +32,7 @@ function getCacheValidTime(): int { * @return JwtApiKey */ function getNullObject(): JwtApiKey { - return new JwtApiKey(-1, null, null, null, null); + return new JwtApiKey(-1, null, null, null, null, null); } /** @@ -45,6 +45,6 @@ function createObjectFromDict(array $dict): JwtApiKey { $conv[strtolower($key)] = $val; } $dict = $conv; - return new JwtApiKey($dict['jwtapikeyid'], $dict['startvalid'], $dict['endvalid'], $dict['userid'], $dict['isrevoked']); + return new JwtApiKey($dict['jwtapikeyid'], $dict['startvalid'], $dict['endvalid'], $dict['userid'], $dict['isrevoked'], $dict['tokenname']); } } diff --git a/src/dba/models/generator.php b/src/dba/models/generator.php index 84397c810..0e794d01e 100644 --- a/src/dba/models/generator.php +++ b/src/dba/models/generator.php @@ -294,6 +294,7 @@ ['name' => 'endValid', 'read_only' => True, 'type' => 'int64'], ['name' => 'userId', 'read_only' => True, 'null' => True, 'type' => 'int', 'relation' => 'User'], ['name' => 'isRevoked', 'read_only' => False, 'null' => True, 'type' => 'bool'], + ['name' => 'tokenName', 'read_only' => False, 'type' => 'str(100)'], ], ]; $CONF['LogEntry'] = [ diff --git a/src/inc/apiv2/model/ApiTokenAPI.php b/src/inc/apiv2/model/ApiTokenAPI.php index 046dc8981..989e0810d 100644 --- a/src/inc/apiv2/model/ApiTokenAPI.php +++ b/src/inc/apiv2/model/ApiTokenAPI.php @@ -96,7 +96,8 @@ protected function createObject(array $data): int { $secret = StartupConfig::getInstance()->getPepper(0); $iat = $data[JwtApiKey::START_VALID]; $expires = $data[JwtApiKey::END_VALID]; - $token = JwtTokenUtils::createKey($this->getCurrentUser()->getId(), $iat, $expires); + $tokenName = $data[JwtApiKey::TOKEN_NAME]; + $token = JwtTokenUtils::createKey($this->getCurrentUser()->getId(), $iat, $expires, $tokenName); $jti = $token->getId(); $payload = [ diff --git a/src/inc/utils/JwtTokenUtils.php b/src/inc/utils/JwtTokenUtils.php index a5aa4fbad..ba85f3454 100644 --- a/src/inc/utils/JwtTokenUtils.php +++ b/src/inc/utils/JwtTokenUtils.php @@ -14,17 +14,18 @@ class JwtTokenUtils { * @param int $userId * @param int $startValid * @param int $endValid + * @param string $tokenName * @return JwtApiKey * @throws HttpError * @throws Exception */ - public static function createKey(int $userId, int $startValid, int $endValid): JwtApiKey { + public static function createKey(int $userId, int $startValid, int $endValid, string $tokenName): JwtApiKey { $user = Factory::getUserFactory()->get($userId); if ($user == null) { throw new HttpError("Invalid user ID"); } - $key = new JwtApiKey(null, $startValid, $endValid, $userId, 0); + $key = new JwtApiKey(null, $startValid, $endValid, $userId, 0, $tokenName); Factory::getJwtApiKeyFactory()->save($key); return $key; } diff --git a/src/migrations/mysql/20260810120000_jwtapikey-tokenname.sql b/src/migrations/mysql/20260810120000_jwtapikey-tokenname.sql new file mode 100644 index 000000000..1f21f3ddb --- /dev/null +++ b/src/migrations/mysql/20260810120000_jwtapikey-tokenname.sql @@ -0,0 +1,2 @@ +-- Add tokenName column to JwtApiKey to allow naming individual API tokens. +ALTER TABLE `JwtApiKey` ADD COLUMN `tokenName` VARCHAR(100) NOT NULL DEFAULT ''; diff --git a/src/migrations/postgres/20260810120000_jwtapikey-tokenname.sql b/src/migrations/postgres/20260810120000_jwtapikey-tokenname.sql new file mode 100644 index 000000000..c9143bffe --- /dev/null +++ b/src/migrations/postgres/20260810120000_jwtapikey-tokenname.sql @@ -0,0 +1,2 @@ +-- Add tokenName column to jwtapikey to allow naming individual API tokens. +ALTER TABLE jwtapikey ADD COLUMN tokenname VARCHAR(100) NOT NULL DEFAULT '';