diff --git a/.tc-environment b/.tc-environment index a009303d..194e6d52 100644 --- a/.tc-environment +++ b/.tc-environment @@ -3,5 +3,6 @@ source .unset-environment export HSADMINNG_POSTGRES_RESTRICTED_USERNAME=restricted export HSADMINNG_POSTGRES_ADMIN_USERNAME=admin export HSADMINNG_SUPERUSER=import-superuser@hostsharing.net +export HSADMINNG_CAS_SERVER= export LANG=en_US.UTF-8 diff --git a/.unset-environment b/.unset-environment index 2be52333..69a50ee3 100644 --- a/.unset-environment +++ b/.unset-environment @@ -5,4 +5,5 @@ unset HSADMINNG_POSTGRES_RESTRICTED_USERNAME unset HSADMINNG_SUPERUSER unset HSADMINNG_MIGRATION_DATA_PATH unset HSADMINNG_OFFICE_DATA_SQL_FILE +unset HSADMINNG_CAS_SERVER= diff --git a/README.md b/README.md index b803dd3d..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:8081/actuator/swagger-ui/index.html (uses management-port and thus bypasses authentication). - -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 @@ -666,6 +667,29 @@ These profiles mean: - **without-test-data**: no test-data is inserted +### How to Run the Application in a Debugger + +Add `' --debug-jvm` to the command line: + + +```sh +gw bootRun --debug-jvm +``` + +At the very beginning, the application is going to wait for a debugger with a message like this: + +> Listening for transport dt_socket at address: 5005 + +As soon as a debugger connects to that port, the application will continue to run. + +In IntelliJ IDEA you need a 'Remote JVM Debug' run configuration like this: + +![IntelliJ IDEA JVM-Debug Run Config](./doc/.images/intellij-idea-jvm-debug-run-config.png) + +Now, to attach IntelliJ IDEA as a debugger, you just need to run that config in debug mode. +If it's selected, just hit the *bug*-symbol next to it. + + ### How to Do a Clean Run of the Application If you frequently need to run with a fresh database and a clean build, you can use this: 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..4c3f0b19 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,7 @@ 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.reflections:reflections:0.10.2' compileOnly 'org.projectlombok:lombok' diff --git a/doc/.images/intellij-idea-jvm-debug-run-config.png b/doc/.images/intellij-idea-jvm-debug-run-config.png new file mode 100644 index 00000000..479df9a2 Binary files /dev/null and b/doc/.images/intellij-idea-jvm-debug-run-config.png differ 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/AuthenticationFilter.java b/src/main/java/net/hostsharing/hsadminng/config/AuthenticationFilter.java deleted file mode 100644 index 1849b815..00000000 --- a/src/main/java/net/hostsharing/hsadminng/config/AuthenticationFilter.java +++ /dev/null @@ -1,39 +0,0 @@ -package net.hostsharing.hsadminng.config; - -import jakarta.servlet.Filter; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import lombok.SneakyThrows; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.stereotype.Component; - -@Component -public class AuthenticationFilter implements Filter { - - @Autowired - private Authenticator authenticator; - - @Override - @SneakyThrows - public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) { - final var httpRequest = (HttpServletRequest) request; - final var httpResponse = (HttpServletResponse) response; - - try { - final var currentSubject = authenticator.authenticate(httpRequest); - - final var authenticatedRequest = new AuthenticatedHttpServletRequestWrapper(httpRequest); - authenticatedRequest.addHeader("current-subject", currentSubject); - - chain.doFilter(authenticatedRequest, response); - } catch (final BadCredentialsException exc) { - // TODO.impl: should not be necessary if ResponseStatusException worked - httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - } - } -} diff --git a/src/main/java/net/hostsharing/hsadminng/config/Authenticator.java b/src/main/java/net/hostsharing/hsadminng/config/Authenticator.java deleted file mode 100644 index 13f4ada4..00000000 --- a/src/main/java/net/hostsharing/hsadminng/config/Authenticator.java +++ /dev/null @@ -1,8 +0,0 @@ -package net.hostsharing.hsadminng.config; - -import jakarta.servlet.http.HttpServletRequest; - -public interface Authenticator { - - String authenticate(final HttpServletRequest httpRequest); -} diff --git a/src/main/java/net/hostsharing/hsadminng/config/CasAuthenticationFilter.java b/src/main/java/net/hostsharing/hsadminng/config/CasAuthenticationFilter.java new file mode 100644 index 00000000..650cad66 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/config/CasAuthenticationFilter.java @@ -0,0 +1,33 @@ +package net.hostsharing.hsadminng.config; + +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +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); + authenticatedRequest.addHeader("current-subject", currentSubject); + filterChain.doFilter(authenticatedRequest, response); + } else { + filterChain.doFilter(request, response); + } + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/config/CasAuthenticator.java b/src/main/java/net/hostsharing/hsadminng/config/CasAuthenticator.java index 5d6dd116..b063a61e 100644 --- a/src/main/java/net/hostsharing/hsadminng/config/CasAuthenticator.java +++ b/src/main/java/net/hostsharing/hsadminng/config/CasAuthenticator.java @@ -1,71 +1,8 @@ package net.hostsharing.hsadminng.config; -import io.micrometer.core.annotation.Timed; -import lombok.SneakyThrows; -import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.annotation.Value; -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.xml.sax.SAXException; - import jakarta.servlet.http.HttpServletRequest; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import java.io.IOException; -public class CasAuthenticator implements Authenticator { +public interface CasAuthenticator { - @Value("${hsadminng.cas.server}") - private String casServerUrl; - - @Value("${hsadminng.cas.service}") - private String serviceUrl; - - private final RestTemplate restTemplate = new RestTemplate(); - - @SneakyThrows - @Timed("app.cas.authenticate") - public String authenticate(final HttpServletRequest httpRequest) { - final var userName = StringUtils.isBlank(casServerUrl) - ? bypassCurrentSubject(httpRequest) - : casValidation(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"); - System.err.println("CasAuthenticator.bypassCurrentSubject: " + userName); - return userName; - } - - private String casValidation(final HttpServletRequest httpRequest) - throws SAXException, IOException, ParserConfigurationException { - - final var ticket = httpRequest.getHeader("Authorization"); - final var url = casServerUrl + "/p3/serviceValidate" + - "?service=" + serviceUrl + - "&ticket=" + ticket; - - System.err.println("CasAuthenticator.casValidation using URL: " + url); - - final var response = restTemplate.getForObject(url, String.class); - - final var doc = DocumentBuilderFactory.newInstance().newDocumentBuilder() - .parse(new java.io.ByteArrayInputStream(response.getBytes())); - if (doc.getElementsByTagName("cas:authenticationSuccess").getLength() == 0) { - // TODO.impl: for unknown reasons, this results in a 403 FORBIDDEN - // throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "CAS service ticket could not be validated"); - 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"); - } - final var userName = doc.getElementsByTagName("cas:user").item(0).getTextContent(); - System.err.println("CAS-user: " + userName); - return userName; - } + String authenticate(final HttpServletRequest httpRequest); } 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 new file mode 100644 index 00000000..0ac28059 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/config/RealCasAuthenticator.java @@ -0,0 +1,100 @@ +package net.hostsharing.hsadminng.config; + +import io.micrometer.core.annotation.Timed; +import lombok.SneakyThrows; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +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; +import java.util.function.Supplier; + +public class RealCasAuthenticator implements CasAuthenticator { + + @Value("${hsadminng.cas.server}") + private String casServerUrl; + + @Value("${hsadminng.cas.service}") + private String serviceUrl; + + private final RestTemplate restTemplate = new RestTemplate(); + + @SneakyThrows + @Timed("app.cas.authenticate") + public String authenticate(final HttpServletRequest httpRequest) { + final var userName = StringUtils.isBlank(casServerUrl) + ? bypassCurrentSubject(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("authorization").replaceAll("^Bearer ", ""); + System.err.println("CasAuthenticator.bypassCurrentSubject: " + userName); + return userName; + } + + private String casAuthentication(final HttpServletRequest httpRequest) + throws SAXException, IOException, ParserConfigurationException { + + 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; + } + + private String fetchServiceTicket(final String ticketGrantingTicket) { + final var tgtUrl = casServerUrl + "/cas/v1/tickets/" + ticketGrantingTicket; + + final var restTemplate = new RestTemplate(); + final var formData = new LinkedMultiValueMap(); + 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("Invalid authorization ticket"); + } + + final var url = casServerUrl + "/cas/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 d279ae12..a11fb592 100644 --- a/src/main/java/net/hostsharing/hsadminng/config/WebSecurityConfig.java +++ b/src/main/java/net/hostsharing/hsadminng/config/WebSecurityConfig.java @@ -1,36 +1,63 @@ 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.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 +// 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/**" }; + private static final String[] AUTHENTICATED_PATHS = new String[] { "/api/**" }; + + @Lazy + @Autowired + private CasAuthenticationFilter authenticationFilter; + @Bean @Profile("!test") public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception { return http .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/api/**").permitAll() // TODO.impl: implement authentication - .requestMatchers("/swagger-ui/**").permitAll() - .requestMatchers("/v3/api-docs/**").permitAll() - .requestMatchers("/actuator/**").permitAll() - .anyRequest().authenticated() + .requestMatchers(PERMITTED_PATHS).permitAll() + .requestMatchers(AUTHENTICATED_PATHS).authenticated() + .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("!test") - public Authenticator casServiceTicketValidator() { - return new CasAuthenticator(); + public CasAuthenticator casServiceTicketValidator() { + return new RealCasAuthenticator(); } + @Bean + public CasAuthenticationFilter authenticationFilter(final CasAuthenticator authenticator) { + return new CasAuthenticationFilter(authenticator); + } } 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/auth.yaml b/src/main/resources/api-definition/auth.yaml index 138c5eaa..345b003b 100644 --- a/src/main/resources/api-definition/auth.yaml +++ b/src/main/resources/api-definition/auth.yaml @@ -6,7 +6,7 @@ components: currentSubject: name: current-subject in: header - required: true + required: false schema: type: string description: Identifying name of the current subject (e.g. user). 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 60fd285e..f7d42bc8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -17,7 +17,7 @@ management: # HOWTO: view the effective application configuration properties: # http://localhost:8081/actuator/configprops - include: info, health, metrics, metric-links, mappings, openapi, swaggerui, configprops, env + include: info, health, metrics, metric-links, mappings, openapi, configprops, env endpoint: env: # TODO.spec: check this, maybe set to when_authorized? @@ -37,6 +37,11 @@ spring: url: ${HSADMINNG_POSTGRES_JDBC_URL} username: postgres + data: + rest: + # do NOT implicilty expose SpringData repositories as REST-controllers + detection-strategy: annotated + sql: init: mode: never @@ -49,10 +54,6 @@ spring: liquibase: contexts: ${spring.profiles.active} -# keep this in sync with test/.../application.yml -springdoc: - use-management-port: true - hsadminng: postgres: leakproof: @@ -66,3 +67,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 5c918943..90238fc1 100644 --- a/src/test/java/net/hostsharing/hsadminng/config/CasAuthenticationFilterIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/config/CasAuthenticationFilterIntegrationTest.java @@ -19,7 +19,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static com.github.tomakehurst.wiremock.client.WireMock.*; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@TestPropertySource(properties = {"server.port=0", "hsadminng.cas.server=http://localhost:8088/cas"}) +@TestPropertySource(properties = {"server.port=0", "hsadminng.cas.server=http://localhost:8088"}) @ActiveProfiles("wiremock") // IMPORTANT: To test prod config, do not use test profile! @Tag("generalIntegrationTest") class CasAuthenticationFilterIntegrationTest { @@ -37,10 +37,10 @@ class CasAuthenticationFilterIntegrationTest { private WireMockServer wireMockServer; @Test - public void shouldAcceptRequest() { + 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 ); @@ -66,7 +66,7 @@ class CasAuthenticationFilterIntegrationTest { } @Test - public void shouldRejectRequest() { + public void shouldRejectRequestWithInvalidCasTicket() { // given wireMockServer.stubFor(get(urlEqualTo("/cas/p3/serviceValidate?service=" + serviceUrl + "&ticket=invalid")) .willReturn(aResponse() diff --git a/src/test/java/net/hostsharing/hsadminng/config/CasAuthenticatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/config/CasAuthenticatorUnitTest.java index 5691e092..c2953c3f 100644 --- a/src/test/java/net/hostsharing/hsadminng/config/CasAuthenticatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/config/CasAuthenticatorUnitTest.java @@ -10,14 +10,15 @@ import static org.mockito.Mockito.mock; class CasAuthenticatorUnitTest { - final CasAuthenticator casAuthenticator = new CasAuthenticator(); + final RealCasAuthenticator casAuthenticator = new RealCasAuthenticator(); @Test void bypassesAuthenticationIfNoCasServerIsConfigured() { // 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/java/net/hostsharing/hsadminng/config/DisableSecurityConfig.java b/src/test/java/net/hostsharing/hsadminng/config/DisableSecurityConfig.java index 4c4f98e8..bcf15c08 100644 --- a/src/test/java/net/hostsharing/hsadminng/config/DisableSecurityConfig.java +++ b/src/test/java/net/hostsharing/hsadminng/config/DisableSecurityConfig.java @@ -21,7 +21,7 @@ public class DisableSecurityConfig { @Bean @Profile("test") - public Authenticator fakeAuthenticator() { - return new FakeAuthenticator(); + public CasAuthenticator fakeAuthenticator() { + return new FakeCasAuthenticator(); } } diff --git a/src/test/java/net/hostsharing/hsadminng/config/FakeAuthenticator.java b/src/test/java/net/hostsharing/hsadminng/config/FakeCasAuthenticator.java similarity index 81% rename from src/test/java/net/hostsharing/hsadminng/config/FakeAuthenticator.java rename to src/test/java/net/hostsharing/hsadminng/config/FakeCasAuthenticator.java index 139ef053..15ac599d 100644 --- a/src/test/java/net/hostsharing/hsadminng/config/FakeAuthenticator.java +++ b/src/test/java/net/hostsharing/hsadminng/config/FakeCasAuthenticator.java @@ -4,7 +4,7 @@ import lombok.SneakyThrows; import jakarta.servlet.http.HttpServletRequest; -public class FakeAuthenticator implements Authenticator { +public class FakeCasAuthenticator implements CasAuthenticator { @Override @SneakyThrows diff --git a/src/test/java/net/hostsharing/hsadminng/config/WebSecurityConfigIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/config/WebSecurityConfigIntegrationTest.java index 3a612b35..be993747 100644 --- a/src/test/java/net/hostsharing/hsadminng/config/WebSecurityConfigIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/config/WebSecurityConfigIntegrationTest.java @@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.config; import java.util.Map; import com.github.tomakehurst.wiremock.WireMockServer; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -18,12 +19,16 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static java.util.Map.entry; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@TestPropertySource(properties = {"management.port=0", "server.port=0", "hsadminng.cas.server=http://localhost:8088/cas"}) +@TestPropertySource(properties = {"management.port=0", "server.port=0", "hsadminng.cas.server=http://localhost:8088"}) @ActiveProfiles("wiremock") // IMPORTANT: To test prod config, do not use test profile! @Tag("generalIntegrationTest") class WebSecurityConfigIntegrationTest { @@ -43,71 +48,151 @@ class WebSecurityConfigIntegrationTest { @Autowired private WireMockServer wireMockServer; - @Test - public void shouldSupportPingEndpoint() { - // given - wireMockServer.stubFor(get(urlEqualTo("/cas/p3/serviceValidate?service=" + serviceUrl + "&ticket=test-user")) + @BeforeEach + void setUp() { + wireMockServer.stubFor(get(anyUrl()) .willReturn(aResponse() .withStatus(200) .withBody(""" - - test-user - + """))); + } - - // fake Authorization header - final var headers = new HttpHeaders(); - headers.set("Authorization", "test-user"); + @Test + void accessToApiWithValidServiceTicketSouldBePermitted() { + // given + givenCasTicketValidationResponse("ST-fake-cas-ticket", "fake-user-name"); // http request final var result = restTemplate.exchange( "http://localhost:" + this.serverPort + "/api/ping", HttpMethod.GET, - new HttpEntity<>(null, headers), + httpHeaders(entry("Authorization", "Bearer ST-fake-cas-ticket")), String.class ); assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(result.getBody()).startsWith("pong test-user"); + assertThat(result.getBody()).startsWith("pong fake-user-name"); } @Test - public void shouldSupportActuatorEndpoint() { + void accessToApiWithValidTicketGrantingTicketShouldBePermitted() { + // given + givenCasServiceTicketForTicketGrantingTicket("TGT-fake-cas-ticket", "ST-fake-cas-ticket"); + givenCasTicketValidationResponse("ST-fake-cas-ticket", "fake-user-name"); + + // http request + final var result = restTemplate.exchange( + "http://localhost:" + this.serverPort + "/api/ping", + HttpMethod.GET, + httpHeaders(entry("Authorization", "Bearer TGT-fake-cas-ticket")), + String.class + ); + + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(result.getBody()).startsWith("pong fake-user-name"); + } + + @Test + void accessToApiWithInvalidTicketGrantingTicketShouldBePermitted() { + // given + givenCasServiceTicketForTicketGrantingTicket("TGT-fake-cas-ticket", "ST-fake-cas-ticket"); + givenCasTicketValidationResponse("ST-fake-cas-ticket", "fake-user-name"); + + // http request + final var result = restTemplate.exchange( + "http://localhost:" + this.serverPort + "/api/ping", + HttpMethod.GET, + httpHeaders(entry("Authorization", "Bearer TGT-WRONG-cas-ticket")), + String.class + ); + + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + void accessToApiWithoutTokenShouldBeDenied() { + final var result = this.restTemplate.getForEntity( + "http://localhost:" + this.serverPort + "/api/ping", String.class); + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + void accessToApiWithInvalidTokenShouldBeDenied() { + // given + givenCasTicketValidationResponse("ST-fake-cas-ticket", "fake-user-name"); + + // when + final var result = restTemplate.exchange( + "http://localhost:" + this.serverPort + "/api/ping", + HttpMethod.GET, + httpHeaders(entry("Authorization", "Bearer ST-WRONG-cas-ticket")), + String.class + ); + + // then + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + void accessToActuatorShouldBePermitted() { final var result = this.restTemplate.getForEntity( "http://localhost:" + this.managementPort + "/actuator", Map.class); assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); } @Test - public void shouldSupportSwaggerUi() { + void accessToSwaggerUiShouldBePermitted() { final var result = this.restTemplate.getForEntity( - "http://localhost:" + this.managementPort + "/actuator/swagger-ui/index.html", String.class); + "http://localhost:" + this.serverPort + "/swagger-ui/index.html", String.class); assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); } @Test - public void shouldSupportApiDocs() { + void accessToApiDocsEndpointShouldBePermitted() { final var result = this.restTemplate.getForEntity( - "http://localhost:" + this.managementPort + "/actuator/v3/api-docs/swagger-config", String.class); - assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); // permitted but not configured + "http://localhost:" + this.serverPort + "/v3/api-docs/swagger-config", String.class); + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(result.getBody()).contains("\"configUrl\":\"/v3/api-docs/swagger-config\""); } @Test - public void shouldSupportHealthEndpoint() { + void accessToActuatorEndpointShouldBePermitted() { final var result = this.restTemplate.getForEntity( "http://localhost:" + this.managementPort + "/actuator/health", Map.class); assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(result.getBody().get("status")).isEqualTo("UP"); } - @Test - public void shouldSupportMetricsEndpoint() { - final var result = this.restTemplate.getForEntity( - "http://localhost:" + this.managementPort + "/actuator/metrics", Map.class); - assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); + private void givenCasServiceTicketForTicketGrantingTicket(final String ticketGrantingTicket, final String serviceTicket) { + wireMockServer.stubFor(post(urlEqualTo("/cas/v1/tickets/" + ticketGrantingTicket)) + .withFormParam("service", equalTo(serviceUrl)) + .willReturn(aResponse() + .withStatus(201) + .withBody(serviceTicket))); } + private void givenCasTicketValidationResponse(final String casToken, final String userName) { + wireMockServer.stubFor(get(urlEqualTo("/cas/p3/serviceValidate?service=" + serviceUrl + "&ticket=" + casToken)) + .willReturn(aResponse() + .withStatus(200) + .withBody(""" + + + ${userName} + + + """.replace("${userName}", userName)))); + } + + @SafeVarargs + private HttpEntity httpHeaders(final Map.Entry... headerValues) { + final var headers = new HttpHeaders(); + for ( Map.Entry headerValue: headerValues ) { + headers.add(headerValue.getKey(), headerValue.getValue()); + } + return new HttpEntity<>(headers); + } } diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 344828e7..a01bcb2e 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -39,10 +39,6 @@ spring: change-log: classpath:/db/changelog/db.changelog-master.yaml contexts: tc,test,dev,pg_stat_statements -# keep this in sync with main/.../application.yml -springdoc: - use-management-port: true - logging: level: liquibase: WARN