1
0

method-level security-control with some open endpoints (e.g. /api/ping) (#191)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/191
This commit is contained in:
Michael Hoennig
2025-08-26 11:50:09 +02:00
parent 5a5c1466b0
commit 2a6e86aca8
27 changed files with 143 additions and 22 deletions
@@ -20,6 +20,7 @@ import net.hostsharing.hsadminng.rbac.context.ContextBasedTest;
import net.hostsharing.hsadminng.rbac.grant.RbacGrantsDiagramService;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.repository.Repository;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RestController;
import jakarta.annotation.PostConstruct;
@@ -369,6 +370,32 @@ public class ArchitectureTest {
.beAnnotatedWith(SecurityRequirement.class).orShould()
.beAnnotatedWith(NoSecurityRequirement.class);
@ArchTest
@SuppressWarnings("unused")
static final ArchRule everyRestControllerShouldRequireAuthentication =
classes()
.that().areAnnotatedWith(RestController.class)
.should(havePreAuthorizeWithValue("isAuthenticated()"))
.because("Every REST controller should require authentication by default, use @PreAuthorize(...) to override this at the endpoint method level.");
private static ArchCondition<JavaClass> havePreAuthorizeWithValue(String expectedValue) {
return new ArchCondition<>("have @PreAuthorize(\"" + expectedValue + "\")") {
@Override
public void check(JavaClass javaClass, ConditionEvents events) {
boolean satisfied = javaClass.tryGetAnnotationOfType(PreAuthorize.class)
.map(annotation -> expectedValue.equals(annotation.value()))
.orElse(false);
String message = javaClass.getDescription() +
(satisfied ? " has @PreAuthorize(\"" + expectedValue + "\")"
: " does not have @PreAuthorize(\"" + expectedValue + "\")");
events.add(new SimpleConditionEvent(javaClass, satisfied, message));
}
};
}
@ArchTest
@SuppressWarnings("unused")
static final ArchRule restControllerMethods = classes()
@@ -398,7 +425,6 @@ public class ArchitectureTest {
.should(haveTableNameEndingWith_rv())
.because("it's required that the table names of RBAC entities end with '_rv'");
private static DescribedPredicate<JavaMethod> hasStaticMethodNamed(final String expectedName) {
return new DescribedPredicate<>("rbac entity") {
@Override
@@ -113,20 +113,37 @@ class WebSecurityConfigIntegrationTest {
}
@Test
void accessToApiWithoutTokenShouldBeDenied() {
void accessToPingApiWithoutTokenShouldBePermitted() {
final var result = this.restTemplate.getForEntity(
"http://localhost:" + this.serverPort + "/api/ping", String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
void accessToApiWithInvalidTokenShouldBeDenied() {
void accessToPongApiWithValidTokenShouldBePermitted() {
// given
givenCasTicketValidationResponse("ST-fake-cas-ticket", "fake-user-name");
// when
final var result = restTemplate.exchange(
"http://localhost:" + this.serverPort + "/api/ping",
"http://localhost:" + this.serverPort + "/api/pong",
HttpMethod.GET,
httpHeaders(entry("Authorization", "Bearer ST-fake-cas-ticket")),
String.class
);
// then
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
void accessToPongApiWithInvalidTokenShouldBeDenied() {
// given
givenCasTicketValidationResponse("ST-fake-cas-ticket", "fake-user-name");
// when
final var result = restTemplate.exchange(
"http://localhost:" + this.serverPort + "/api/pong",
HttpMethod.GET,
httpHeaders(entry("Authorization", "Bearer ST-WRONG-cas-ticket")),
String.class