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
@@ -8,6 +8,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Profile; 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.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
@@ -18,6 +19,7 @@ import jakarta.servlet.http.HttpServletResponse;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true) // Add this annotation
// TODO.impl: securitySchemes should work in OpenAPI yaml, but the Spring templates seem not to support it // 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) @SecurityScheme(type = SecuritySchemeType.HTTP, name = "casTicket", scheme = "bearer", bearerFormat = "CAS ticket", description = "CAS ticket", in = SecuritySchemeIn.HEADER)
public class WebSecurityConfig { public class WebSecurityConfig {
@@ -35,12 +37,13 @@ public class WebSecurityConfig {
return http return http
.authorizeHttpRequests(authorize -> authorize .authorizeHttpRequests(authorize -> authorize
.requestMatchers( .requestMatchers(
// only list endpoints implemented in libraries here
"/swagger-ui/**", "/swagger-ui/**",
"/v3/api-docs/**", "/v3/api-docs/**",
"/actuator/**", "/actuator/**"
"/api/hs/hosting/asset-types/**" // otherwise use @PreAuthorize annotation at the controller class / endpoint method level
).permitAll() ).permitAll()
.requestMatchers("/api/**").authenticated() .requestMatchers("/api/**").permitAll() // controlled at method level
.anyRequest().denyAll() .anyRequest().denyAll()
) )
.addFilterBefore(authenticationFilter, AuthenticationFilter.class) .addFilterBefore(authenticationFilter, AuthenticationFilter.class)
@@ -10,10 +10,13 @@ import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ContextResource
import net.hostsharing.hsadminng.mapper.StrictMapper; import net.hostsharing.hsadminng.mapper.StrictMapper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; 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.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@RestController @RestController
@PreAuthorize("isAuthenticated()")
@NoSecurityRequirement @NoSecurityRequirement
public class HsCredentialsContextsController implements ContextsApi { public class HsCredentialsContextsController implements ContextsApi {
@@ -29,9 +32,11 @@ public class HsCredentialsContextsController implements ContextsApi {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
@Timed("app.credentials.contexts.getListOfLoginContexts") @Timed("app.credentials.contexts.getListOfLoginContexts")
@PreAuthorize("permitAll()")
public ResponseEntity<List<ContextResource>> getListOfContexts(final String assumedRoles) { public ResponseEntity<List<ContextResource>> getListOfContexts(final String assumedRoles) {
if (SecurityContextHolder.getContext().getAuthentication().isAuthenticated()) {
context.assumeRoles(assumedRoles); context.assumeRoles(assumedRoles);
}
final var loginContexts = contextRepo.findAll(); final var loginContexts = contextRepo.findAll();
final var result = mapper.mapList(loginContexts, ContextResource.class); final var result = mapper.mapList(loginContexts, ContextResource.class);
return ResponseEntity.ok(result); return ResponseEntity.ok(result);
@@ -27,6 +27,7 @@ import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity;
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectRepository; import net.hostsharing.hsadminng.rbac.subject.RbacSubjectRepository;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; 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; import static java.util.Optional.of;
@RestController @RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket") @SecurityRequirement(name = "casTicket")
public class HsCredentialsController implements CredentialsApi { public class HsCredentialsController implements CredentialsApi {
@@ -19,6 +19,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
@@ -33,6 +34,7 @@ import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateR
@RestController @RestController
@Profile("!only-prod-schema") @Profile("!only-prod-schema")
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket") @SecurityRequirement(name = "casTicket")
public class HsBookingItemController implements HsBookingItemsApi { public class HsBookingItemController implements HsBookingItemsApi {
@@ -12,6 +12,7 @@ import net.hostsharing.hsadminng.mapper.StrictMapper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
@@ -23,6 +24,7 @@ import java.util.function.BiConsumer;
@RestController @RestController
@Profile("!only-prod-schema") @Profile("!only-prod-schema")
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket") @SecurityRequirement(name = "casTicket")
public class HsBookingProjectController implements HsBookingProjectsApi { public class HsBookingProjectController implements HsBookingProjectsApi {
@@ -18,6 +18,7 @@ import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
@@ -30,6 +31,7 @@ import java.util.function.BiConsumer;
@RestController @RestController
@Profile("!only-prod-schema") @Profile("!only-prod-schema")
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket") @SecurityRequirement(name = "casTicket")
public class HsHostingAssetController implements HsHostingAssetsApi { public class HsHostingAssetController implements HsHostingAssetsApi {
@@ -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 net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
@@ -15,10 +16,12 @@ import java.util.Map;
@RestController @RestController
@Profile("!only-prod-schema") @Profile("!only-prod-schema")
@PreAuthorize("isAuthenticated()")
@NoSecurityRequirement @NoSecurityRequirement
public class HsHostingAssetPropsController implements HsHostingAssetPropsApi { public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
@Override @Override
@PreAuthorize("permitAll()")
@Timed("app.hosting.assets.api.getListOfHostingAssetTypes") @Timed("app.hosting.assets.api.getListOfHostingAssetTypes")
public ResponseEntity<List<String>> getListOfHostingAssetTypes() { public ResponseEntity<List<String>> getListOfHostingAssetTypes() {
final var resource = HostingAssetEntityValidatorRegistry.types().stream() final var resource = HostingAssetEntityValidatorRegistry.types().stream()
@@ -28,6 +31,7 @@ public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
} }
@Override @Override
@PreAuthorize("permitAll()")
@Timed("app.hosting.assets.api.getListOfHostingAssetTypeProps") @Timed("app.hosting.assets.api.getListOfHostingAssetTypeProps")
public ResponseEntity<List<Object>> getListOfHostingAssetTypeProps( public ResponseEntity<List<Object>> getListOfHostingAssetTypeProps(
final HsHostingAssetTypeResource assetType) { final HsHostingAssetTypeResource assetType) {
@@ -11,6 +11,7 @@ import org.iban4j.BicUtil;
import org.iban4j.IbanUtil; import org.iban4j.IbanUtil;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
@@ -19,6 +20,7 @@ import java.util.List;
import java.util.UUID; import java.util.UUID;
@RestController @RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket") @SecurityRequirement(name = "casTicket")
public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi { public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
@@ -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 net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactResource;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; 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; import static net.hostsharing.hsadminng.errors.Validate.validate;
@RestController @RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket") @SecurityRequirement(name = "casTicket")
public class HsOfficeContactController implements HsOfficeContactsApi { public class HsOfficeContactController implements HsOfficeContactsApi {
@@ -17,6 +17,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.DateTimeFormat.ISO; import org.springframework.format.annotation.DateTimeFormat.ISO;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; 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; import static net.hostsharing.hsadminng.lambda.WithNonNull.withNonNull;
@RestController @RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket") @SecurityRequirement(name = "casTicket")
public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAssetsApi { public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAssetsApi {
@@ -14,6 +14,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.DateTimeFormat.ISO; import org.springframework.format.annotation.DateTimeFormat.ISO;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; 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; import static net.hostsharing.hsadminng.hs.validation.UuidResolver.resolve;
@RestController @RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket") @SecurityRequirement(name = "casTicket")
public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopSharesApi { public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopSharesApi {
@@ -17,6 +17,7 @@ import org.apache.commons.lang3.Validate;
import org.hibernate.Hibernate; import org.hibernate.Hibernate;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; 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; import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag;
@RestController @RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket") @SecurityRequirement(name = "casTicket")
public class HsOfficeDebitorController implements HsOfficeDebitorsApi { public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
@@ -12,6 +12,7 @@ import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRealRepository
import net.hostsharing.hsadminng.mapper.StrictMapper; import net.hostsharing.hsadminng.mapper.StrictMapper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; 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; import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag;
@RestController @RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket") @SecurityRequirement(name = "casTicket")
public class HsOfficeMembershipController implements HsOfficeMembershipsApi { public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
@@ -22,6 +22,7 @@ import net.hostsharing.hsadminng.persistence.BaseEntity;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; 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; import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag;
@RestController @RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket") @SecurityRequirement(name = "casTicket")
public class HsOfficePartnerController implements HsOfficePartnersApi { public class HsOfficePartnerController implements HsOfficePartnersApi {
@@ -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 net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonResource;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
@@ -18,6 +19,7 @@ import java.util.List;
import java.util.UUID; import java.util.UUID;
@RestController @RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket") @SecurityRequirement(name = "casTicket")
public class HsOfficePersonController implements HsOfficePersonsApi { public class HsOfficePersonController implements HsOfficePersonsApi {
@@ -13,6 +13,7 @@ import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealRepository;
import net.hostsharing.hsadminng.mapper.StrictMapper; import net.hostsharing.hsadminng.mapper.StrictMapper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; 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; import static net.hostsharing.hsadminng.mapper.KeyValueMap.from;
@RestController @RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket") @SecurityRequirement(name = "casTicket")
public class HsOfficeRelationController implements HsOfficeRelationsApi { public class HsOfficeRelationController implements HsOfficeRelationsApi {
@@ -13,6 +13,7 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMa
import net.hostsharing.hsadminng.mapper.StrictMapper; import net.hostsharing.hsadminng.mapper.StrictMapper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; 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; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
@RestController @RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket") @SecurityRequirement(name = "casTicket")
public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi { public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
@@ -1,26 +1,38 @@
package net.hostsharing.hsadminng.ping; 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.config.MessageTranslator;
import net.hostsharing.hsadminng.generated.api.v1.api.TestApi;
import org.springframework.beans.factory.annotation.Autowired; 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.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@RestController
@Controller @PreAuthorize("isAuthenticated()")
public class PingController { @SecurityRequirement(name = "casTicket")
public class PingController implements TestApi {
@Autowired @Autowired
private MessageTranslator messageTranslator; private MessageTranslator messageTranslator;
@ResponseBody @PreAuthorize("permitAll()")
@RequestMapping(value = "/api/ping", method = RequestMethod.GET) @Timed("app.api.ping")
public String ping() { public ResponseEntity<String> ping() {
final var userName = SecurityContextHolder.getContext().getAuthentication().getName(); final var userName = SecurityContextHolder.getContext().getAuthentication().getName();
// HOWTO translate text with placeholders - also see in resource files i18n/messages_*.properties. // HOWTO translate text with placeholders - also see in resource files i18n/messages_*.properties.
final var translatedMessage = messageTranslator.translate("pong {0} - in English", userName); 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<String> 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");
} }
} }
@@ -14,6 +14,7 @@ import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity;
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectRepository; import net.hostsharing.hsadminng.rbac.subject.RbacSubjectRepository;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@@ -21,6 +22,7 @@ import java.util.List;
import java.util.UUID; import java.util.UUID;
@RestController @RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket") @SecurityRequirement(name = "casTicket")
public class RbacContextController implements RbacContextApi { public class RbacContextController implements RbacContextApi {
@@ -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 net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacGrantResource;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
@@ -18,6 +19,7 @@ import java.util.List;
import java.util.UUID; import java.util.UUID;
@RestController @RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket") @SecurityRequirement(name = "casTicket")
public class RbacGrantController implements RbacGrantsApi { public class RbacGrantController implements RbacGrantsApi {
@@ -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 net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacRoleResource;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
@RestController @RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket") @SecurityRequirement(name = "casTicket")
public class RbacRoleController implements RbacRolesApi { public class RbacRoleController implements RbacRolesApi {
@@ -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 net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacSubjectResource;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
@@ -17,6 +18,7 @@ import java.util.List;
import java.util.UUID; import java.util.UUID;
@RestController @RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket") @SecurityRequirement(name = "casTicket")
public class RbacSubjectController implements RbacSubjectsApi { public class RbacSubjectController implements RbacSubjectsApi {
@@ -31,6 +33,7 @@ public class RbacSubjectController implements RbacSubjectsApi {
@Override @Override
@Transactional @Transactional
@PreAuthorize("permitAll()")
@Timed("app.rbac.subjects.api.postNewSubject") @Timed("app.rbac.subjects.api.postNewSubject")
public ResponseEntity<RbacSubjectResource> postNewSubject( public ResponseEntity<RbacSubjectResource> postNewSubject(
final RbacSubjectResource body final RbacSubjectResource body
@@ -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 net.hostsharing.hsadminng.test.generated.api.v1.model.TestCustomerResource;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
@@ -16,6 +17,7 @@ import jakarta.persistence.PersistenceContext;
import java.util.List; import java.util.List;
@RestController @RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket") @SecurityRequirement(name = "casTicket")
public class TestCustomerController implements TestCustomersApi { public class TestCustomerController implements TestCustomersApi {
@@ -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 net.hostsharing.hsadminng.test.generated.api.v1.model.TestPackageUpdateResource;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@@ -16,6 +17,7 @@ import java.util.List;
import java.util.UUID; import java.util.UUID;
@RestController @RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket") @SecurityRequirement(name = "casTicket")
public class TestPackageController implements TestPackagesApi { public class TestPackageController implements TestPackagesApi {
@@ -17,6 +17,19 @@ paths:
"200": "200":
description: OK description: OK
content: content:
'application/json': 'text/plain':
schema:
type: string
/api/pong:
get:
tags:
- test
operationId: pong
responses:
"200":
description: OK
content:
'text/plain':
schema: schema:
type: string type: string
@@ -20,6 +20,7 @@ import net.hostsharing.hsadminng.rbac.context.ContextBasedTest;
import net.hostsharing.hsadminng.rbac.grant.RbacGrantsDiagramService; import net.hostsharing.hsadminng.rbac.grant.RbacGrantsDiagramService;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.repository.Repository; import org.springframework.data.repository.Repository;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
@@ -369,6 +370,32 @@ public class ArchitectureTest {
.beAnnotatedWith(SecurityRequirement.class).orShould() .beAnnotatedWith(SecurityRequirement.class).orShould()
.beAnnotatedWith(NoSecurityRequirement.class); .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 @ArchTest
@SuppressWarnings("unused") @SuppressWarnings("unused")
static final ArchRule restControllerMethods = classes() static final ArchRule restControllerMethods = classes()
@@ -398,7 +425,6 @@ public class ArchitectureTest {
.should(haveTableNameEndingWith_rv()) .should(haveTableNameEndingWith_rv())
.because("it's required that the table names of RBAC entities end with '_rv'"); .because("it's required that the table names of RBAC entities end with '_rv'");
private static DescribedPredicate<JavaMethod> hasStaticMethodNamed(final String expectedName) { private static DescribedPredicate<JavaMethod> hasStaticMethodNamed(final String expectedName) {
return new DescribedPredicate<>("rbac entity") { return new DescribedPredicate<>("rbac entity") {
@Override @Override
@@ -113,20 +113,37 @@ class WebSecurityConfigIntegrationTest {
} }
@Test @Test
void accessToApiWithoutTokenShouldBeDenied() { void accessToPingApiWithoutTokenShouldBePermitted() {
final var result = this.restTemplate.getForEntity( final var result = this.restTemplate.getForEntity(
"http://localhost:" + this.serverPort + "/api/ping", String.class); "http://localhost:" + this.serverPort + "/api/ping", String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
} }
@Test @Test
void accessToApiWithInvalidTokenShouldBeDenied() { void accessToPongApiWithValidTokenShouldBePermitted() {
// given // given
givenCasTicketValidationResponse("ST-fake-cas-ticket", "fake-user-name"); givenCasTicketValidationResponse("ST-fake-cas-ticket", "fake-user-name");
// when // when
final var result = restTemplate.exchange( 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, HttpMethod.GET,
httpHeaders(entry("Authorization", "Bearer ST-WRONG-cas-ticket")), httpHeaders(entry("Authorization", "Bearer ST-WRONG-cas-ticket")),
String.class String.class