feat(security): introduce AgentOwnershipGuard for agent-id JWT subject check
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
package io.cameleer.server.app.security;
|
||||
|
||||
import io.cameleer.server.core.security.JwtService.JwtValidationResult;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
/**
|
||||
* Verifies that the authenticated JWT subject matches the {@code {id}} path
|
||||
* variable on agent self-service endpoints. Required wherever an AGENT-role
|
||||
* caller could otherwise act on another agent's identity.
|
||||
*/
|
||||
@Component
|
||||
public class AgentOwnershipGuard {
|
||||
|
||||
public void requireAgentOwnership(String pathId, HttpServletRequest request) {
|
||||
String subject = resolveAgentSubject(request);
|
||||
if (subject == null || !subject.equals(pathId)) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN,
|
||||
"Agent token does not own the requested agent id");
|
||||
}
|
||||
}
|
||||
|
||||
public String resolveAgentSubject(HttpServletRequest request) {
|
||||
Object attr = request.getAttribute(JwtAuthenticationFilter.JWT_RESULT_ATTR);
|
||||
if (attr instanceof JwtValidationResult result) {
|
||||
return result.subject();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package io.cameleer.server.app.security;
|
||||
|
||||
import io.cameleer.server.core.security.JwtService.JwtValidationResult;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class AgentOwnershipGuardTest {
|
||||
|
||||
private final AgentOwnershipGuard guard = new AgentOwnershipGuard();
|
||||
|
||||
@Test
|
||||
void allowsWhenSubjectMatchesPathId() {
|
||||
HttpServletRequest request = requestWith(jwt("agent-A"));
|
||||
guard.requireAgentOwnership("agent-A", request); // does not throw
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsWhenSubjectMismatchesPathId() {
|
||||
HttpServletRequest request = requestWith(jwt("agent-A"));
|
||||
assertThatThrownBy(() -> guard.requireAgentOwnership("agent-B", request))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.extracting(e -> ((ResponseStatusException) e).getStatusCode())
|
||||
.isEqualTo(HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsWhenJwtAttributeMissing() {
|
||||
HttpServletRequest request = new MockHttpServletRequest();
|
||||
assertThatThrownBy(() -> guard.requireAgentOwnership("agent-A", request))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.extracting(e -> ((ResponseStatusException) e).getStatusCode())
|
||||
.isEqualTo(HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsWhenSubjectIsNull() {
|
||||
HttpServletRequest request = requestWith(jwt(null));
|
||||
assertThatThrownBy(() -> guard.requireAgentOwnership("agent-A", request))
|
||||
.isInstanceOf(ResponseStatusException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void exposesSubjectViaResolvedHelper() {
|
||||
HttpServletRequest request = requestWith(jwt("agent-A"));
|
||||
assertThat(guard.resolveAgentSubject(request)).isEqualTo("agent-A");
|
||||
}
|
||||
|
||||
private JwtValidationResult jwt(String subject) {
|
||||
return new JwtValidationResult(subject, "default-app", "default", List.of("AGENT"), Instant.now());
|
||||
}
|
||||
|
||||
private HttpServletRequest requestWith(JwtValidationResult result) {
|
||||
MockHttpServletRequest req = new MockHttpServletRequest();
|
||||
req.setAttribute(JwtAuthenticationFilter.JWT_RESULT_ATTR, result);
|
||||
return req;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user