From d5806b56dd0aca8e2d37e2d6721fcf72f4367363 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:34:11 +0100 Subject: [PATCH] Allow admins to create team-scoped API keys --- .../service/ApiKeyManagementService.java | 17 +++++++++++++---- .../service/ApiKeyManagementServiceTest.java | 18 ++++++++++++++++++ .../public/locales/en-US/translation.toml | 2 +- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/ApiKeyManagementService.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/ApiKeyManagementService.java index 3a33255f33..22faee1381 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/ApiKeyManagementService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/ApiKeyManagementService.java @@ -71,7 +71,7 @@ public class ApiKeyManagementService { User caller = requireCaller(); migrateLegacyKey(caller); - boolean isManager = policyAuthority.canEditPolicies(); + boolean isManager = isTeamKeyManager(); Long teamId = policyAuthority.currentUserTeamId(); String teamName = teamId == null ? null : teamNameFor(teamId); @@ -153,9 +153,10 @@ public class ApiKeyManagementService { Long teamId = null; String teamName = null; if (scope.isTeamScoped()) { - if (!policyAuthority.canEditPolicies()) { + if (!isTeamKeyManager()) { throw new ResponseStatusException( - HttpStatus.FORBIDDEN, "Only a team leader may create team API keys"); + HttpStatus.FORBIDDEN, + "Team API keys can only be created by a team leader or an admin"); } teamId = policyAuthority.currentUserTeamId(); if (teamId == null) { @@ -204,12 +205,20 @@ public class ApiKeyManagementService { clearLegacyColumnIfMatches(key); } + /** + * Team keys are managed by a team leader (the SaaS rule) or an admin (either flavor) - an admin + * is effectively a leader for this purpose, which the SaaS team-leader-only check misses. + */ + private boolean isTeamKeyManager() { + return policyAuthority.canEditPolicies() || userService.isCurrentUserAdmin(); + } + /** Whether the caller may revoke a given key. */ private boolean canManage(User caller, ApiKey key) { if (key.getScope() == ApiKeyScope.PERSONAL) { return key.getOwnerUserId().equals(caller.getId()); } - return policyAuthority.canEditPolicies() + return isTeamKeyManager() && key.getTeamId() != null && key.getTeamId().equals(policyAuthority.currentUserTeamId()); } diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/security/service/ApiKeyManagementServiceTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/security/service/ApiKeyManagementServiceTest.java index 8d032a1f5f..32e1ee1ec2 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/security/service/ApiKeyManagementServiceTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/security/service/ApiKeyManagementServiceTest.java @@ -235,6 +235,24 @@ class ApiKeyManagementServiceTest { assertThat(saved.getValue().getTeamId()).isEqualTo(5L); } + @Test + @DisplayName("an admin who is not a team leader can still create a team key") + void adminCanCreateTeamKey() { + when(policyAuthority.canEditPolicies()).thenReturn(false); + when(userService.isCurrentUserAdmin()).thenReturn(true); + when(policyAuthority.currentUserTeamId()).thenReturn(5L); + Team team = new Team(); + team.setName("Acme"); + when(teamRepository.findById(5L)).thenReturn(Optional.of(team)); + + service.createKey(new CreateApiKeyRequest("Admin team key", "team-members")); + + ArgumentCaptor saved = ArgumentCaptor.forClass(ApiKey.class); + verify(apiKeyRepository).save(saved.capture()); + assertThat(saved.getValue().getScope()).isEqualTo(ApiKeyScope.TEAM_MEMBERS); + assertThat(saved.getValue().getTeamId()).isEqualTo(5L); + } + // ---- revocation --------------------------------------------------------- @Test diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index 49e9388573..1b84244e24 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -7078,7 +7078,7 @@ secretKeyCaption = "Secret key" secretWarning = "Store this in a secrets manager. Stirling only ever stores a hash, so there is no way to recover it later." subtitle = "Scope the key to the minimum it needs. You can revoke it at any time." subtitleCreated = "Copy this secret now — it won't be shown again." -teamScopeLeaderOnly = "Only team leaders can create team-scoped keys." +teamScopeLeaderOnly = "Team-scoped keys need team-leader or admin rights and a team to scope them to." title = "Create API key" titleCreated = "Key created" yourTeam = "your team"