1
0

migrate from CAS to Oauth2-JWT Auth (#197)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/197
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
Michael Hoennig
2025-09-08 15:27:28 +02:00
parent bc06001ce9
commit d7a78d0a79
125 changed files with 1537 additions and 1549 deletions
@@ -0,0 +1,78 @@
package net.hostsharing.hsadminng.config;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.security.config.Customizer;
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;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.web.SecurityFilterChain;
import jakarta.servlet.http.HttpServletResponse;
import static net.hostsharing.hsadminng.config.JwtFakeBearer.RSA_KEY;
@Configuration
@EnableWebSecurity
// securitySchemes should work in OpenAPI yaml, but the Spring templates seem not to support it
@SecurityScheme(
name = "bearerAuth",
type = SecuritySchemeType.HTTP,
scheme = "bearer",
bearerFormat = "JWT"
)
public abstract class BaseWebSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(
// only list endpoints implemented in libraries here
"/swagger-ui/**",
"/v3/api-docs/**",
"/actuator/**",
"/fake-jwt/**"
// otherwise use @PreAuthorize annotation at the controller class / endpoint method level
).permitAll()
.requestMatchers("/api/**").permitAll() // controlled at method level
.anyRequest().denyAll()
)
.oauth2ResourceServer(oauth ->
oauth.jwt(Customizer.withDefaults()))
.csrf(AbstractHttpConfigurer::disable)
.cors(AbstractHttpConfigurer::disable)
.exceptionHandling(exception -> exception
.authenticationEntryPoint((request, response, authException) ->
// For unknown reason Spring security returns 403 FORBIDDEN for a BadCredentialsException.
// But it should return 401 UNAUTHORIZED.
response.sendError(HttpServletResponse.SC_UNAUTHORIZED)
)
)
.build();
}
@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri:http://localhost:${server.port}/fake-jwt/.well-known/jwks.json}")
private String jwkSetUri;
@Bean
@Profile("!fake-jwt")
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
}
@Bean
@Profile("fake-jwt")
@SneakyThrows
public JwtDecoder fakeJwtDecoder() {
// For fake-jwt profile, use the same RSA key as JwtFakeBearer
return NimbusJwtDecoder.withPublicKey(RSA_KEY.toRSAPublicKey()).build();
}
}
@@ -1,36 +0,0 @@
package net.hostsharing.hsadminng.config;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
// Do NOT use @Component (or similar) here, this would register the filter directly.
// But we need to register it in the SecurityFilterChain created by WebSecurityConfig.
// The bean gets created in net.hostsharing.hsadminng.config.WebSecurityConfig.authenticationFilter.
@AllArgsConstructor
public class CasAuthenticationFilter extends OncePerRequestFilter {
private CasAuthenticator authenticator;
@Override
@SneakyThrows
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
if (request.getHeader("authorization") != null) {
final var authenticatedRequest = new AuthenticatedHttpServletRequestWrapper(request);
final var currentSubject = authenticator.authenticate(request);
final var authentication = new UsernamePasswordAuthenticationToken(currentSubject, null, null);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(authenticatedRequest, response);
} else {
filterChain.doFilter(request, response);
}
}
}
@@ -1,8 +0,0 @@
package net.hostsharing.hsadminng.config;
import jakarta.servlet.http.HttpServletRequest;
public interface CasAuthenticator {
String authenticate(final HttpServletRequest httpRequest);
}
@@ -1,14 +0,0 @@
package net.hostsharing.hsadminng.config;
import lombok.SneakyThrows;
import jakarta.servlet.http.HttpServletRequest;
public class FakeCasAuthenticator implements CasAuthenticator {
@Override
@SneakyThrows
public String authenticate(final HttpServletRequest httpRequest) {
return httpRequest.getHeader("Authorization").replaceAll("^Bearer ", "");
}
}
@@ -0,0 +1,40 @@
package net.hostsharing.hsadminng.config;
import io.micrometer.core.annotation.Timed;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Profile;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@Profile("fake-jwt")
@NoSecurityRequirement
@Slf4j
public class FakeJwtController {
@PostMapping(value = "/fake-jwt/token", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
@Timed("app.config.jwt.token")
public ResponseEntity<Map<String, Object>> token(
@RequestParam String username,
@RequestParam String password,
@RequestParam(defaultValue = "openid profile") String scope) {
log.info("Fake JWT: Issuing token for user: {}", username);
// Accept any username/password for local testing
String token = JwtFakeBearer.bearer(username).replace("Bearer ", "");
return ResponseEntity.ok(Map.of(
"access_token", token,
"token_type", "Bearer",
"expires_in", 3600,
"scope", scope
));
}
}
@@ -11,18 +11,16 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
@Configuration
public class JsonObjectMapperConfiguration {
public static ObjectMapper build() {
return new JsonObjectMapperConfiguration().customObjectMapper().build();
return new JsonObjectMapperConfiguration().customObjectMapper();
}
@Bean
@Primary
public Jackson2ObjectMapperBuilder customObjectMapper() {
// HOWTO: add JSON converters and specify other JSON mapping configurations
public ObjectMapper customObjectMapper() {
return new Jackson2ObjectMapperBuilder()
.modules(new JsonNullableModule(), new JavaTimeModule())
.featuresToEnable(
@@ -30,6 +28,7 @@ public class JsonObjectMapperConfiguration {
DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS,
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.build();
}
}
@@ -0,0 +1,42 @@
package net.hostsharing.hsadminng.config;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import lombok.SneakyThrows;
import lombok.val;
import java.util.Date;
/**
* Provides a fake JWT bearer generator.
*/
public class JwtFakeBearer {
public static final RSAKey RSA_KEY = generateRSAKey(2048, "test-key");
@SneakyThrows
public static String bearer(final String subject) {
val claims = new JWTClaimsSet.Builder()
.subject(subject)
.issuer("http://test-issuer")
.audience("api")
.expirationTime(new Date(System.currentTimeMillis() + 3600_000))
.build();
val signed = new SignedJWT(
new JWSHeader.Builder(JWSAlgorithm.RS256)
.keyID(RSA_KEY.getKeyID()).build(), claims);
signed.sign(new RSASSASigner(RSA_KEY.toPrivateKey()));
return "Bearer " + signed.serialize();
}
@SneakyThrows
private static RSAKey generateRSAKey(final int size, final String keyID) {
return new RSAKeyGenerator(size).keyID(keyID).generate();
}
}
@@ -1,86 +0,0 @@
package net.hostsharing.hsadminng.config;
import io.micrometer.core.annotation.Timed;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
import jakarta.servlet.http.HttpServletRequest;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
// HOWTO add logger
@Slf4j
@RequiredArgsConstructor
public class RealCasAuthenticator implements CasAuthenticator {
@Value("${hsadminng.cas.server}")
private String casServerUrl;
@Value("${hsadminng.cas.service}")
private String serviceUrl;
private final MessageTranslator messageTranslator;
private final RestTemplate restTemplate = new RestTemplate();
@SneakyThrows
@Timed("app.cas.authenticate")
public String authenticate(final HttpServletRequest httpRequest) {
final var ticket = httpRequest.getHeader("authorization").replaceAll("^Bearer ", "");
final var serviceTicket = ticket.startsWith("TGT-")
? fetchServiceTicket(ticket)
: ticket;
final var userName = extractUserName(verifyServiceTicket(serviceTicket));
// HOWTO log some message for a certain log level (trace, debug, info, warn, error)
log.debug("CAS-user: {}", userName);
return userName;
}
private String fetchServiceTicket(final String ticketGrantingTicket) {
final var tgtUrl = casServerUrl + "/cas/v1/tickets/" + ticketGrantingTicket;
final var restTemplate = new RestTemplate();
final var formData = new LinkedMultiValueMap<String, String>();
formData.add("service", serviceUrl);
return restTemplate.postForObject(tgtUrl, formData, String.class);
}
private Document verifyServiceTicket(final String serviceTicket) throws SAXException, IOException, ParserConfigurationException {
if ( !serviceTicket.startsWith("ST-") ) {
throwBadCredentialsException("auth.unknown-authorization-ticket");
}
final var url = casServerUrl + "/cas/p3/serviceValidate" +
"?service=" + serviceUrl +
"&ticket=" + serviceTicket;
final var response = restTemplate.getForObject(url, String.class);
return DocumentBuilderFactory.newInstance().newDocumentBuilder()
.parse(new java.io.ByteArrayInputStream(response.getBytes()));
}
private String extractUserName(final Document verification) {
if (verification.getElementsByTagName("cas:authenticationSuccess").getLength() == 0) {
throwBadCredentialsException("auth.cas-service-ticket-could-not-be-verified");
}
return verification.getElementsByTagName("cas:user").item(0).getTextContent();
}
private void throwBadCredentialsException(final String messageKey) {
final var translatedMessage = messageTranslator.translate(messageKey);
throw new BadCredentialsException(translatedMessage);
}
}
@@ -1,77 +1,12 @@
package net.hostsharing.hsadminng.config;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import org.springframework.beans.factory.annotation.Autowired;
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;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFilter;
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 {
@Lazy
@Autowired
private CasAuthenticationFilter authenticationFilter;
@Autowired
private MessageTranslator messageTranslator;
@Bean
@Profile("!test")
public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(
// only list endpoints implemented in libraries here
"/swagger-ui/**",
"/v3/api-docs/**",
"/actuator/**"
// otherwise use @PreAuthorize annotation at the controller class / endpoint method level
).permitAll()
.requestMatchers("/api/**").permitAll() // controlled at method level
.anyRequest().denyAll()
)
.addFilterBefore(authenticationFilter, AuthenticationFilter.class)
.csrf(AbstractHttpConfigurer::disable)
.exceptionHandling(exception -> exception
.authenticationEntryPoint((request, response, authException) ->
// For unknown reasons Spring security returns 403 FORBIDDEN for a BadCredentialsException.
// But it should return 401 UNAUTHORIZED.
response.sendError(HttpServletResponse.SC_UNAUTHORIZED)
)
)
.build();
}
@Bean
@Profile("realCasAuthenticator")
public CasAuthenticator realCasServiceTicketValidator() {
return new RealCasAuthenticator(messageTranslator);
}
@Bean
@Profile("fakeCasAuthenticator")
public CasAuthenticator fakeCasServiceTicketValidator() {
return new FakeCasAuthenticator();
}
@Bean
public CasAuthenticationFilter authenticationFilter(final CasAuthenticator authenticator) {
return new CasAuthenticationFilter(authenticator);
}
@Profile("!test")
@EnableMethodSecurity // this does not work with @WebMvcTest, see WebSecurityConfigForWebMvcTests
public class WebSecurityConfig extends BaseWebSecurityConfig {
}
@@ -5,19 +5,17 @@ import java.util.List;
import io.micrometer.core.annotation.Timed;
import lombok.val;
import net.hostsharing.hsadminng.config.NoSecurityRequirement;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.context.Context;
import net.hostsharing.hsadminng.accounts.generated.api.v1.api.ContextsApi;
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 {
@@ -33,7 +31,6 @@ public class HsCredentialsContextsController implements ContextsApi {
@Override
@Transactional(readOnly = true)
@Timed("app.credentials.contexts.getListOfLoginContexts")
@PreAuthorize("permitAll()")
public ResponseEntity<List<ContextResource>> getListOfContexts(final String assumedRoles) {
if (SecurityContextHolder.getContext().getAuthentication().isAuthenticated()) {
context.assumeRoles(assumedRoles);
@@ -14,7 +14,7 @@ import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ContextResource
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.CurrentLoginUserResource;
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.RbacSubjectResource;
import net.hostsharing.hsadminng.config.MessageTranslator;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.context.Context;
import net.hostsharing.hsadminng.accounts.generated.api.v1.api.CredentialsApi;
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.CredentialsInsertResource;
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.CredentialsPatchResource;
@@ -42,7 +42,7 @@ import static java.util.Optional.of;
@RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket")
@SecurityRequirement(name = "bearerAuth")
public class HsCredentialsController implements CredentialsApi {
@Autowired
@@ -4,7 +4,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.context.Context;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingItemsApi;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemInsertResource;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource;
@@ -35,7 +35,7 @@ import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateR
@RestController
@Profile("!only-prod-schema")
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket")
@SecurityRequirement(name = "bearerAuth")
public class HsBookingItemController implements HsBookingItemsApi {
@Autowired
@@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.hs.booking.project;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.context.Context;
import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorRepository;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingProjectsApi;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectInsertResource;
@@ -25,7 +25,7 @@ import java.util.function.BiConsumer;
@RestController
@Profile("!only-prod-schema")
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket")
@SecurityRequirement(name = "bearerAuth")
public class HsBookingProjectController implements HsBookingProjectsApi {
@Autowired
@@ -7,7 +7,7 @@ import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntityS
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntityValidatorRegistry;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetsApi;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.context.Context;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetInsertResource;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetPatchResource;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetResource;
@@ -32,7 +32,7 @@ import java.util.function.BiConsumer;
@RestController
@Profile("!only-prod-schema")
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket")
@SecurityRequirement(name = "bearerAuth")
public class HsHostingAssetController implements HsHostingAssetsApi {
@Autowired
@@ -7,7 +7,6 @@ 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;
@@ -16,12 +15,10 @@ 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()
@@ -31,7 +28,6 @@ public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
}
@Override
@PreAuthorize("permitAll()")
@Timed("app.hosting.assets.api.getListOfHostingAssetTypeProps")
public ResponseEntity<List<Object>> getListOfHostingAssetTypeProps(
final HsHostingAssetTypeResource assetType) {
@@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.hs.office.bankaccount;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.context.Context;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeBankAccountsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountResource;
@@ -21,7 +21,7 @@ import java.util.UUID;
@RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket")
@SecurityRequirement(name = "bearerAuth")
public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
@Autowired
@@ -3,7 +3,7 @@ package net.hostsharing.hsadminng.hs.office.contact;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.context.Context;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeContactsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactPatchResource;
@@ -23,7 +23,7 @@ import static net.hostsharing.hsadminng.errors.Validate.validate;
@RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket")
@SecurityRequirement(name = "bearerAuth")
public class HsOfficeContactController implements HsOfficeContactsApi {
@Autowired
@@ -3,7 +3,7 @@ package net.hostsharing.hsadminng.hs.office.coopassets;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.config.MessageTranslator;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.context.Context;
import net.hostsharing.hsadminng.errors.MultiValidationException;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopAssetsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionInsertResource;
@@ -40,7 +40,7 @@ import static net.hostsharing.hsadminng.lambda.WithNonNull.withNonNull;
@RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket")
@SecurityRequirement(name = "bearerAuth")
public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAssetsApi {
@Autowired
@@ -3,7 +3,7 @@ package net.hostsharing.hsadminng.hs.office.coopshares;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.config.MessageTranslator;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.context.Context;
import net.hostsharing.hsadminng.errors.MultiValidationException;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopSharesApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionInsertResource;
@@ -31,7 +31,7 @@ import static net.hostsharing.hsadminng.hs.validation.UuidResolver.resolve;
@RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket")
@SecurityRequirement(name = "bearerAuth")
public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopSharesApi {
@Autowired
@@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.hs.office.debitor;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.context.Context;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountRepository;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealRepository;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeDebitorsApi;
@@ -35,7 +35,7 @@ import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag;
@RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket")
@SecurityRequirement(name = "bearerAuth")
public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
@Autowired
@@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.hs.office.membership;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.context.Context;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeMembershipsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipPatchResource;
@@ -27,7 +27,7 @@ import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag;
@RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket")
@SecurityRequirement(name = "bearerAuth")
public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
@Autowired
@@ -3,7 +3,7 @@ package net.hostsharing.hsadminng.hs.office.partner;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.config.MessageTranslator;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.context.Context;
import net.hostsharing.hsadminng.errors.ReferenceNotFoundException;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactFromResourceConverter;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
@@ -39,7 +39,7 @@ import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag;
@RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket")
@SecurityRequirement(name = "bearerAuth")
public class HsOfficePartnerController implements HsOfficePartnersApi {
@Autowired
@@ -3,7 +3,7 @@ package net.hostsharing.hsadminng.hs.office.person;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.context.Context;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficePersonsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonPatchResource;
@@ -20,7 +20,7 @@ import java.util.UUID;
@RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket")
@SecurityRequirement(name = "bearerAuth")
public class HsOfficePersonController implements HsOfficePersonsApi {
@Autowired
@@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.hs.office.relation;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.context.Context;
import net.hostsharing.hsadminng.errors.Validate;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealRepository;
@@ -29,7 +29,7 @@ import static net.hostsharing.hsadminng.mapper.KeyValueMap.from;
@RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket")
@SecurityRequirement(name = "bearerAuth")
public class HsOfficeRelationController implements HsOfficeRelationsApi {
@Autowired
@@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.hs.office.sepamandate;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.context.Context;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountRepository;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeSepaMandatesApi;
@@ -27,7 +27,7 @@ import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateR
@RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket")
@SecurityRequirement(name = "bearerAuth")
public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
@Autowired
@@ -6,19 +6,16 @@ import net.hostsharing.hsadminng.config.NoSecurityRequirement;
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.web.bind.annotation.RestController;
@RestController
@PreAuthorize("isAuthenticated()")
@NoSecurityRequirement
public class PingController implements TestApi {
@Autowired
private MessageTranslator messageTranslator;
@PreAuthorize("permitAll()")
@Timed("app.api.ping")
public ResponseEntity<String> ping() {
// HOWTO translate text with placeholders - also see in resource files i18n/messages_*.properties.
@@ -26,7 +23,6 @@ public class PingController implements TestApi {
return ResponseEntity.ok(translatedMessage + "\n");
}
@PreAuthorize("isAuthenticated()")
@Timed("app.api.pong")
public ResponseEntity<String> pong() {
final var userName = SecurityContextHolder.getContext().getAuthentication().getName();
@@ -1,7 +1,8 @@
package net.hostsharing.hsadminng.context;
package net.hostsharing.hsadminng.rbac.context;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import lombok.val;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
@@ -9,6 +10,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.request.RequestContextHolder;
import jakarta.persistence.EntityExistsException;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.servlet.http.HttpServletRequest;
@@ -74,7 +76,7 @@ public class Context {
""");
query.setParameter("currentTask", shortenToMaxLength(currentTask, 127));
query.setParameter("currentRequest", currentRequest);
query.setParameter("currentSubject", currentSubject);
query.setParameter("currentSubject", subjectName(currentSubject));
query.setParameter("assumedRoles", assumedRoles != null ? assumedRoles : "");
query.executeUpdate();
}
@@ -119,6 +121,27 @@ public class Context {
.orElse("unknown");
}
private String subjectName(final String nameOrUuid) {
if (nameOrUuid == null) {
return null;
}
// TODO.impl: maybe it should be the other way around: UUID as the default and just optionally the name
try {
val authenticatedUuid = UUID.fromString(nameOrUuid);
val subjectName = findSubjectNameByUuid(authenticatedUuid)
.orElseThrow(() -> new EntityExistsException("Subject not found"));
return subjectName;
} catch (final IllegalArgumentException e) {
return nameOrUuid;
}
}
private Optional<String> findSubjectNameByUuid(final UUID authenticatedUuid) {
return Optional.ofNullable(em.createNativeQuery("SELECT name FROM rbac.subject s WHERE s.uuid=:uuid")
.setParameter("uuid", authenticatedUuid)
.getSingleResult()).map(Object::toString);
}
private String toTask(final HttpServletRequest request) {
if (isRequestScopeAvailable()) {
return request.getMethod() + " " + request.getRequestURI();
@@ -2,7 +2,6 @@ package net.hostsharing.hsadminng.rbac.context;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacContextApi;
import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacContextResource;
@@ -23,7 +22,7 @@ import java.util.UUID;
@RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket")
@SecurityRequirement(name = "bearerAuth")
public class RbacContextController implements RbacContextApi {
@Autowired
@@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.rbac.grant;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.context.Context;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacGrantsApi;
import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacGrantResource;
@@ -20,7 +20,7 @@ import java.util.UUID;
@RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket")
@SecurityRequirement(name = "bearerAuth")
public class RbacGrantController implements RbacGrantsApi {
@Autowired
@@ -1,6 +1,6 @@
package net.hostsharing.hsadminng.rbac.grant;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.context.Context;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.rbac.role;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.context.Context;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacRolesApi;
import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacRoleResource;
@@ -16,7 +16,7 @@ import java.util.List;
@RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket")
@SecurityRequirement(name = "bearerAuth")
public class RbacRoleController implements RbacRolesApi {
@Autowired
@@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.rbac.subject;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.context.Context;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacSubjectsApi;
import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacSubjectPermissionResource;
@@ -19,7 +19,7 @@ import java.util.UUID;
@RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket")
@SecurityRequirement(name = "bearerAuth")
public class RbacSubjectController implements RbacSubjectsApi {
@Autowired
@@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.rbac.test.cust;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.context.Context;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.test.generated.api.v1.api.TestCustomersApi;
import net.hostsharing.hsadminng.test.generated.api.v1.model.TestCustomerResource;
@@ -18,7 +18,7 @@ import java.util.List;
@RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket")
@SecurityRequirement(name = "bearerAuth")
public class TestCustomerController implements TestCustomersApi {
@Autowired
@@ -3,7 +3,7 @@ package net.hostsharing.hsadminng.rbac.test.pac;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.context.Context;
import net.hostsharing.hsadminng.test.generated.api.v1.api.TestPackagesApi;
import net.hostsharing.hsadminng.test.generated.api.v1.model.TestPackageResource;
import net.hostsharing.hsadminng.test.generated.api.v1.model.TestPackageUpdateResource;
@@ -18,7 +18,7 @@ import java.util.UUID;
@RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "casTicket")
@SecurityRequirement(name = "bearerAuth")
public class TestPackageController implements TestPackagesApi {
@Autowired
@@ -147,15 +147,11 @@ public final class Stringify<B> {
@SuppressWarnings("unchecked")
PropertyValue(final B object, final Property<B, ?> prop) {
// FIXME: simplify
final var typedProp = (Property<B, V>) prop;
final var value = typedProp.getValue(object);
final var stringifiedValue = value instanceof Stringifyable stringifyable
? stringifyable.toShortString()
: Objects.toString(value);
this.prop = typedProp;
this.value = (V) value;
this.stringValue = stringifiedValue;
this.prop = (Property<B, V>) prop;
this.value = (V) this.prop.getValue(object);
this.stringValue = this.value instanceof Stringifyable s
? s.toShortString()
: Objects.toString(this.value);
}
boolean notNullAndNotEmpty() {