From 8725c6a125c1f963bbee24b9ae58a43844d3dc0c Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 13 Mar 2025 16:41:08 +0100 Subject: [PATCH] WIP --- README.md | 7 ++- bin/howto | 2 +- build.gradle | 6 +- .../hsadminng/HsadminNgApplication.java | 2 + .../config/CasAuthenticationFilter.java | 11 +++- .../config/NoSecurityRequirement.java | 18 ++++++ .../config/RealCasAuthenticator.java | 57 +++++++++++++++---- .../hsadminng/config/WebSecurityConfig.java | 5 ++ .../booking/item/HsBookingItemController.java | 2 + .../project/HsBookingProjectController.java | 2 + .../asset/HsHostingAssetController.java | 2 + .../asset/HsHostingAssetPropsController.java | 2 + .../HsOfficeBankAccountController.java | 3 +- .../contact/HsOfficeContactController.java | 2 + ...OfficeCoopAssetsTransactionController.java | 2 + ...OfficeCoopSharesTransactionController.java | 2 + .../debitor/HsOfficeDebitorController.java | 3 +- .../HsOfficeMembershipController.java | 2 + .../partner/HsOfficePartnerController.java | 3 +- .../person/HsOfficePersonController.java | 3 +- .../relation/HsOfficeRelationController.java | 2 + .../HsOfficeSepaMandateController.java | 3 +- .../rbac/grant/RbacGrantController.java | 2 + .../rbac/role/RbacRoleController.java | 2 + .../rbac/subject/RbacSubjectController.java | 2 + .../test/cust/TestCustomerController.java | 2 + .../rbac/test/pac/TestPackageController.java | 2 + .../api-definition/test/test-customers.yaml | 1 + src/main/resources/application.yml | 8 ++- .../hsadminng/arch/ArchitectureTest.java | 11 ++++ ...asAuthenticationFilterIntegrationTest.java | 4 +- .../config/CasAuthenticatorUnitTest.java | 3 +- src/test/resources/application.yml | 2 +- 33 files changed, 150 insertions(+), 30 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/config/NoSecurityRequirement.java diff --git a/README.md b/README.md index a1b5f52d..2ce96594 100644 --- a/README.md +++ b/README.md @@ -132,9 +132,10 @@ Also try for example 'admin@xxx.example.com' or 'unknown@example.org'. If you want a formatted JSON output, you can pipe the result to `jq` or similar. -And to see the full, currently implemented, API, open http://localhost:8080/swagger-ui/index.html (on same port as the API to avoid CORS problems). - -If you still need to install some of these tools, find some hints in the next chapters. +And to see the full, currently implemented, API, open http://localhost:8080/swagger-ui/index.html). +For a locally running app without CAS-authentication (export HSADMINNG_CAS_SERVER=''), +authorize using the name of the subject (e.g. "superuser-alex@hostsharing.net" in case of test-data). +Otherwise, use a valid CAS-ticket. ### PostgreSQL Server diff --git a/bin/howto b/bin/howto index 0a3b6404..be2aad69 100755 --- a/bin/howto +++ b/bin/howto @@ -31,7 +31,7 @@ def search_keywords_in_files(keywords): sys.exit(1) # Allowed comment symbols - comment_symbols = {"//", "#", ";"} + comment_symbols = {"//", "#", "##", "###", "####", "#####", ";"} for root, dirs, files in os.walk("."): # Ausschließen bestimmter Verzeichnisse diff --git a/build.gradle b/build.gradle index f3a12160..16a58e9f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.4.1' + id 'org.springframework.boot' version '3.4.2' id 'io.spring.dependency-management' version '1.1.7' // manages implicit dependencies id 'io.openapiprocessor.openapi-processor' version '2023.2' // generates Controller-interface and resources from API-spec id 'com.github.jk1.dependency-license-report' version '2.9' // checks dependency-license compatibility @@ -67,7 +67,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.10.0' - implementation 'org.springdoc:springdoc-openapi:2.8.3' implementation 'org.postgresql:postgresql' implementation 'org.liquibase:liquibase-core' implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.9.0' @@ -77,7 +76,8 @@ dependencies { implementation 'net.java.dev.jna:jna:5.16.0' implementation 'org.modelmapper:modelmapper:3.2.2' implementation 'org.iban4j:iban4j:3.2.10-RELEASE' - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.3' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.5' + //implementation 'org.springdoc:springdoc-openapi-security:1.8.0' implementation 'org.reflections:reflections:0.10.2' compileOnly 'org.projectlombok:lombok' diff --git a/src/main/java/net/hostsharing/hsadminng/HsadminNgApplication.java b/src/main/java/net/hostsharing/hsadminng/HsadminNgApplication.java index af29526b..a1180099 100644 --- a/src/main/java/net/hostsharing/hsadminng/HsadminNgApplication.java +++ b/src/main/java/net/hostsharing/hsadminng/HsadminNgApplication.java @@ -1,9 +1,11 @@ package net.hostsharing.hsadminng; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication +@OpenAPIDefinition public class HsadminNgApplication { public static void main(String[] args) { diff --git a/src/main/java/net/hostsharing/hsadminng/config/CasAuthenticationFilter.java b/src/main/java/net/hostsharing/hsadminng/config/CasAuthenticationFilter.java index 41b0a93e..788a66a8 100644 --- a/src/main/java/net/hostsharing/hsadminng/config/CasAuthenticationFilter.java +++ b/src/main/java/net/hostsharing/hsadminng/config/CasAuthenticationFilter.java @@ -2,6 +2,7 @@ package net.hostsharing.hsadminng.config; import lombok.AllArgsConstructor; import lombok.SneakyThrows; +import net.hostsharing.hsadminng.context.HttpServletRequestWithCachedBody; import org.springframework.web.filter.OncePerRequestFilter; import jakarta.servlet.FilterChain; @@ -21,12 +22,16 @@ public class CasAuthenticationFilter extends OncePerRequestFilter { protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) { - if (request.getHeader("Authorization") != null) { + request.getInputStream(); + + if (request.getHeader("authorization") != null) { final var authenticatedRequest = new AuthenticatedHttpServletRequestWrapper(request); final var currentSubject = authenticator.authenticate(request); authenticatedRequest.addHeader("current-subject", currentSubject); - filterChain.doFilter(authenticatedRequest, response); + authenticatedRequest.getInputStream(); + filterChain.doFilter(new HttpServletRequestWithCachedBody(authenticatedRequest), response); + } else { + filterChain.doFilter(request, response); } - filterChain.doFilter(request, response); } } diff --git a/src/main/java/net/hostsharing/hsadminng/config/NoSecurityRequirement.java b/src/main/java/net/hostsharing/hsadminng/config/NoSecurityRequirement.java new file mode 100644 index 00000000..70a87cff --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/config/NoSecurityRequirement.java @@ -0,0 +1,18 @@ +package net.hostsharing.hsadminng.config; + +import io.swagger.v3.oas.annotations.security.SecurityRequirement; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; + +/** Explicitly marks a REST-Controller for not requiring authorization for Swagger UI. + * + * @see SecurityRequirement + */ +@Target(TYPE) +@Retention(RetentionPolicy.CLASS) +public @interface NoSecurityRequirement { +} diff --git a/src/main/java/net/hostsharing/hsadminng/config/RealCasAuthenticator.java b/src/main/java/net/hostsharing/hsadminng/config/RealCasAuthenticator.java index 66f4c8ed..e80cc183 100644 --- a/src/main/java/net/hostsharing/hsadminng/config/RealCasAuthenticator.java +++ b/src/main/java/net/hostsharing/hsadminng/config/RealCasAuthenticator.java @@ -8,6 +8,7 @@ import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.client.RestTemplate; +import org.w3c.dom.Document; import org.xml.sax.SAXException; import jakarta.servlet.http.HttpServletRequest; @@ -31,27 +32,34 @@ public class RealCasAuthenticator implements CasAuthenticator { public String authenticate(final HttpServletRequest httpRequest) { final var userName = StringUtils.isBlank(casServerUrl) ? bypassCurrentSubject(httpRequest) - : casValidation(httpRequest); + : casAuthentication(httpRequest); final var authentication = new UsernamePasswordAuthenticationToken(userName, null, null); SecurityContextHolder.getContext().setAuthentication(authentication); return authentication.getName(); } private static String bypassCurrentSubject(final HttpServletRequest httpRequest) { - final var userName = httpRequest.getHeader("current-subject"); + final var userName = httpRequest.getHeader("authorization").replaceAll("^Bearer ", ""); System.err.println("CasAuthenticator.bypassCurrentSubject: " + userName); return userName; } - private String casValidation(final HttpServletRequest httpRequest) + private String casAuthentication(final HttpServletRequest httpRequest) throws SAXException, IOException, ParserConfigurationException { - final var ticket = httpRequest.getHeader("Authorization"); - final var url = casServerUrl + "/p3/serviceValidate" + - "?service=" + serviceUrl + - "&ticket=" + ticket; + final var ticket = httpRequest.getHeader("authorization").replaceAll("^Bearer ", ""); + final var serviceTicket = ticket.startsWith("TGT-") + ? fetchServiceTicket(ticket) + : ticket; + final var userName = extractUserName(verifyServiceTicket(serviceTicket)); + System.err.println("CAS-user: " + userName); + return userName; + } - System.err.println("CasAuthenticator.casValidation using URL: " + url); + private String fetchServiceTicket(final String ticketGrantingTicket) throws SAXException, IOException, ParserConfigurationException { + final var url = casServerUrl + "/cas/v1/tickets" + + "?service=" + serviceUrl + + "&ticket=" + ticketGrantingTicket; final var response = ((Supplier) () -> restTemplate.getForObject(url, String.class)).get(); @@ -61,11 +69,40 @@ public class RealCasAuthenticator implements CasAuthenticator { System.err.println("CAS service ticket could not be validated"); System.err.println("CAS-validation-URL: " + url); System.err.println(response); - throw new BadCredentialsException("CAS service ticket could not be validated"); + throwBadCredentialsException("CAS service ticket could not be validated"); } final var userName = doc.getElementsByTagName("cas:user").item(0).getTextContent(); - System.err.println("CAS-user: " + userName); return userName; } + private Document verifyServiceTicket(final String serviceTicket) throws SAXException, IOException, ParserConfigurationException { + if ( !serviceTicket.startsWith("ST-") ) { + throwBadCredentialsException("Invalid authorization ticket"); + } + + final var url = casServerUrl + "/p3/serviceValidate" + + "?service=" + serviceUrl + + "&ticket=" + serviceTicket; + + final var response = ((Supplier) () -> restTemplate.getForObject(url, String.class)).get(); + + return DocumentBuilderFactory.newInstance().newDocumentBuilder() + .parse(new java.io.ByteArrayInputStream(response.getBytes())); + + } + + private String extractUserName(final Document verification) { + + if (verification.getElementsByTagName("cas:authenticationSuccess").getLength() == 0) { + System.err.println("CAS service ticket could not be validated"); + System.err.println(verification); + throwBadCredentialsException("CAS service ticket could not be validated"); + } + return verification.getElementsByTagName("cas:user").item(0).getTextContent(); + } + + private String throwBadCredentialsException(final String message) { + throw new BadCredentialsException(message); + } + } diff --git a/src/main/java/net/hostsharing/hsadminng/config/WebSecurityConfig.java b/src/main/java/net/hostsharing/hsadminng/config/WebSecurityConfig.java index 4bcae65a..a11fb592 100644 --- a/src/main/java/net/hostsharing/hsadminng/config/WebSecurityConfig.java +++ b/src/main/java/net/hostsharing/hsadminng/config/WebSecurityConfig.java @@ -1,5 +1,8 @@ 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; @@ -15,6 +18,8 @@ import jakarta.servlet.http.HttpServletResponse; @Configuration @EnableWebSecurity +// 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 { private static final String[] PERMITTED_PATHS = new String[] { "/swagger-ui/**", "/v3/api-docs/**", "/actuator/**" }; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java index 6b563041..685b30d7 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java @@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.booking.item; 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.hs.booking.generated.api.v1.api.HsBookingItemsApi; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemInsertResource; @@ -32,6 +33,7 @@ import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateR @RestController @Profile("!only-office") +@SecurityRequirement(name = "casTicket") public class HsBookingItemController implements HsBookingItemsApi { @Autowired diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectController.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectController.java index 11c135dd..acc5957b 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectController.java @@ -1,6 +1,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.hs.booking.debitor.HsBookingDebitorRepository; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingProjectsApi; @@ -22,6 +23,7 @@ import java.util.function.BiConsumer; @RestController @Profile("!only-office") +@SecurityRequirement(name = "casTicket") public class HsBookingProjectController implements HsBookingProjectsApi { @Autowired diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java index 7d12d714..6c36d21f 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java @@ -1,6 +1,7 @@ package net.hostsharing.hsadminng.hs.hosting.asset; import io.micrometer.core.annotation.Timed; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealRepository; import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntitySaveProcessor; import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntityValidatorRegistry; @@ -29,6 +30,7 @@ import java.util.function.BiConsumer; @RestController @Profile("!only-office") +@SecurityRequirement(name = "casTicket") public class HsHostingAssetController implements HsHostingAssetsApi { @Autowired diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java index d843ff87..bea792ba 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java @@ -1,6 +1,7 @@ package net.hostsharing.hsadminng.hs.hosting.asset; import io.micrometer.core.annotation.Timed; +import net.hostsharing.hsadminng.config.NoSecurityRequirement; import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntityValidatorRegistry; import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetPropsApi; import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource; @@ -14,6 +15,7 @@ import java.util.Map; @RestController @Profile("!only-office") +@NoSecurityRequirement public class HsHostingAssetPropsController implements HsHostingAssetPropsApi { @Override diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountController.java index 37b9d404..86b82955 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountController.java @@ -1,6 +1,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.hs.office.generated.api.v1.api.HsOfficeBankAccountsApi; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountInsertResource; @@ -18,7 +19,7 @@ import java.util.List; import java.util.UUID; @RestController - +@SecurityRequirement(name = "casTicket") public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi { @Autowired diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java index bf8a9c09..7022d515 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactController.java @@ -1,6 +1,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.hs.office.generated.api.v1.api.HsOfficeContactsApi; @@ -20,6 +21,7 @@ import java.util.UUID; import static net.hostsharing.hsadminng.errors.Validate.validate; @RestController +@SecurityRequirement(name = "casTicket") public class HsOfficeContactController implements HsOfficeContactsApi { @Autowired diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java index 9073e564..01401eef 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java @@ -1,6 +1,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.context.Context; import net.hostsharing.hsadminng.errors.MultiValidationException; import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopAssetsApi; @@ -37,6 +38,7 @@ import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOffic import static net.hostsharing.hsadminng.lambda.WithNonNull.withNonNull; @RestController +@SecurityRequirement(name = "casTicket") public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAssetsApi { @Autowired diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java index 5583d44e..9f42f413 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java @@ -1,6 +1,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.context.Context; import net.hostsharing.hsadminng.errors.MultiValidationException; import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopSharesApi; @@ -27,6 +28,7 @@ import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOffic import static net.hostsharing.hsadminng.hs.validation.UuidResolver.resolve; @RestController +@SecurityRequirement(name = "casTicket") public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopSharesApi { @Autowired diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorController.java index 2e0ff210..5f709bda 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorController.java @@ -1,6 +1,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.hs.office.bankaccount.HsOfficeBankAccountRepository; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealRepository; @@ -32,7 +33,7 @@ import static net.hostsharing.hsadminng.hs.validation.UuidResolver.resolve; import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag; @RestController - +@SecurityRequirement(name = "casTicket") public class HsOfficeDebitorController implements HsOfficeDebitorsApi { @Autowired diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipController.java index edf5d0a6..4c24d1e1 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipController.java @@ -1,6 +1,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.hs.office.generated.api.v1.api.HsOfficeMembershipsApi; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipInsertResource; @@ -24,6 +25,7 @@ import static net.hostsharing.hsadminng.errors.Validate.validate; import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag; @RestController +@SecurityRequirement(name = "casTicket") public class HsOfficeMembershipController implements HsOfficeMembershipsApi { @Autowired diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java index 6074c909..f99b290b 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java @@ -1,6 +1,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.context.Context; import net.hostsharing.hsadminng.errors.ReferenceNotFoundException; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactFromResourceConverter; @@ -35,7 +36,7 @@ import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType. import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag; @RestController - +@SecurityRequirement(name = "casTicket") public class HsOfficePartnerController implements HsOfficePartnersApi { @Autowired diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonController.java index 28a1b56f..ff763bbe 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonController.java @@ -1,6 +1,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.hs.office.generated.api.v1.api.HsOfficePersonsApi; @@ -17,7 +18,7 @@ import java.util.List; import java.util.UUID; @RestController - +@SecurityRequirement(name = "casTicket") public class HsOfficePersonController implements HsOfficePersonsApi { @Autowired diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationController.java index 53e2712b..2473b7ad 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationController.java @@ -1,6 +1,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.errors.Validate; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; @@ -26,6 +27,7 @@ import java.util.function.BiConsumer; import static net.hostsharing.hsadminng.mapper.KeyValueMap.from; @RestController +@SecurityRequirement(name = "casTicket") public class HsOfficeRelationController implements HsOfficeRelationsApi { @Autowired diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateController.java index 61399d1e..ad19fb42 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateController.java @@ -1,6 +1,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.hs.office.bankaccount.HsOfficeBankAccountRepository; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; @@ -26,7 +27,7 @@ import java.util.function.BiConsumer; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange; @RestController - +@SecurityRequirement(name = "casTicket") public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi { @Autowired diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/grant/RbacGrantController.java b/src/main/java/net/hostsharing/hsadminng/rbac/grant/RbacGrantController.java index 02dc6ae2..21389e1b 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/grant/RbacGrantController.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/grant/RbacGrantController.java @@ -1,6 +1,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.mapper.StrictMapper; import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacGrantsApi; @@ -17,6 +18,7 @@ import java.util.List; import java.util.UUID; @RestController +@SecurityRequirement(name = "casTicket") public class RbacGrantController implements RbacGrantsApi { @Autowired diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/role/RbacRoleController.java b/src/main/java/net/hostsharing/hsadminng/rbac/role/RbacRoleController.java index de7446e8..dc12465d 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/role/RbacRoleController.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/role/RbacRoleController.java @@ -1,6 +1,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.mapper.StrictMapper; import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacRolesApi; @@ -13,6 +14,7 @@ import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController +@SecurityRequirement(name = "casTicket") public class RbacRoleController implements RbacRolesApi { @Autowired diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/subject/RbacSubjectController.java b/src/main/java/net/hostsharing/hsadminng/rbac/subject/RbacSubjectController.java index c893fb98..f47e7159 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/subject/RbacSubjectController.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/subject/RbacSubjectController.java @@ -1,6 +1,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.mapper.StrictMapper; import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacSubjectsApi; @@ -16,6 +17,7 @@ import java.util.List; import java.util.UUID; @RestController +@SecurityRequirement(name = "casTicket") public class RbacSubjectController implements RbacSubjectsApi { @Autowired diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/test/cust/TestCustomerController.java b/src/main/java/net/hostsharing/hsadminng/rbac/test/cust/TestCustomerController.java index 82e3a57d..d8e15cfd 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/test/cust/TestCustomerController.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/test/cust/TestCustomerController.java @@ -1,5 +1,6 @@ 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.mapper.StrictMapper; import net.hostsharing.hsadminng.test.generated.api.v1.api.TestCustomersApi; @@ -15,6 +16,7 @@ import jakarta.persistence.PersistenceContext; import java.util.List; @RestController +@SecurityRequirement(name = "casTicket") public class TestCustomerController implements TestCustomersApi { @Autowired diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageController.java b/src/main/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageController.java index 43fd3b0b..86735af0 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageController.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageController.java @@ -1,5 +1,6 @@ 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; @@ -15,6 +16,7 @@ import java.util.List; import java.util.UUID; @RestController +@SecurityRequirement(name = "casTicket") public class TestPackageController implements TestPackagesApi { @Autowired diff --git a/src/main/resources/api-definition/test/test-customers.yaml b/src/main/resources/api-definition/test/test-customers.yaml index 8e81426a..017608e2 100644 --- a/src/main/resources/api-definition/test/test-customers.yaml +++ b/src/main/resources/api-definition/test/test-customers.yaml @@ -4,6 +4,7 @@ get: tags: - testCustomers operationId: listCustomers + parameters: - $ref: 'auth.yaml#/components/parameters/currentSubject' - $ref: 'auth.yaml#/components/parameters/assumedRoles' diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7abab833..f50899b6 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -57,7 +57,7 @@ spring: # keep this in sync with test/.../application.yml springdoc: # SwaggerUI must run on the same port as the API itself, otherwise CORS will block accessing the API - use-management-port: false + x-use-management-port: false hsadminng: postgres: @@ -72,3 +72,9 @@ metrics: http: server: requests: true + +logging: + level: + org: + springframework: + security: TRACE diff --git a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java index 664b803c..628cf5d7 100644 --- a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java +++ b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java @@ -11,7 +11,9 @@ import com.tngtech.archunit.lang.ArchRule; import com.tngtech.archunit.lang.ConditionEvents; import com.tngtech.archunit.lang.SimpleConditionEvent; import io.micrometer.core.annotation.Timed; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import net.hostsharing.hsadminng.HsadminNgApplication; +import net.hostsharing.hsadminng.config.NoSecurityRequirement; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRbacEntity; import net.hostsharing.hsadminng.rbac.context.ContextBasedTest; @@ -352,6 +354,15 @@ public class ArchitectureTest { static final ArchRule restControllerNaming = classes().that().areAnnotatedWith(RestController.class).should().haveSimpleNameEndingWith("Controller"); + @ArchTest + @SuppressWarnings("unused") + static final ArchRule restControllerSecurityRequirement = + // TODO.impl: seems that the Spring templates for the OpenAPI generator don't support this, + // thus we need this annotation to support Swagger UI authorization. + classes().that().areAnnotatedWith(RestController.class).should() + .beAnnotatedWith(SecurityRequirement.class).orShould() + .beAnnotatedWith(NoSecurityRequirement.class); + @ArchTest @SuppressWarnings("unused") static final ArchRule restControllerMethods = classes() diff --git a/src/test/java/net/hostsharing/hsadminng/config/CasAuthenticationFilterIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/config/CasAuthenticationFilterIntegrationTest.java index 17c1a582..57e9865b 100644 --- a/src/test/java/net/hostsharing/hsadminng/config/CasAuthenticationFilterIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/config/CasAuthenticationFilterIntegrationTest.java @@ -40,7 +40,7 @@ class CasAuthenticationFilterIntegrationTest { public void shouldAcceptRequestWithValidCasTicket() { // given final var username = "test-user-" + randomAlphanumeric(4); - wireMockServer.stubFor(get(urlEqualTo("/cas/p3/serviceValidate?service=" + serviceUrl + "&ticket=valid")) + wireMockServer.stubFor(get(urlEqualTo("/cas/p3/serviceValidate?service=" + serviceUrl + "&ticket=ST-valid")) .willReturn(aResponse() .withStatus(200) .withBody(""" @@ -56,7 +56,7 @@ class CasAuthenticationFilterIntegrationTest { final var result = restTemplate.exchange( "http://localhost:" + this.serverPort + "/api/ping", HttpMethod.GET, - new HttpEntity<>(null, headers("Authorization", "valid")), + new HttpEntity<>(null, headers("Authorization", "ST-valid")), String.class ); diff --git a/src/test/java/net/hostsharing/hsadminng/config/CasAuthenticatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/config/CasAuthenticatorUnitTest.java index f8f2bfc1..c2953c3f 100644 --- a/src/test/java/net/hostsharing/hsadminng/config/CasAuthenticatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/config/CasAuthenticatorUnitTest.java @@ -17,7 +17,8 @@ class CasAuthenticatorUnitTest { // given final var request = mock(HttpServletRequest.class); - given(request.getHeader("current-subject")).willReturn("given-user"); + // bypassing the CAS-server HTTP-request fakes the user from the authorization header's fake CAS-ticket + given(request.getHeader("authorization")).willReturn("Bearer given-user"); // when final var userName = casAuthenticator.authenticate(request); diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index abf049a6..c728db3b 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -42,7 +42,7 @@ spring: # keep this in sync with main/.../application.yml springdoc: # SwaggerUI must run on the same port as the API itself, otherwise CORS will block accessing the API - use-management-port: false + x-use-management-port: false logging: