Allow admins to create team-scoped API keys

This commit is contained in:
Anthony Stirling
2026-07-10 13:34:11 +01:00
parent 49dbf76670
commit d5806b56dd
3 changed files with 32 additions and 5 deletions
@@ -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());
}
@@ -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<ApiKey> 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
@@ -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"