From 2a6e86aca86c11cf16f84ce1f667236fbc63f47f Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 26 Aug 2025 11:50:09 +0200 Subject: [PATCH] method-level security-control with some open endpoints (e.g. /api/ping) (#191) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/191 --- .../hsadminng/config/WebSecurityConfig.java | 9 +++-- .../HsCredentialsContextsController.java | 9 +++-- .../hs/accounts/HsCredentialsController.java | 2 ++ .../booking/item/HsBookingItemController.java | 2 ++ .../project/HsBookingProjectController.java | 2 ++ .../asset/HsHostingAssetController.java | 2 ++ .../asset/HsHostingAssetPropsController.java | 4 +++ .../HsOfficeBankAccountController.java | 2 ++ .../contact/HsOfficeContactController.java | 2 ++ ...OfficeCoopAssetsTransactionController.java | 2 ++ ...OfficeCoopSharesTransactionController.java | 2 ++ .../debitor/HsOfficeDebitorController.java | 2 ++ .../HsOfficeMembershipController.java | 2 ++ .../partner/HsOfficePartnerController.java | 2 ++ .../person/HsOfficePersonController.java | 2 ++ .../relation/HsOfficeRelationController.java | 2 ++ .../HsOfficeSepaMandateController.java | 2 ++ .../hsadminng/ping/PingController.java | 34 +++++++++++++------ .../rbac/context/RbacContextController.java | 2 ++ .../rbac/grant/RbacGrantController.java | 2 ++ .../rbac/role/RbacRoleController.java | 2 ++ .../rbac/subject/RbacSubjectController.java | 3 ++ .../test/cust/TestCustomerController.java | 2 ++ .../rbac/test/pac/TestPackageController.java | 2 ++ .../api-definition/api-definition.yaml | 15 +++++++- .../hsadminng/arch/ArchitectureTest.java | 28 ++++++++++++++- .../WebSecurityConfigIntegrationTest.java | 25 +++++++++++--- 27 files changed, 143 insertions(+), 22 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/config/WebSecurityConfig.java b/src/main/java/net/hostsharing/hsadminng/config/WebSecurityConfig.java index 92ab5f2c..8a89dd52 100644 --- a/src/main/java/net/hostsharing/hsadminng/config/WebSecurityConfig.java +++ b/src/main/java/net/hostsharing/hsadminng/config/WebSecurityConfig.java @@ -8,6 +8,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Profile; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -18,6 +19,7 @@ import jakarta.servlet.http.HttpServletResponse; @Configuration @EnableWebSecurity +@EnableMethodSecurity(prePostEnabled = true) // Add this annotation // TODO.impl: securitySchemes should work in OpenAPI yaml, but the Spring templates seem not to support it @SecurityScheme(type = SecuritySchemeType.HTTP, name = "casTicket", scheme = "bearer", bearerFormat = "CAS ticket", description = "CAS ticket", in = SecuritySchemeIn.HEADER) public class WebSecurityConfig { @@ -35,12 +37,13 @@ public class WebSecurityConfig { return http .authorizeHttpRequests(authorize -> authorize .requestMatchers( + // only list endpoints implemented in libraries here "/swagger-ui/**", "/v3/api-docs/**", - "/actuator/**", - "/api/hs/hosting/asset-types/**" + "/actuator/**" + // otherwise use @PreAuthorize annotation at the controller class / endpoint method level ).permitAll() - .requestMatchers("/api/**").authenticated() + .requestMatchers("/api/**").permitAll() // controlled at method level .anyRequest().denyAll() ) .addFilterBefore(authenticationFilter, AuthenticationFilter.class) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextsController.java b/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextsController.java index f5ad666f..b61841dd 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextsController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextsController.java @@ -10,10 +10,13 @@ import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ContextResource import net.hostsharing.hsadminng.mapper.StrictMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; @RestController +@PreAuthorize("isAuthenticated()") @NoSecurityRequirement public class HsCredentialsContextsController implements ContextsApi { @@ -29,9 +32,11 @@ public class HsCredentialsContextsController implements ContextsApi { @Override @Transactional(readOnly = true) @Timed("app.credentials.contexts.getListOfLoginContexts") + @PreAuthorize("permitAll()") public ResponseEntity> getListOfContexts(final String assumedRoles) { - context.assumeRoles(assumedRoles); - + if (SecurityContextHolder.getContext().getAuthentication().isAuthenticated()) { + context.assumeRoles(assumedRoles); + } final var loginContexts = contextRepo.findAll(); final var result = mapper.mapList(loginContexts, ContextResource.class); return ResponseEntity.ok(result); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsController.java b/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsController.java index 64668ca1..52e52c7e 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsController.java @@ -27,6 +27,7 @@ import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity; import net.hostsharing.hsadminng.rbac.subject.RbacSubjectRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; @@ -38,6 +39,7 @@ import static java.util.Optional.ofNullable; import static java.util.Optional.of; @RestController +@PreAuthorize("isAuthenticated()") @SecurityRequirement(name = "casTicket") public class HsCredentialsController implements CredentialsApi { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java index 30733d53..077e1019 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java @@ -19,6 +19,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Profile; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; @@ -33,6 +34,7 @@ import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateR @RestController @Profile("!only-prod-schema") +@PreAuthorize("isAuthenticated()") @SecurityRequirement(name = "casTicket") public class HsBookingItemController implements HsBookingItemsApi { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectController.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectController.java index 3340dd1d..c47b9ab0 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectController.java @@ -12,6 +12,7 @@ import net.hostsharing.hsadminng.mapper.StrictMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Profile; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; @@ -23,6 +24,7 @@ import java.util.function.BiConsumer; @RestController @Profile("!only-prod-schema") +@PreAuthorize("isAuthenticated()") @SecurityRequirement(name = "casTicket") public class HsBookingProjectController implements HsBookingProjectsApi { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java index 31dd5610..c3266549 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java @@ -18,6 +18,7 @@ import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Profile; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; @@ -30,6 +31,7 @@ import java.util.function.BiConsumer; @RestController @Profile("!only-prod-schema") +@PreAuthorize("isAuthenticated()") @SecurityRequirement(name = "casTicket") public class HsHostingAssetController implements HsHostingAssetsApi { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java index ed842a11..09f5eb6c 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java @@ -7,6 +7,7 @@ import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetP import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource; import org.springframework.context.annotation.Profile; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.RestController; import java.util.List; @@ -15,10 +16,12 @@ import java.util.Map; @RestController @Profile("!only-prod-schema") +@PreAuthorize("isAuthenticated()") @NoSecurityRequirement public class HsHostingAssetPropsController implements HsHostingAssetPropsApi { @Override + @PreAuthorize("permitAll()") @Timed("app.hosting.assets.api.getListOfHostingAssetTypes") public ResponseEntity> getListOfHostingAssetTypes() { final var resource = HostingAssetEntityValidatorRegistry.types().stream() @@ -28,6 +31,7 @@ public class HsHostingAssetPropsController implements HsHostingAssetPropsApi { } @Override + @PreAuthorize("permitAll()") @Timed("app.hosting.assets.api.getListOfHostingAssetTypeProps") public ResponseEntity> getListOfHostingAssetTypeProps( final HsHostingAssetTypeResource assetType) { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountController.java index 90b306bc..e01bb455 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountController.java @@ -11,6 +11,7 @@ import org.iban4j.BicUtil; import org.iban4j.IbanUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; @@ -19,6 +20,7 @@ import java.util.List; import java.util.UUID; @RestController +@PreAuthorize("isAuthenticated()") @SecurityRequirement(name = "casTicket") public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java index 699a678b..ddcec488 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java @@ -10,6 +10,7 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContac import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactResource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; @@ -21,6 +22,7 @@ import java.util.UUID; import static net.hostsharing.hsadminng.errors.Validate.validate; @RestController +@PreAuthorize("isAuthenticated()") @SecurityRequirement(name = "casTicket") public class HsOfficeContactController implements HsOfficeContactsApi { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java index b65b560f..fc273f75 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java @@ -17,6 +17,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat.ISO; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; @@ -38,6 +39,7 @@ import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOffic import static net.hostsharing.hsadminng.lambda.WithNonNull.withNonNull; @RestController +@PreAuthorize("isAuthenticated()") @SecurityRequirement(name = "casTicket") public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAssetsApi { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java index 2bacca2b..5fc1b969 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java @@ -14,6 +14,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat.ISO; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; @@ -29,6 +30,7 @@ import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOffic import static net.hostsharing.hsadminng.hs.validation.UuidResolver.resolve; @RestController +@PreAuthorize("isAuthenticated()") @SecurityRequirement(name = "casTicket") public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopSharesApi { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorController.java index 3e784a99..dbfc6a31 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorController.java @@ -17,6 +17,7 @@ import org.apache.commons.lang3.Validate; import org.hibernate.Hibernate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; @@ -33,6 +34,7 @@ import static net.hostsharing.hsadminng.hs.validation.UuidResolver.resolve; import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag; @RestController +@PreAuthorize("isAuthenticated()") @SecurityRequirement(name = "casTicket") public class HsOfficeDebitorController implements HsOfficeDebitorsApi { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipController.java index 72bac844..abed7d07 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipController.java @@ -12,6 +12,7 @@ import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRealRepository import net.hostsharing.hsadminng.mapper.StrictMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; @@ -25,6 +26,7 @@ import static net.hostsharing.hsadminng.errors.Validate.validate; import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag; @RestController +@PreAuthorize("isAuthenticated()") @SecurityRequirement(name = "casTicket") public class HsOfficeMembershipController implements HsOfficeMembershipsApi { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java index 2f5c0ea4..9c9acda0 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java @@ -22,6 +22,7 @@ import net.hostsharing.hsadminng.persistence.BaseEntity; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; @@ -37,6 +38,7 @@ import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType. import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag; @RestController +@PreAuthorize("isAuthenticated()") @SecurityRequirement(name = "casTicket") public class HsOfficePartnerController implements HsOfficePartnersApi { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonController.java index fac7bdff..b59d6b1d 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonController.java @@ -10,6 +10,7 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePerson import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonResource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; @@ -18,6 +19,7 @@ import java.util.List; import java.util.UUID; @RestController +@PreAuthorize("isAuthenticated()") @SecurityRequirement(name = "casTicket") public class HsOfficePersonController implements HsOfficePersonsApi { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationController.java index e93cb343..24f5ef2e 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationController.java @@ -13,6 +13,7 @@ import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealRepository; import net.hostsharing.hsadminng.mapper.StrictMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; @@ -27,6 +28,7 @@ import java.util.function.BiConsumer; import static net.hostsharing.hsadminng.mapper.KeyValueMap.from; @RestController +@PreAuthorize("isAuthenticated()") @SecurityRequirement(name = "casTicket") public class HsOfficeRelationController implements HsOfficeRelationsApi { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateController.java index 3b59ff3d..c320fbc4 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateController.java @@ -13,6 +13,7 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMa import net.hostsharing.hsadminng.mapper.StrictMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; @@ -25,6 +26,7 @@ import java.util.function.BiConsumer; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange; @RestController +@PreAuthorize("isAuthenticated()") @SecurityRequirement(name = "casTicket") public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi { diff --git a/src/main/java/net/hostsharing/hsadminng/ping/PingController.java b/src/main/java/net/hostsharing/hsadminng/ping/PingController.java index be639ab6..668defba 100644 --- a/src/main/java/net/hostsharing/hsadminng/ping/PingController.java +++ b/src/main/java/net/hostsharing/hsadminng/ping/PingController.java @@ -1,26 +1,38 @@ package net.hostsharing.hsadminng.ping; +import io.micrometer.core.annotation.Timed; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import net.hostsharing.hsadminng.config.MessageTranslator; +import net.hostsharing.hsadminng.generated.api.v1.api.TestApi; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; - -@Controller -public class PingController { +@RestController +@PreAuthorize("isAuthenticated()") +@SecurityRequirement(name = "casTicket") +public class PingController implements TestApi { @Autowired private MessageTranslator messageTranslator; - @ResponseBody - @RequestMapping(value = "/api/ping", method = RequestMethod.GET) - public String ping() { + @PreAuthorize("permitAll()") + @Timed("app.api.ping") + public ResponseEntity ping() { final var userName = SecurityContextHolder.getContext().getAuthentication().getName(); // HOWTO translate text with placeholders - also see in resource files i18n/messages_*.properties. final var translatedMessage = messageTranslator.translate("pong {0} - in English", userName); - return translatedMessage + "\n"; + return ResponseEntity.ok(translatedMessage + "\n"); + } + + @PreAuthorize("isAuthenticated()") + @Timed("app.api.pong") + public ResponseEntity pong() { + final var userName = SecurityContextHolder.getContext().getAuthentication().getName(); + // HOWTO translate text with placeholders - also see in resource files i18n/messages_*.properties. + final var translatedMessage = messageTranslator.translate("ping {0} - in English", userName); + return ResponseEntity.ok(translatedMessage + "\n"); } } diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/context/RbacContextController.java b/src/main/java/net/hostsharing/hsadminng/rbac/context/RbacContextController.java index 1bc1a348..9392aa96 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/context/RbacContextController.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/context/RbacContextController.java @@ -14,6 +14,7 @@ import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity; import net.hostsharing.hsadminng.rbac.subject.RbacSubjectRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; @@ -21,6 +22,7 @@ import java.util.List; import java.util.UUID; @RestController +@PreAuthorize("isAuthenticated()") @SecurityRequirement(name = "casTicket") public class RbacContextController implements RbacContextApi { diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/grant/RbacGrantController.java b/src/main/java/net/hostsharing/hsadminng/rbac/grant/RbacGrantController.java index e265fb79..82c336f2 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/grant/RbacGrantController.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/grant/RbacGrantController.java @@ -8,6 +8,7 @@ import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacGrantsApi; import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacGrantResource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; @@ -18,6 +19,7 @@ import java.util.List; import java.util.UUID; @RestController +@PreAuthorize("isAuthenticated()") @SecurityRequirement(name = "casTicket") public class RbacGrantController implements RbacGrantsApi { diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/role/RbacRoleController.java b/src/main/java/net/hostsharing/hsadminng/rbac/role/RbacRoleController.java index aa7e7676..6ecf235a 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/role/RbacRoleController.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/role/RbacRoleController.java @@ -8,12 +8,14 @@ import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacRolesApi; import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacRoleResource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController +@PreAuthorize("isAuthenticated()") @SecurityRequirement(name = "casTicket") public class RbacRoleController implements RbacRolesApi { diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/subject/RbacSubjectController.java b/src/main/java/net/hostsharing/hsadminng/rbac/subject/RbacSubjectController.java index 47ceb2d7..de1a7411 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/subject/RbacSubjectController.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/subject/RbacSubjectController.java @@ -9,6 +9,7 @@ import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacSubjectPermissi import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacSubjectResource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; @@ -17,6 +18,7 @@ import java.util.List; import java.util.UUID; @RestController +@PreAuthorize("isAuthenticated()") @SecurityRequirement(name = "casTicket") public class RbacSubjectController implements RbacSubjectsApi { @@ -31,6 +33,7 @@ public class RbacSubjectController implements RbacSubjectsApi { @Override @Transactional + @PreAuthorize("permitAll()") @Timed("app.rbac.subjects.api.postNewSubject") public ResponseEntity postNewSubject( final RbacSubjectResource body diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/test/cust/TestCustomerController.java b/src/main/java/net/hostsharing/hsadminng/rbac/test/cust/TestCustomerController.java index 82e9876e..16e6e7f5 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/test/cust/TestCustomerController.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/test/cust/TestCustomerController.java @@ -7,6 +7,7 @@ import net.hostsharing.hsadminng.test.generated.api.v1.api.TestCustomersApi; import net.hostsharing.hsadminng.test.generated.api.v1.model.TestCustomerResource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; @@ -16,6 +17,7 @@ import jakarta.persistence.PersistenceContext; import java.util.List; @RestController +@PreAuthorize("isAuthenticated()") @SecurityRequirement(name = "casTicket") public class TestCustomerController implements TestCustomersApi { diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageController.java b/src/main/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageController.java index c9459fcf..1eed7c6a 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageController.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageController.java @@ -9,6 +9,7 @@ import net.hostsharing.hsadminng.test.generated.api.v1.model.TestPackageResource import net.hostsharing.hsadminng.test.generated.api.v1.model.TestPackageUpdateResource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; @@ -16,6 +17,7 @@ import java.util.List; import java.util.UUID; @RestController +@PreAuthorize("isAuthenticated()") @SecurityRequirement(name = "casTicket") public class TestPackageController implements TestPackagesApi { diff --git a/src/main/resources/api-definition/api-definition.yaml b/src/main/resources/api-definition/api-definition.yaml index a81ef222..17cb76d6 100644 --- a/src/main/resources/api-definition/api-definition.yaml +++ b/src/main/resources/api-definition/api-definition.yaml @@ -17,6 +17,19 @@ paths: "200": description: OK content: - 'application/json': + 'text/plain': + schema: + type: string + + /api/pong: + get: + tags: + - test + operationId: pong + responses: + "200": + description: OK + content: + 'text/plain': schema: type: string diff --git a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java index 13cf9709..811b1cbe 100644 --- a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java +++ b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java @@ -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 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 hasStaticMethodNamed(final String expectedName) { return new DescribedPredicate<>("rbac entity") { @Override diff --git a/src/test/java/net/hostsharing/hsadminng/config/WebSecurityConfigIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/config/WebSecurityConfigIntegrationTest.java index bd88e0f8..0210c00e 100644 --- a/src/test/java/net/hostsharing/hsadminng/config/WebSecurityConfigIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/config/WebSecurityConfigIntegrationTest.java @@ -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