fix: use correct role-based JWT tokens in all integration tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-16 20:03:38 +01:00
parent 39f9925e71
commit 9f74e47ecf
11 changed files with 76 additions and 28 deletions

View File

@@ -27,11 +27,39 @@ public class TestSecurityHelper {
} }
/** /**
* Registers a test agent and returns a valid JWT access token for it. * Registers a test agent and returns a valid JWT access token with AGENT role.
*/ */
public String registerTestAgent(String agentId) { public String registerTestAgent(String agentId) {
agentRegistryService.register(agentId, "test", "test-group", "1.0", List.of(), Map.of()); agentRegistryService.register(agentId, "test", "test-group", "1.0", List.of(), Map.of());
return jwtService.createAccessToken(agentId, "test-group"); return jwtService.createAccessToken(agentId, "test-group", List.of("AGENT"));
}
/**
* Returns a valid JWT access token with the given roles (no agent registration).
*/
public String createToken(String subject, String group, List<String> roles) {
return jwtService.createAccessToken(subject, group, roles);
}
/**
* Returns a valid JWT access token with OPERATOR role.
*/
public String operatorToken() {
return jwtService.createAccessToken("test-operator", "user", List.of("OPERATOR"));
}
/**
* Returns a valid JWT access token with ADMIN role.
*/
public String adminToken() {
return jwtService.createAccessToken("test-admin", "user", List.of("ADMIN"));
}
/**
* Returns a valid JWT access token with VIEWER role.
*/
public String viewerToken() {
return jwtService.createAccessToken("test-viewer", "user", List.of("VIEWER"));
} }
/** /**

View File

@@ -29,11 +29,13 @@ class AgentCommandControllerIT extends AbstractPostgresIT {
@Autowired @Autowired
private TestSecurityHelper securityHelper; private TestSecurityHelper securityHelper;
private String jwt; private String agentJwt;
private String operatorJwt;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
jwt = securityHelper.registerTestAgent("test-agent-command-it"); agentJwt = securityHelper.registerTestAgent("test-agent-command-it");
operatorJwt = securityHelper.operatorToken();
} }
private ResponseEntity<String> registerAgent(String agentId, String name, String group) { private ResponseEntity<String> registerAgent(String agentId, String name, String group) {
@@ -65,7 +67,7 @@ class AgentCommandControllerIT extends AbstractPostgresIT {
ResponseEntity<String> response = restTemplate.postForEntity( ResponseEntity<String> response = restTemplate.postForEntity(
"/api/v1/agents/" + agentId + "/commands", "/api/v1/agents/" + agentId + "/commands",
new HttpEntity<>(commandJson, securityHelper.authHeaders(jwt)), new HttpEntity<>(commandJson, securityHelper.authHeaders(operatorJwt)),
String.class); String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
@@ -88,7 +90,7 @@ class AgentCommandControllerIT extends AbstractPostgresIT {
ResponseEntity<String> response = restTemplate.postForEntity( ResponseEntity<String> response = restTemplate.postForEntity(
"/api/v1/agents/groups/" + group + "/commands", "/api/v1/agents/groups/" + group + "/commands",
new HttpEntity<>(commandJson, securityHelper.authHeaders(jwt)), new HttpEntity<>(commandJson, securityHelper.authHeaders(operatorJwt)),
String.class); String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
@@ -110,7 +112,7 @@ class AgentCommandControllerIT extends AbstractPostgresIT {
ResponseEntity<String> response = restTemplate.postForEntity( ResponseEntity<String> response = restTemplate.postForEntity(
"/api/v1/agents/commands", "/api/v1/agents/commands",
new HttpEntity<>(commandJson, securityHelper.authHeaders(jwt)), new HttpEntity<>(commandJson, securityHelper.authHeaders(operatorJwt)),
String.class); String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
@@ -131,7 +133,7 @@ class AgentCommandControllerIT extends AbstractPostgresIT {
ResponseEntity<String> cmdResponse = restTemplate.postForEntity( ResponseEntity<String> cmdResponse = restTemplate.postForEntity(
"/api/v1/agents/" + agentId + "/commands", "/api/v1/agents/" + agentId + "/commands",
new HttpEntity<>(commandJson, securityHelper.authHeaders(jwt)), new HttpEntity<>(commandJson, securityHelper.authHeaders(operatorJwt)),
String.class); String.class);
JsonNode cmdBody = objectMapper.readTree(cmdResponse.getBody()); JsonNode cmdBody = objectMapper.readTree(cmdResponse.getBody());
@@ -140,7 +142,7 @@ class AgentCommandControllerIT extends AbstractPostgresIT {
ResponseEntity<Void> ackResponse = restTemplate.exchange( ResponseEntity<Void> ackResponse = restTemplate.exchange(
"/api/v1/agents/" + agentId + "/commands/" + commandId + "/ack", "/api/v1/agents/" + agentId + "/commands/" + commandId + "/ack",
HttpMethod.POST, HttpMethod.POST,
new HttpEntity<>(securityHelper.authHeadersNoBody(jwt)), new HttpEntity<>(securityHelper.authHeadersNoBody(agentJwt)),
Void.class); Void.class);
assertThat(ackResponse.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(ackResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
@@ -154,7 +156,7 @@ class AgentCommandControllerIT extends AbstractPostgresIT {
ResponseEntity<Void> response = restTemplate.exchange( ResponseEntity<Void> response = restTemplate.exchange(
"/api/v1/agents/" + agentId + "/commands/nonexistent-cmd-id/ack", "/api/v1/agents/" + agentId + "/commands/nonexistent-cmd-id/ack",
HttpMethod.POST, HttpMethod.POST,
new HttpEntity<>(securityHelper.authHeadersNoBody(jwt)), new HttpEntity<>(securityHelper.authHeadersNoBody(agentJwt)),
Void.class); Void.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
@@ -168,7 +170,7 @@ class AgentCommandControllerIT extends AbstractPostgresIT {
ResponseEntity<String> response = restTemplate.postForEntity( ResponseEntity<String> response = restTemplate.postForEntity(
"/api/v1/agents/nonexistent-agent-xyz/commands", "/api/v1/agents/nonexistent-agent-xyz/commands",
new HttpEntity<>(commandJson, securityHelper.authHeaders(jwt)), new HttpEntity<>(commandJson, securityHelper.authHeaders(operatorJwt)),
String.class); String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);

View File

@@ -28,10 +28,12 @@ class AgentRegistrationControllerIT extends AbstractPostgresIT {
private TestSecurityHelper securityHelper; private TestSecurityHelper securityHelper;
private String jwt; private String jwt;
private String viewerJwt;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
jwt = securityHelper.registerTestAgent("test-agent-registration-it"); jwt = securityHelper.registerTestAgent("test-agent-registration-it");
viewerJwt = securityHelper.viewerToken();
} }
private ResponseEntity<String> registerAgent(String agentId, String name) { private ResponseEntity<String> registerAgent(String agentId, String name) {
@@ -114,7 +116,7 @@ class AgentRegistrationControllerIT extends AbstractPostgresIT {
ResponseEntity<String> response = restTemplate.exchange( ResponseEntity<String> response = restTemplate.exchange(
"/api/v1/agents", "/api/v1/agents",
HttpMethod.GET, HttpMethod.GET,
new HttpEntity<>(securityHelper.authHeadersNoBody(jwt)), new HttpEntity<>(securityHelper.authHeadersNoBody(viewerJwt)),
String.class); String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
@@ -131,7 +133,7 @@ class AgentRegistrationControllerIT extends AbstractPostgresIT {
ResponseEntity<String> response = restTemplate.exchange( ResponseEntity<String> response = restTemplate.exchange(
"/api/v1/agents?status=LIVE", "/api/v1/agents?status=LIVE",
HttpMethod.GET, HttpMethod.GET,
new HttpEntity<>(securityHelper.authHeadersNoBody(jwt)), new HttpEntity<>(securityHelper.authHeadersNoBody(viewerJwt)),
String.class); String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
@@ -148,7 +150,7 @@ class AgentRegistrationControllerIT extends AbstractPostgresIT {
ResponseEntity<String> response = restTemplate.exchange( ResponseEntity<String> response = restTemplate.exchange(
"/api/v1/agents?status=INVALID", "/api/v1/agents?status=INVALID",
HttpMethod.GET, HttpMethod.GET,
new HttpEntity<>(securityHelper.authHeadersNoBody(jwt)), new HttpEntity<>(securityHelper.authHeadersNoBody(viewerJwt)),
String.class); String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);

View File

@@ -45,10 +45,12 @@ class AgentSseControllerIT extends AbstractPostgresIT {
private int port; private int port;
private String jwt; private String jwt;
private String operatorJwt;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
jwt = securityHelper.registerTestAgent("test-agent-sse-it"); jwt = securityHelper.registerTestAgent("test-agent-sse-it");
operatorJwt = securityHelper.operatorToken();
} }
private ResponseEntity<String> registerAgent(String agentId, String name, String group) { private ResponseEntity<String> registerAgent(String agentId, String name, String group) {
@@ -76,7 +78,7 @@ class AgentSseControllerIT extends AbstractPostgresIT {
return restTemplate.postForEntity( return restTemplate.postForEntity(
"/api/v1/agents/" + agentId + "/commands", "/api/v1/agents/" + agentId + "/commands",
new HttpEntity<>(json, securityHelper.authHeaders(jwt)), new HttpEntity<>(json, securityHelper.authHeaders(operatorJwt)),
String.class); String.class);
} }

View File

@@ -34,6 +34,7 @@ class DetailControllerIT extends AbstractPostgresIT {
private final ObjectMapper objectMapper = new ObjectMapper(); private final ObjectMapper objectMapper = new ObjectMapper();
private String jwt; private String jwt;
private String viewerJwt;
private String seededExecutionId; private String seededExecutionId;
/** /**
@@ -43,6 +44,7 @@ class DetailControllerIT extends AbstractPostgresIT {
@BeforeAll @BeforeAll
void seedTestData() { void seedTestData() {
jwt = securityHelper.registerTestAgent("test-agent-detail-it"); jwt = securityHelper.registerTestAgent("test-agent-detail-it");
viewerJwt = securityHelper.viewerToken();
String json = """ String json = """
{ {
@@ -217,7 +219,7 @@ class DetailControllerIT extends AbstractPostgresIT {
} }
private ResponseEntity<String> detailGet(String path) { private ResponseEntity<String> detailGet(String path) {
HttpHeaders headers = securityHelper.authHeadersNoBody(jwt); HttpHeaders headers = securityHelper.authHeadersNoBody(viewerJwt);
return restTemplate.exchange( return restTemplate.exchange(
"/api/v1/executions" + path, "/api/v1/executions" + path,
HttpMethod.GET, HttpMethod.GET,

View File

@@ -29,6 +29,7 @@ class DiagramRenderControllerIT extends AbstractPostgresIT {
private TestSecurityHelper securityHelper; private TestSecurityHelper securityHelper;
private String jwt; private String jwt;
private String viewerJwt;
private String contentHash; private String contentHash;
/** /**
@@ -37,6 +38,7 @@ class DiagramRenderControllerIT extends AbstractPostgresIT {
@BeforeEach @BeforeEach
void seedDiagram() { void seedDiagram() {
jwt = securityHelper.registerTestAgent("test-agent-diagram-render-it"); jwt = securityHelper.registerTestAgent("test-agent-diagram-render-it");
viewerJwt = securityHelper.viewerToken();
String json = """ String json = """
{ {
@@ -73,7 +75,7 @@ class DiagramRenderControllerIT extends AbstractPostgresIT {
@Test @Test
void getSvg_withAcceptHeader_returnsSvg() { void getSvg_withAcceptHeader_returnsSvg() {
HttpHeaders headers = securityHelper.authHeadersNoBody(jwt); HttpHeaders headers = securityHelper.authHeadersNoBody(viewerJwt);
headers.set("Accept", "image/svg+xml"); headers.set("Accept", "image/svg+xml");
ResponseEntity<String> response = restTemplate.exchange( ResponseEntity<String> response = restTemplate.exchange(
@@ -90,7 +92,7 @@ class DiagramRenderControllerIT extends AbstractPostgresIT {
@Test @Test
void getJson_withAcceptHeader_returnsJson() { void getJson_withAcceptHeader_returnsJson() {
HttpHeaders headers = securityHelper.authHeadersNoBody(jwt); HttpHeaders headers = securityHelper.authHeadersNoBody(viewerJwt);
headers.set("Accept", "application/json"); headers.set("Accept", "application/json");
ResponseEntity<String> response = restTemplate.exchange( ResponseEntity<String> response = restTemplate.exchange(
@@ -107,7 +109,7 @@ class DiagramRenderControllerIT extends AbstractPostgresIT {
@Test @Test
void getNonExistentHash_returns404() { void getNonExistentHash_returns404() {
HttpHeaders headers = securityHelper.authHeadersNoBody(jwt); HttpHeaders headers = securityHelper.authHeadersNoBody(viewerJwt);
headers.set("Accept", "image/svg+xml"); headers.set("Accept", "image/svg+xml");
ResponseEntity<String> response = restTemplate.exchange( ResponseEntity<String> response = restTemplate.exchange(
@@ -122,7 +124,7 @@ class DiagramRenderControllerIT extends AbstractPostgresIT {
@Test @Test
void getWithNoAcceptHeader_defaultsToSvg() { void getWithNoAcceptHeader_defaultsToSvg() {
HttpHeaders headers = securityHelper.authHeadersNoBody(jwt); HttpHeaders headers = securityHelper.authHeadersNoBody(viewerJwt);
ResponseEntity<String> response = restTemplate.exchange( ResponseEntity<String> response = restTemplate.exchange(
"/api/v1/diagrams/{hash}/render", "/api/v1/diagrams/{hash}/render",

View File

@@ -35,6 +35,7 @@ class SearchControllerIT extends AbstractPostgresIT {
private final ObjectMapper objectMapper = new ObjectMapper(); private final ObjectMapper objectMapper = new ObjectMapper();
private String jwt; private String jwt;
private String viewerJwt;
/** /**
* Seed test data: Insert executions with varying statuses, times, durations, * Seed test data: Insert executions with varying statuses, times, durations,
@@ -43,6 +44,7 @@ class SearchControllerIT extends AbstractPostgresIT {
@BeforeAll @BeforeAll
void seedTestData() { void seedTestData() {
jwt = securityHelper.registerTestAgent("test-agent-search-it"); jwt = securityHelper.registerTestAgent("test-agent-search-it");
viewerJwt = securityHelper.viewerToken();
// Execution 1: COMPLETED, short duration, no errors // Execution 1: COMPLETED, short duration, no errors
ingest(""" ingest("""
@@ -376,7 +378,7 @@ class SearchControllerIT extends AbstractPostgresIT {
return restTemplate.exchange( return restTemplate.exchange(
"/api/v1/search/executions", "/api/v1/search/executions",
HttpMethod.POST, HttpMethod.POST,
new HttpEntity<>(jsonBody, securityHelper.authHeaders(jwt)), new HttpEntity<>(jsonBody, securityHelper.authHeaders(viewerJwt)),
String.class); String.class);
} }
} }

View File

@@ -153,13 +153,13 @@ class JwtRefreshIT extends AbstractPostgresIT {
JsonNode refreshBody2 = objectMapper.readTree(refreshResponse.getBody()); JsonNode refreshBody2 = objectMapper.readTree(refreshResponse.getBody());
String newAccessToken = refreshBody2.get("accessToken").asText(); String newAccessToken = refreshBody2.get("accessToken").asText();
// Use the new access token to hit a protected endpoint // Use the new access token to hit a protected endpoint accessible by AGENT role
HttpHeaders authHeaders = new HttpHeaders(); HttpHeaders authHeaders = new HttpHeaders();
authHeaders.set("Authorization", "Bearer " + newAccessToken); authHeaders.set("Authorization", "Bearer " + newAccessToken);
authHeaders.set("X-Cameleer-Protocol-Version", "1"); authHeaders.set("X-Cameleer-Protocol-Version", "1");
ResponseEntity<String> response = restTemplate.exchange( ResponseEntity<String> response = restTemplate.exchange(
"/api/v1/agents", "/api/v1/search/executions",
HttpMethod.GET, HttpMethod.GET,
new HttpEntity<>(authHeaders), new HttpEntity<>(authHeaders),
String.class); String.class);

View File

@@ -81,13 +81,13 @@ class RegistrationSecurityIT extends AbstractPostgresIT {
JsonNode regBody = objectMapper.readTree(regResponse.getBody()); JsonNode regBody = objectMapper.readTree(regResponse.getBody());
String accessToken = regBody.get("accessToken").asText(); String accessToken = regBody.get("accessToken").asText();
// Use the access token to hit a protected endpoint // Use the access token to hit a protected endpoint accessible by AGENT role
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + accessToken); headers.set("Authorization", "Bearer " + accessToken);
headers.set("X-Cameleer-Protocol-Version", "1"); headers.set("X-Cameleer-Protocol-Version", "1");
ResponseEntity<String> response = restTemplate.exchange( ResponseEntity<String> response = restTemplate.exchange(
"/api/v1/agents", "/api/v1/search/executions",
HttpMethod.GET, HttpMethod.GET,
new HttpEntity<>(headers), new HttpEntity<>(headers),
String.class); String.class);

View File

@@ -28,10 +28,12 @@ class SecurityFilterIT extends AbstractPostgresIT {
private TestSecurityHelper securityHelper; private TestSecurityHelper securityHelper;
private String jwt; private String jwt;
private String viewerJwt;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
jwt = securityHelper.registerTestAgent("test-agent-security-filter-it"); jwt = securityHelper.registerTestAgent("test-agent-security-filter-it");
viewerJwt = securityHelper.viewerToken();
} }
@Test @Test
@@ -53,7 +55,7 @@ class SecurityFilterIT extends AbstractPostgresIT {
ResponseEntity<String> response = restTemplate.exchange( ResponseEntity<String> response = restTemplate.exchange(
"/api/v1/agents", "/api/v1/agents",
HttpMethod.GET, HttpMethod.GET,
new HttpEntity<>(securityHelper.authHeadersNoBody(jwt)), new HttpEntity<>(securityHelper.authHeadersNoBody(viewerJwt)),
String.class); String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);

View File

@@ -1,6 +1,7 @@
package com.cameleer3.server.app.security; package com.cameleer3.server.app.security;
import com.cameleer3.server.app.AbstractPostgresIT; import com.cameleer3.server.app.AbstractPostgresIT;
import com.cameleer3.server.app.TestSecurityHelper;
import com.cameleer3.server.core.security.Ed25519SigningService; import com.cameleer3.server.core.security.Ed25519SigningService;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@@ -52,6 +53,9 @@ class SseSigningIT extends AbstractPostgresIT {
@Autowired @Autowired
private ObjectMapper objectMapper; private ObjectMapper objectMapper;
@Autowired
private TestSecurityHelper securityHelper;
@Autowired @Autowired
private Ed25519SigningService ed25519SigningService; private Ed25519SigningService ed25519SigningService;
@@ -165,6 +169,7 @@ class SseSigningIT extends AbstractPostgresIT {
String agentId = "sse-sign-it-" + UUID.randomUUID().toString().substring(0, 8); String agentId = "sse-sign-it-" + UUID.randomUUID().toString().substring(0, 8);
JsonNode registration = registerAgentWithAuth(agentId); JsonNode registration = registerAgentWithAuth(agentId);
String accessToken = registration.get("accessToken").asText(); String accessToken = registration.get("accessToken").asText();
String operatorToken = securityHelper.operatorToken();
String serverPublicKey = registration.get("serverPublicKey").asText(); String serverPublicKey = registration.get("serverPublicKey").asText();
SseStream stream = openSseStream(agentId, accessToken); SseStream stream = openSseStream(agentId, accessToken);
@@ -177,7 +182,7 @@ class SseSigningIT extends AbstractPostgresIT {
await().atMost(10, TimeUnit.SECONDS).pollInterval(200, TimeUnit.MILLISECONDS) await().atMost(10, TimeUnit.SECONDS).pollInterval(200, TimeUnit.MILLISECONDS)
.ignoreExceptions() .ignoreExceptions()
.until(() -> { .until(() -> {
sendCommand(agentId, "config-update", originalPayload, accessToken); sendCommand(agentId, "config-update", originalPayload, operatorToken);
List<String> lines = stream.snapshot(); List<String> lines = stream.snapshot();
return lines.stream().anyMatch(l -> l.contains("event:config-update")); return lines.stream().anyMatch(l -> l.contains("event:config-update"));
}); });
@@ -221,6 +226,7 @@ class SseSigningIT extends AbstractPostgresIT {
String agentId = "sse-sign-trace-" + UUID.randomUUID().toString().substring(0, 8); String agentId = "sse-sign-trace-" + UUID.randomUUID().toString().substring(0, 8);
JsonNode registration = registerAgentWithAuth(agentId); JsonNode registration = registerAgentWithAuth(agentId);
String accessToken = registration.get("accessToken").asText(); String accessToken = registration.get("accessToken").asText();
String operatorToken = securityHelper.operatorToken();
String serverPublicKey = registration.get("serverPublicKey").asText(); String serverPublicKey = registration.get("serverPublicKey").asText();
SseStream stream = openSseStream(agentId, accessToken); SseStream stream = openSseStream(agentId, accessToken);
@@ -232,7 +238,7 @@ class SseSigningIT extends AbstractPostgresIT {
await().atMost(10, TimeUnit.SECONDS).pollInterval(200, TimeUnit.MILLISECONDS) await().atMost(10, TimeUnit.SECONDS).pollInterval(200, TimeUnit.MILLISECONDS)
.ignoreExceptions() .ignoreExceptions()
.until(() -> { .until(() -> {
sendCommand(agentId, "deep-trace", originalPayload, accessToken); sendCommand(agentId, "deep-trace", originalPayload, operatorToken);
List<String> lines = stream.snapshot(); List<String> lines = stream.snapshot();
return lines.stream().anyMatch(l -> l.contains("event:deep-trace")); return lines.stream().anyMatch(l -> l.contains("event:deep-trace"));
}); });