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.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)
@@ -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<List<ContextResource>> 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);
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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<List<String>> 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<List<Object>> getListOfHostingAssetTypeProps(
final HsHostingAssetTypeResource assetType) {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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<String> 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<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 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 {
@@ -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 {
@@ -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 {
@@ -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<RbacSubjectResource> postNewSubject(
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 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 {
@@ -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 {
@@ -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