feat: API token management in workspace settings#10624
Conversation
|
Connected to Huly®: UBERF-15850 |
c84d786 to
efdbe1b
Compare
Follow-up: REST API should apply sensible defaults for
|
c7d2bf2 to
aaa8348
Compare
|
Hi @dnplkndll |
8663ec8 to
865fd71
Compare
|
@ArtyomSavchenko Thanks for the review! Formatting has been fixed — the issue was a prettier version mismatch (local 3.8.1 vs project's 3.6.2). New code now matches the existing codebase style with no formatting noise on existing lines. Changes in this update3 commits:
What's new since last push
Suggestions for future consideration
|
c39720c to
058c7da
Compare
|
|
||
| import { AccountRole, type MeasureContext, type PersonUuid, type WorkspaceUuid } from '@hcengineering/core' | ||
| import platform, { PlatformError, Severity, Status } from '@hcengineering/platform' | ||
| import { decodeTokenVerbose, generateToken } from '@hcengineering/server-token' |
There was a problem hiding this comment.
Could you please check warnings in this file?
CI formatting step is failed due to this:
https://github.com/hcengineering/platform/actions/runs/23317689234/job/67851578625?pr=10624
|
Hi @dnplkndll |
|
sure what is your opinion on the use of MCP versus api? My motivation was that I would prefer to have tool access in a controlled predicable / scriptable way with permissions on things. then you can test on a staging build out script you can repeat. |
f8cf105 to
4f332bb
Compare
ebdcb02 to
370a096
Compare
| "ApiTokenScopePreset": "Permissions", | ||
| "ApiTokenScopeReadOnly": "Read Only", | ||
| "ApiTokenScopeReadWrite": "Read & Write", | ||
| "ApiTokenScopeFullAccess": "Full Access" |
There was a problem hiding this comment.
Could you please check translations in this file and others?
Looks like many sentences were not translated and remained in English.
| return window.location.origin | ||
| } | ||
|
|
||
| $: baseApiUrl = getBaseUrl() + '/_transactor/api/v1' |
There was a problem hiding this comment.
That's not correct, transactor endpoint address cannot be built in this way, it is returned by account service when user is authenticated.
|
|
||
| async function resolveScopeLabels(): Promise<void> { | ||
| const lang = $themeStore.language | ||
| const labels = await Promise.all([ |
There was a problem hiding this comment.
There is no need to manually translate strings, we have dropdown version that uses i18n strings
| // Re-check if stale or missing | ||
| if (cached == null || now - cached.checkedAt > REVOCATION_CACHE_TTL_MS) { | ||
| try { | ||
| const revoked = await accountClient.checkApiTokenRevoked(apiTokenId) |
There was a problem hiding this comment.
This does not make sense.
- User comes with revokable token that contains token Id (that is how we identify revokable token).
- We use this token (that is potentially revoked or expired at the moment) to check whether the token is revoked
If the token is revoked, the account cannot reply anything other than 401 or 403, because the token is revoked/expired.
The account method, that returns boolean is redundant. We can use selectWorkspace or getWorkspaceInfo for this.
Additionally, I would implement token expiration / revocation logic and lower level. Maybe at the same level as decodeToken, for example we can add a new verifyToken method next to the decodeToken, that:
- Decodes token
- Checks whether the token is expired
- If the token is revokable, checks whether token is expired
- Returns decoded token
This will allow reuse token expiration / revocation logic in services to prevent accessing blobs, or other data with expired tokens.
But in order to do this, we will have to additionally set up some metadata in the server-token plugin (either address of the account endpoint, or method to verify whether the token is expired).
|
Thanks for the contribution We also have a button to generate token on workspace settings page, we will need to remove it in favor of the new approach. |
370a096 to
cf87e38
Compare
e0e0743 to
a3a8019
Compare
Add UI and backend support for creating, listing, and revoking API tokens scoped to workspaces. Includes owner-level workspace token visibility, OpenAPI documentation, Mongo/Postgres persistence, and i18n translations. Signed-off-by: Don Kendall <kendall@donkendall.com>
Embed apiTokenId in JWT extra field and add a per-token revocation cache (60s TTL) in the transactor REST handler. Revoked tokens are now rejected within ~60 seconds instead of remaining valid until JWT expiry. Adds checkApiTokenRevoked account service method for the transactor to query individual token revocation status. Signed-off-by: Don Kendall <kendall@donkendall.com>
Add coarse-grained scope enforcement for API tokens. Tokens can now be created with scopes ['read:*'], ['read:*','write:*'], or ['read:*','write:*','delete:*']. Existing tokens without scopes retain full access (backward compatible). - DB: v26 migration adds scopes TEXT[] column to api_tokens - Types: add scopes field to ApiToken and ApiTokenInfo - Operations: createApiToken accepts/validates/persists scopes, embeds in JWT via extra.scopes - Enforcement: withSession checks scopes against method; tx handler additionally requires delete:* for TxRemoveDoc - Client: createApiToken signature accepts optional scopes param - UI: scope preset dropdown in create popup (default: Read Only), permissions column in token list with i18n labels - Also fixes 3 pre-existing TS2322/TS2345 errors in operations.ts Signed-off-by: Don Kendall <kendall@donkendall.com>
- scopes.test.ts: 8 tests for hasScope() and getRequiredScope() logic - apiTokenScopes.test.ts: 7 tests for createApiToken scope validation (valid scopes, multiple scopes, no scopes backward compat, invalid format rejection, empty array rejection, domain-scope rejection) and listApiTokens scopes inclusion - Export hasScope/getRequiredScope from rpc.ts for testability Signed-off-by: Don Kendall <kendall@donkendall.com>
…tting - Restrict API token creation/revocation to AccountRole.User or higher (guests cannot use API tokens), per reviewer suggestion - Add 5 missing translation keys (ApiTokenPermissions, ApiTokenScopePreset, ApiTokenScopeReadOnly, ApiTokenScopeReadWrite, ApiTokenScopeFullAccess) to all non-en locale files to fix locale parity CI test - Fix prettier formatting in apiTokenScopes.test.ts - Rename local `extra` to `tokenExtra` in createApiToken to avoid shadowing the decoded token's `extra` field Signed-off-by: Don Kendall <kendall@donkendall.com>
- rpc.ts: use system service token for checkApiTokenRevoked so the revocation check is not coupled to the user's potentially-revoked bearer token; systemAccountUuid + service:'server' ensures account service always accepts the call - ApiDocsSection.svelte: derive transactor base URL from login.metadata.LoginEndpoint (set on auth) instead of window.location.origin, which is not necessarily the transactor host - ApiTokenCreatePopup.svelte: replace manual translate() calls and themeStore language watch with DropdownLabelsIntl + DropdownIntlItem[], which handle i18n automatically; error state is now IntlString - General.svelte: remove legacy GenerateApiToken button, handler, and ApiTokenPopup import in favour of the new ApiTokens settings panel Signed-off-by: Don Kendall <kendall@donkendall.com>
Signed-off-by: Don Kendall <kendall@donkendall.com>
Signed-off-by: Don Kendall <dkendall@ledoweb.com>
Signed-off-by: Don Kendall <dkendall@ledoweb.com>
a3a8019 to
dfa360b
Compare
The feat/* branches got rebased onto upstream/develop to clear the DCO + merge-conflict blockers on PRs hcengineering#10705 and hcengineering#10624. That puts commits on those branches that depend on files only present in develop > v0.7.423, so the prior 'merge feat/* onto v0.7.423 tag' flow now conflicts. Three changes: 1. ledoent/upstream-tag.txt → develop tip (fa2930d). The file now carries any ref — tag, branch, or SHA. We pin to a SHA so each aggregate run is reproducible; bump it when we want to follow develop forward. 2. gitaggregate.yml — resolve the ref through 'refs/tags/<ref>' first, then 'upstream/<ref>', falling back to the raw value. Output and variable renamed upstream_tag → upstream_ref to match the new semantics. 3. common/scripts/show_tag.js — read common/scripts/version.txt before trying 'git describe --tags'. Develop has no tag in its ancestry so the previous git-describe-only path printed the hardcoded "0.6.0" fallback, which then showed up next to the Help & Support button. Matches the pattern already used by show_version.js. Signed-off-by: Don Kendall <dkendall@ledoweb.com>
The feat/* branches got rebased onto upstream/develop to clear the DCO + merge-conflict blockers on PRs hcengineering#10705 and hcengineering#10624. That puts commits on those branches that depend on files only present in develop > v0.7.423, so the prior 'merge feat/* onto v0.7.423 tag' flow now conflicts. Three changes: 1. ledoent/upstream-tag.txt → develop tip (fa2930d). The file now carries any ref — tag, branch, or SHA. We pin to a SHA so each aggregate run is reproducible; bump it when we want to follow develop forward. 2. gitaggregate.yml — resolve the ref through 'refs/tags/<ref>' first, then 'upstream/<ref>', falling back to the raw value. Output and variable renamed upstream_tag → upstream_ref to match the new semantics. 3. common/scripts/show_tag.js — read common/scripts/version.txt before trying 'git describe --tags'. Develop has no tag in its ancestry so the previous git-describe-only path printed the hardcoded "0.6.0" fallback, which then showed up next to the Help & Support button. Matches the pattern already used by show_version.js. Signed-off-by: Don Kendall <dkendall@ledoweb.com>
Resolve conflicts: - migrations: keep develop's V26 (pending_configuration); api_tokens table (with scopes column) becomes V27 - lang files: union of develop's keys and the API token strings Signed-off-by: Don Kendall <dkendall@ledoweb.com>
Address @aonnikov's review: the bespoke checkApiTokenRevoked RPC and the ad-hoc revocation cache in the transactor are replaced by a reusable verifyToken in server-token that checks signature, expiry, and (for revokable API tokens) revocation via a pluggable checker. - server-token: add verifyToken + isTokenExpired + setApiTokenRevocationChecker (the 'method to verify' metadata the plugin needs, without depending on the account client). Revocation cache (60s TTL) now lives here, reusable by any service (transactor, blob access, etc.). - account: the account is now authoritative — wrap() rejects revoked/expired API tokens, so any account method (selectWorkspace/getWorkspaceInfo/...) naturally 401s. Removed the redundant checkApiTokenRevoked method. - account-client: drop checkApiTokenRevoked. - transactor: withSession uses verifyToken; the registered checker reuses the existing getLoginInfoByToken instead of a dedicated boolean RPC. Scopes are parsed once and threaded through (no re-decode in the tx handler). - tests: verifyToken/isTokenExpired unit coverage. Signed-off-by: Don Kendall <dkendall@ledoweb.com>
…ndpoint Address @aonnikov: the REST API host must come from the transactor endpoint returned by the account service (the login endpoint, a ws(s):// URL), not be constructed from window.location. Convert ws->http and append /api/v1, matching the existing ServerManagerGeneral pattern. Signed-off-by: Don Kendall <dkendall@ledoweb.com>
Address @ArtyomSavchenko: the new API token UI strings were left in English in non-en locales. Provide translations for ru/de/es/fr/it/pt/pt-br/zh/ja/cs/tr. The en/ru locale-parity test passes (the prior failure was an en/ru key mismatch, resolved by the develop merge). Signed-off-by: Don Kendall <dkendall@ledoweb.com>
…DocsSection) Signed-off-by: Don Kendall <dkendall@ledoweb.com>
|
Thanks for the reviews — pushed an update addressing the feedback. Conflicts with
Unit tests added for |
|
Hi @dnplkndll |
Summary
Workspace-scoped API tokens with a built-in REST API, managed from workspace settings. Tokens are JWTs with configurable expiry (7–365 days) and optional coarse scopes. Expiry and revocation are enforced centrally so the policy is reusable across services.
What changed
API token management (
server/account/)ApiTokentype +apiTokencollection (Mongo + Postgres). Migration V27 createsapi_tokens(including thescopes TEXT[]column).createApiToken,listApiTokens,revokeApiToken, plus owner-scopedlistWorkspaceApiTokens/revokeWorkspaceApiToken.AccountRole.Useror higher (not guests); per-account token cap.Centralized token verification (
server-token) — addresses @aonnikov's reviewverifyToken()alongsidedecodeToken(): verifies signature, checks expiry, and — for revokable API tokens — revocation via a pluggable checker (setApiTokenRevocationChecker). Reusable by any service (transactor, blob access, …); the 60s revocation cache now lives here.wrap()rejects revoked/expired API tokens, so existing methods (selectWorkspace,getWorkspaceInfo, …) naturally return 401. The redundantcheckApiTokenRevokedRPC was removed; the transactor's checker reuses the existinggetLoginInfoByToken.Scope enforcement (
pods/server/src/rpc.ts) — Phase 1read:*/write:*/delete:*, enforced per method. Scopes are parsed once per request and threaded through (no re-decode in thetxhandler). No scopes = full access (legacy-compatible).Frontend (
plugins/setting-resources/)ApiTokens.sveltesettings page;ApiTokenCreatePopup.svelte(i18n dropdowns for scope preset + expiry);ApiDocsSection.svelte.window.location.i18n
Docs
openapi.yamldescribing the REST API.Test plan
verifyToken(signature/expiry/revocation), scope helpers, scope enforcement, token CRUD.find-allworks,txreturns 403.txworks,TxRemoveDocreturns 403; Full Access → all ops work.Ref: #10622