Repository navigation
feat(api-keys): give API keys custom roles, as teams have them - #5211
Merged
Merged
Conversation
…mRole, copies kept by CustomRoleSync An API key can be given a project's custom roles, as a team can. The key holds a copy of each of the role's permissions as an ordinary ApiKeyPermission row marked with the role (customRoleId / apiKeyCustomRoleId / customRolePermissionId), kept equal to the role by the same CustomRoleSync that keeps teams' copies - so the API key auth path reads them with no change. A permission the role limits to what its holder owns gives a key nothing (a key owns nothing), so no copy of it is made. - ApiKeyCustomRole model (+ migration and the online-DDL links migration). - ApiKeyCustomRoleService: key and role in the project, the giver must read the role, and every permission the key would hold is held to the key grant ceiling. - ApiKeyPermissionService: copies are changed only through their role; own-row uniqueness; cached key permissions are now kept per project generation (CacheGenerations), so a change reaches every server within a second. - CustomRole permission counts are readable through a relation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ooks, copies, cache generations, ceilings Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… wired into the drift workflow ApiKeyCustomRolesPostgres runs the real services, sync, key permission cache and permission layer: a role gives a key what the same permissions given directly give, Owned gives a key nothing, every change reaches the key's next request and starts a new generation, refusals, cascades. The clone suites whose code runs a role's sync now clone ApiKeyCustomRole too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e and Create API Key - An API key's page lists its custom roles above its own permissions, with Add Custom Role and Remove, and says how many of a role's permissions apply to the key when some are limited to what someone owns. - A role's page lists the API keys given it (Give to an API Key, Remove), and its reach and delete sentences count the keys. - Create API Key offers the custom roles the user could give under Access. - The key's own permissions table no longer lists a role's copies. - Delete consequences, the Roles page and the docs (17 languages) say roles go to teams and API keys; the audit log page lists API Key Custom Role. - Every new string in the 16 Dashboard locales. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ate API Key - ApiKeyViewCustomRoles: the key's Custom Roles card - hidden in a project with no role, each role a link with how many of its permissions apply to the key, Add Custom Role offering only roles the key lacks and sending one ApiKeyCustomRole, off (with the reason) when the key has every role or the reader may not change keys, Remove's wording, and the key's own Permissions card never listing a role's copies. - RoleViewApiKeys: the role's API Keys card, the reach sentence and the Delete dialog counting keys, Give to an API Key offering only keys without the role, Remove's wording, and nothing about keys for someone who may not read them. - ApiKeysCreateAccess: custom roles under Access - a role giving a key nothing is not offered, owned-scope permissions are not weighed, a picked role is given once the key exists, a refusal leaves the key. - CustomRoleRelationSelect: the cards' relation selects (permissionCount on TeamCustomRole and ApiKeyCustomRole, apiKey name) pass the relation read check; unflagged columns are still refused. - RoleAccess, CustomRoleOptions, CustomRoleCopy, GrantablePermission: the key helpers and copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…390 pixels The custom roles fixture now also mounts the real SettingsRoutes, with two keys: Deploy bot (Team Lead and Incident Helper, Workflow Viewer of its own) and Terraform (no role). ApiKeyCustomRoles.spec.ts drives a key's Custom Roles card, Add Custom Role and Remove, the role's API Keys card with Give to an API Key, the reach and Delete counting keys, Create API Key with a custom role, a reader who may not read keys, a phone with long names, and dark mode. CustomRoles.spec.ts follows the new Roles card and delete wording, and a role no team or key has. The custom roles suite takes 3.5 minutes with its bundle locally (70 specs); the CI group note says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The key's Custom Roles card draws the menu's word for Roles without a hard-coded English fallback, as the A-to-L hard-coded string guard asks. - TeamCreateAccessGuard pins the API Keys page as it pins the Teams page: the custom roles after the built-in ones, and a custom role given through the helper Add Custom Role uses. - The Postgres suite orders a key's own row before a role's copy of the same permission, which random ids used to order by chance. - Test type fixes the Common compile-tests job asks for, and Prettier on the files this branch added. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ece of their path Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…om-roles Master took 1806300000000 (AddOwnerRuleCreatorTeams) and 1806400000000 (WidenDetectionRuleName), so this branch's migrations move after them: AddApiKeyCustomRoles1806500000000 and AddApiKeyPermissionCustomRoleLinks1806510000000. The Dashboard locales are master's, with this branch's 34 strings extracted again and their translations applied in all 16 languages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d the Terraform provider The real OpenAPI spec and provider parser: /api-key-custom-role creates, lists, reads and deletes a grant naming a key and a role, never changes one in place; oneuptime_api_key_custom_role replaces a grant whose key or role changes; a key's copies of a role's permissions are read and never written, in the API and in oneuptime_api_key_permission. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
…e's keys get UI review of custom roles on API keys. - A key's Custom Roles card: "2 of its 3 permissions apply to this key" now has an (i) that says why, in the words the role's Scope column uses: Permissions scoped to "Only resources they own" don't apply to an API key: a key owns nothing. Add Custom Role names that exception only when a role it offers has such a permission. - A role's API Keys card said "These API keys can do everything this role allows" even for a role with an owned-only permission. It now says how many of the role's permissions the keys get, or that none apply, and follows a change of the role's permissions. Give to an API Key says the exception only when the role has such a permission. - Give to an API Key is off, with the reason, when every key has the role or the project has no key yet; the empty card then links to the API Keys page instead of offering a dialog with an empty "No options." list. - Phone cards of the one-column role, key and team tables leave out the column's title over every card (hideTitleOnMobile, now typed on ModelTable columns too). - The ApiKeyCustomRole table description (API reference, Terraform) says the Owned exception. - 13 new strings in 17 locales; the three they replace are gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 17 specs The custom roles offline suite now takes 4.1 minutes with its bundle locally, about 1.2 more than on master, after the specs for custom roles on API keys. Group 7 took 21.5 minutes on master's last run without them, so it would run past the 22 the groups are kept to. Archivable Values (about 1.5 minutes, Chromium only) moves to group 9, which took 18.9 to 19.4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r count pattern in its test DashboardComponentsAToLHardcodedGuard counts the API Keys link's English fallback as a hard-coded string; the role page test's /\d+ API keys?/ retried from every digit of a long number. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lains A screen reader heard "About Permissions that apply"; it now hears "About the permissions that apply to this key", worded in each language for its own About sentence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
simlarsen
pushed a commit
that referenced
this pull request
Oct 23, 2026
Both sides appended keys at the end of the 17 Dashboard locale files (#5211's API key custom roles, and this branch's Overview card): kept both, this branch's first, master's values on any clash; i18n:extract stable.
simlarsen
pushed a commit
that referenced
this pull request
Oct 23, 2026
…type, and workflow steps cannot give one Common Test has failed on master since #5211 merged (it merged without a finished run), in two guards that read every model: - AuditLogModelConfiguration: ApiKeyCustomRole filed its audit entries under the root type "ApiKey", but an API key's own entries are filed under its singular name, "API Key", and no audited model is called "ApiKey". The project audit log looks a root up by that type to link an entry to its page, so a role given to or taken from a key linked nowhere. The pointer now names "API Key", as TeamCustomRole's names "Team". 14.0.87 does not have #5211, so no stored entry has the old type. - WorkflowStepCredentialSettings: its survey pins every record a workflow step can write that names a setting holding credentials, so each new one is looked at. ApiKeyCustomRole.apiKey names an API key, like ApiKeyPermission.apiKey; the existing rule refuses it to a step and lets a person who may read API keys set it. It is pinned now, and the refusal and the person's case run for it too. Both suites pass (146), and so do the custom role suites (235), the audit root backfill (10) and ee's audit log recorder suites (128). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 23, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
API keys can now be given custom roles, the way teams are. The maintainer asked: "We can create custom roles and add that to teams, but we cannot add that to API keys. Can we please add functionality where we can add custom roles to API keys as well?"
This is the team mechanism reused, not a second one:
ApiKeyCustomRolerow says "this key has this role". It sits besideTeamCustomRoleand has the CRUD route/api-key-custom-role.ApiKeyPermissioncopy of each of the role's permissions, with the same labels.CustomRoleSynccreates these copies and keeps them in step with the role, as it does for teams. Each copy is marked withcustomRoleId,apiKeyCustomRoleIdandcustomRolePermissionId, and the database deletes it when the role is deleted, taken away, or loses that permission (ON DELETE CASCADE).ApiKeyPermission. So a key's effective permissions include its roles with nothing new to get wrong: labels and scoping behave as for the key's own permissions.Safety
ApiKeyPermissionService.assertCallerCanGrantPermission). A property test shows this ceiling is never looser than the team ceiling for a role's copies.CacheGenerations.@EnableAuditLog).Where it shows
oneuptime_api_key_custom_roleresource (changing its key or role replaces it).oneuptime_api_key_permissioncan read the three copy columns but not set them.permissions/index.mdandconfiguration/audit-logs.md, in all 17 languages.Migrations
1806500000000-AddApiKeyCustomRoles: the table with its indexes and FKs, plus the three columns onApiKeyPermission.1806510000000-AddApiKeyPermissionCustomRoleLinks: the copy columns' indexes and FKs, built withOnlineDdloutside a transaction.Tests
ApiKeyRoleCopyPlan.test.ts(seeded property tests).CustomRoleSyncApiKeys.test.ts,ApiKeyCustomRoleService.test.ts,ApiKeyPermissionRoleCopies.test.tsandApiKeyCeilingWithinTeamCeiling.test.ts.ApiKeyCustomRolesPostgres.test.ts, 20 tests, added to thepostgres-schema-driftworkflow. It covers a role alone, a role beside the key's own permissions, label-scoped roles, Owned-scope permissions, a key's own block, two roles holding the same permission, cache invalidation on every kind of change, and refusals.ApiKeyCustomRoleApiAndTerraform.test.ts, which reads the real OpenAPI spec and the real provider parser.ApiKeyViewCustomRoles.test.tsxandRoleViewApiKeys.test.tsx;ApiKeysCreateAccess.test.tsx;CustomRoleRelationSelect.test.ts, which pins a relation-select bug onCustomRole.permissionCountthat was fixed here;packages/E2E/CustomRoles/ApiKeyCustomRoles.spec.ts, 12 specs on the custom roles fixture (which now also mountsSettingsRoutes), at 1280 and 390 px and in dark mode.Verification (local)
tscandcompile-testspass. The guards, locale suites and this branch's suites pass, including withee/moved aside.tscpasses.Tests/Dashboard, the App root suites andTests/FeatureSet/Docsall pass exceptCliDocsFacts. That suite fails on master too (master'sDatabaseServicesort change) and none of its files differ from master here.i18n:check,i18n:validateanddocs:check-anchorspass.UI review
Looked at: every surface this PR adds or changes, rendered from the real pages through the offline custom roles fixture in headless Chromium (Inter loaded), at 1280 and 390 px, light and dark, in English, German and Japanese, with long names and empty, one-row and every-row states:
What was not great, and what changed:
hideTitleOnMobile, which they already passed through to the table.ApiKeyCustomRolenow state the Owned exception (the builder left it out).DashboardComponentsAToLHardcodedGuardflags./\d+ API keys?/) that the ReDoS checker found quadratic, now anchored at a word edge.Strings: 13 new sentences (one with its
_oneform) in all 17 locales, translated, with each language's own quote marks around the scope's name. The three builder sentences they replace are removed.i18n:checkreports no translation lost.Tests added or changed:
CustomRoleCopy.test.ts: the reason, the API Keys card in every case (unknown, none, some and all owned, counts out of range), both dialog descriptions, German. Every new sentence is inen.json.CustomRoleOptions.test.ts:loadCustomRoleGrantsreads one role when given acustomRoleId;getApiKeyRoleCoverage.ApiKeyViewCustomRoles.test.tsx: the (i) only on a partial role, its accessible name and tooltip, an all-owned role, and the Add dialog with and without the exception.RoleViewApiKeys.test.tsx: the card and Give dialog for partial and all-owned roles, one role's permissions read, the card following a change of permissions, a project with no key (empty card, link, Give off with its reason), and every key having the role.ApiKeyCustomRoles.spec.ts(offline browser), 5 new and 4 updated specs:?keys=none, with the link working;?keys=none.ApiKeyCustomRoleApiAndTerraform.test.tspins the new description.Run locally:
Localesuites, andnpx jest Guardplus the guard-like suites (168 suites).CustomRolesDocsandPermissionsAndSelfHostedDocsFacts(191 suites). The changed suites also ran withee/moved aside.Tests/Ops(64 suites).tscandcompile-tests, Appcompile, Dashboardtscand E2Etsc.i18n:check, prettier and eslint on every changed code file, the--listFilespackage check, and the ReDoS checker, which is clean..mdfiles this branch changes are not prettier-clean, and neither are master's copies of them.Left as it was, on purpose:
ButtonStyleType.NORMAL) still looks enabled, across the whole product. The tooltip says why it is off; restyling the shared Button is a change of its own.🤖 Generated with Claude Code