unauthenticated swagger-ui on- server-port and proper security filter integration into Spring Security #163
@ -3,5 +3,6 @@ source .unset-environment
|
|||||||
export HSADMINNG_POSTGRES_RESTRICTED_USERNAME=restricted
|
export HSADMINNG_POSTGRES_RESTRICTED_USERNAME=restricted
|
||||||
export HSADMINNG_POSTGRES_ADMIN_USERNAME=admin
|
export HSADMINNG_POSTGRES_ADMIN_USERNAME=admin
|
||||||
export HSADMINNG_SUPERUSER=import-superuser@hostsharing.net
|
export HSADMINNG_SUPERUSER=import-superuser@hostsharing.net
|
||||||
|
export HSADMINNG_CAS_SERVER=
|
||||||
|
|
||||||
export LANG=en_US.UTF-8
|
export LANG=en_US.UTF-8
|
||||||
|
@ -5,4 +5,5 @@ unset HSADMINNG_POSTGRES_RESTRICTED_USERNAME
|
|||||||
unset HSADMINNG_SUPERUSER
|
unset HSADMINNG_SUPERUSER
|
||||||
unset HSADMINNG_MIGRATION_DATA_PATH
|
unset HSADMINNG_MIGRATION_DATA_PATH
|
||||||
unset HSADMINNG_OFFICE_DATA_SQL_FILE
|
unset HSADMINNG_OFFICE_DATA_SQL_FILE
|
||||||
|
unset HSADMINNG_CAS_SERVER=
|
||||||
|
|
||||||
|
30
README.md
30
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.
|
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).
|
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=''),
|
||||||
If you still need to install some of these tools, find some hints in the next chapters.
|
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
|
### PostgreSQL Server
|
||||||
@ -666,6 +667,29 @@ These profiles mean:
|
|||||||
- **without-test-data**: no test-data is inserted
|
- **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:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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
|
### 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:
|
If you frequently need to run with a fresh database and a clean build, you can use this:
|
||||||
|
@ -31,7 +31,7 @@ def search_keywords_in_files(keywords):
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Allowed comment symbols
|
# Allowed comment symbols
|
||||||
comment_symbols = {"//", "#", ";"}
|
comment_symbols = {"//", "#", "##", "###", "####", "#####", ";"}
|
||||||
|
|
||||||
for root, dirs, files in os.walk("."):
|
for root, dirs, files in os.walk("."):
|
||||||
# Ausschließen bestimmter Verzeichnisse
|
# Ausschließen bestimmter Verzeichnisse
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'java'
|
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.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 '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
|
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-actuator'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-security'
|
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||||
implementation 'com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.10.0'
|
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.postgresql:postgresql'
|
||||||
implementation 'org.liquibase:liquibase-core'
|
implementation 'org.liquibase:liquibase-core'
|
||||||
implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.9.0'
|
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 'net.java.dev.jna:jna:5.16.0'
|
||||||
implementation 'org.modelmapper:modelmapper:3.2.2'
|
implementation 'org.modelmapper:modelmapper:3.2.2'
|
||||||
implementation 'org.iban4j:iban4j:3.2.10-RELEASE'
|
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'
|
implementation 'org.reflections:reflections:0.10.2'
|
||||||
|
|
||||||
compileOnly 'org.projectlombok:lombok'
|
compileOnly 'org.projectlombok:lombok'
|
||||||
|
BIN
doc/.images/intellij-idea-jvm-debug-run-config.png
Normal file
BIN
doc/.images/intellij-idea-jvm-debug-run-config.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 49 KiB |
@ -1,9 +1,11 @@
|
|||||||
package net.hostsharing.hsadminng;
|
package net.hostsharing.hsadminng;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
|
@OpenAPIDefinition
|
||||||
public class HsadminNgApplication {
|
public class HsadminNgApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
package net.hostsharing.hsadminng.config;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
|
|
||||||
public interface Authenticator {
|
|
||||||
|
|
||||||
String authenticate(final HttpServletRequest httpRequest);
|
|
||||||
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,71 +1,8 @@
|
|||||||
package net.hostsharing.hsadminng.config;
|
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 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}")
|
String authenticate(final HttpServletRequest httpRequest);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
}
|
@ -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<String, String>();
|
||||||
|
formData.add("service", serviceUrl);
|
||||||
|
|
||||||
|
return restTemplate.postForObject(tgtUrl, formData, String.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Document verifyServiceTicket(final String serviceTicket) throws SAXException, IOException, ParserConfigurationException {
|
||||||
|
if ( !serviceTicket.startsWith("ST-") ) {
|
||||||
|
throwBadCredentialsException("Invalid authorization ticket");
|
||||||
|
}
|
||||||
|
|
||||||
|
final var url = casServerUrl + "/cas/p3/serviceValidate" +
|
||||||
|
"?service=" + serviceUrl +
|
||||||
|
"&ticket=" + serviceTicket;
|
||||||
|
|
||||||
|
final var response = ((Supplier<String>) () -> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,36 +1,63 @@
|
|||||||
package net.hostsharing.hsadminng.config;
|
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.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.context.annotation.Profile;
|
import org.springframework.context.annotation.Profile;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.authentication.AuthenticationFilter;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@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 {
|
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
|
@Bean
|
||||||
@Profile("!test")
|
@Profile("!test")
|
||||||
public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception {
|
public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception {
|
||||||
return http
|
return http
|
||||||
.authorizeHttpRequests(authorize -> authorize
|
.authorizeHttpRequests(authorize -> authorize
|
||||||
.requestMatchers("/api/**").permitAll() // TODO.impl: implement authentication
|
.requestMatchers(PERMITTED_PATHS).permitAll()
|
||||||
.requestMatchers("/swagger-ui/**").permitAll()
|
.requestMatchers(AUTHENTICATED_PATHS).authenticated()
|
||||||
.requestMatchers("/v3/api-docs/**").permitAll()
|
.anyRequest().denyAll()
|
||||||
.requestMatchers("/actuator/**").permitAll()
|
|
||||||
.anyRequest().authenticated()
|
|
||||||
)
|
)
|
||||||
|
.addFilterBefore(authenticationFilter, AuthenticationFilter.class)
|
||||||
.csrf(AbstractHttpConfigurer::disable)
|
.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();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@Profile("!test")
|
@Profile("!test")
|
||||||
public Authenticator casServiceTicketValidator() {
|
public CasAuthenticator casServiceTicketValidator() {
|
||||||
return new CasAuthenticator();
|
return new RealCasAuthenticator();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CasAuthenticationFilter authenticationFilter(final CasAuthenticator authenticator) {
|
||||||
|
return new CasAuthenticationFilter(authenticator);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.booking.item;
|
|||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import io.micrometer.core.annotation.Timed;
|
import io.micrometer.core.annotation.Timed;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
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.api.HsBookingItemsApi;
|
||||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemInsertResource;
|
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemInsertResource;
|
||||||
@ -32,6 +33,7 @@ import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateR
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@Profile("!only-office")
|
@Profile("!only-office")
|
||||||
|
@SecurityRequirement(name = "casTicket")
|
||||||
public class HsBookingItemController implements HsBookingItemsApi {
|
public class HsBookingItemController implements HsBookingItemsApi {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.hostsharing.hsadminng.hs.booking.project;
|
package net.hostsharing.hsadminng.hs.booking.project;
|
||||||
|
|
||||||
import io.micrometer.core.annotation.Timed;
|
import io.micrometer.core.annotation.Timed;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorRepository;
|
import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorRepository;
|
||||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingProjectsApi;
|
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingProjectsApi;
|
||||||
@ -22,6 +23,7 @@ import java.util.function.BiConsumer;
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@Profile("!only-office")
|
@Profile("!only-office")
|
||||||
|
@SecurityRequirement(name = "casTicket")
|
||||||
public class HsBookingProjectController implements HsBookingProjectsApi {
|
public class HsBookingProjectController implements HsBookingProjectsApi {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.hostsharing.hsadminng.hs.hosting.asset;
|
package net.hostsharing.hsadminng.hs.hosting.asset;
|
||||||
|
|
||||||
import io.micrometer.core.annotation.Timed;
|
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.booking.item.HsBookingItemRealRepository;
|
||||||
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntitySaveProcessor;
|
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntitySaveProcessor;
|
||||||
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntityValidatorRegistry;
|
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntityValidatorRegistry;
|
||||||
@ -29,6 +30,7 @@ import java.util.function.BiConsumer;
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@Profile("!only-office")
|
@Profile("!only-office")
|
||||||
|
@SecurityRequirement(name = "casTicket")
|
||||||
public class HsHostingAssetController implements HsHostingAssetsApi {
|
public class HsHostingAssetController implements HsHostingAssetsApi {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.hostsharing.hsadminng.hs.hosting.asset;
|
package net.hostsharing.hsadminng.hs.hosting.asset;
|
||||||
|
|
||||||
import io.micrometer.core.annotation.Timed;
|
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.asset.validators.HostingAssetEntityValidatorRegistry;
|
||||||
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetPropsApi;
|
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetPropsApi;
|
||||||
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource;
|
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource;
|
||||||
@ -14,6 +15,7 @@ import java.util.Map;
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@Profile("!only-office")
|
@Profile("!only-office")
|
||||||
|
@NoSecurityRequirement
|
||||||
public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
|
public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.hostsharing.hsadminng.hs.office.bankaccount;
|
package net.hostsharing.hsadminng.hs.office.bankaccount;
|
||||||
|
|
||||||
import io.micrometer.core.annotation.Timed;
|
import io.micrometer.core.annotation.Timed;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
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.api.HsOfficeBankAccountsApi;
|
||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountInsertResource;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountInsertResource;
|
||||||
@ -18,7 +19,7 @@ import java.util.List;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@SecurityRequirement(name = "casTicket")
|
||||||
public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
|
public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.hostsharing.hsadminng.hs.office.contact;
|
package net.hostsharing.hsadminng.hs.office.contact;
|
||||||
|
|
||||||
import io.micrometer.core.annotation.Timed;
|
import io.micrometer.core.annotation.Timed;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeContactsApi;
|
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;
|
import static net.hostsharing.hsadminng.errors.Validate.validate;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@SecurityRequirement(name = "casTicket")
|
||||||
public class HsOfficeContactController implements HsOfficeContactsApi {
|
public class HsOfficeContactController implements HsOfficeContactsApi {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.hostsharing.hsadminng.hs.office.coopassets;
|
package net.hostsharing.hsadminng.hs.office.coopassets;
|
||||||
|
|
||||||
import io.micrometer.core.annotation.Timed;
|
import io.micrometer.core.annotation.Timed;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
import net.hostsharing.hsadminng.errors.MultiValidationException;
|
import net.hostsharing.hsadminng.errors.MultiValidationException;
|
||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopAssetsApi;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.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;
|
import static net.hostsharing.hsadminng.lambda.WithNonNull.withNonNull;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@SecurityRequirement(name = "casTicket")
|
||||||
public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAssetsApi {
|
public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAssetsApi {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.hostsharing.hsadminng.hs.office.coopshares;
|
package net.hostsharing.hsadminng.hs.office.coopshares;
|
||||||
|
|
||||||
import io.micrometer.core.annotation.Timed;
|
import io.micrometer.core.annotation.Timed;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
import net.hostsharing.hsadminng.errors.MultiValidationException;
|
import net.hostsharing.hsadminng.errors.MultiValidationException;
|
||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopSharesApi;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.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;
|
import static net.hostsharing.hsadminng.hs.validation.UuidResolver.resolve;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@SecurityRequirement(name = "casTicket")
|
||||||
public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopSharesApi {
|
public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopSharesApi {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.hostsharing.hsadminng.hs.office.debitor;
|
package net.hostsharing.hsadminng.hs.office.debitor;
|
||||||
|
|
||||||
import io.micrometer.core.annotation.Timed;
|
import io.micrometer.core.annotation.Timed;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountRepository;
|
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountRepository;
|
||||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealRepository;
|
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;
|
import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@SecurityRequirement(name = "casTicket")
|
||||||
public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.hostsharing.hsadminng.hs.office.membership;
|
package net.hostsharing.hsadminng.hs.office.membership;
|
||||||
|
|
||||||
import io.micrometer.core.annotation.Timed;
|
import io.micrometer.core.annotation.Timed;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
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.api.HsOfficeMembershipsApi;
|
||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipInsertResource;
|
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;
|
import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@SecurityRequirement(name = "casTicket")
|
||||||
public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
|
public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.hostsharing.hsadminng.hs.office.partner;
|
package net.hostsharing.hsadminng.hs.office.partner;
|
||||||
|
|
||||||
import io.micrometer.core.annotation.Timed;
|
import io.micrometer.core.annotation.Timed;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
import net.hostsharing.hsadminng.errors.ReferenceNotFoundException;
|
import net.hostsharing.hsadminng.errors.ReferenceNotFoundException;
|
||||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactFromResourceConverter;
|
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;
|
import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@SecurityRequirement(name = "casTicket")
|
||||||
public class HsOfficePartnerController implements HsOfficePartnersApi {
|
public class HsOfficePartnerController implements HsOfficePartnersApi {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.hostsharing.hsadminng.hs.office.person;
|
package net.hostsharing.hsadminng.hs.office.person;
|
||||||
|
|
||||||
import io.micrometer.core.annotation.Timed;
|
import io.micrometer.core.annotation.Timed;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficePersonsApi;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficePersonsApi;
|
||||||
@ -17,7 +18,7 @@ import java.util.List;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@SecurityRequirement(name = "casTicket")
|
||||||
public class HsOfficePersonController implements HsOfficePersonsApi {
|
public class HsOfficePersonController implements HsOfficePersonsApi {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.hostsharing.hsadminng.hs.office.relation;
|
package net.hostsharing.hsadminng.hs.office.relation;
|
||||||
|
|
||||||
import io.micrometer.core.annotation.Timed;
|
import io.micrometer.core.annotation.Timed;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
import net.hostsharing.hsadminng.errors.Validate;
|
import net.hostsharing.hsadminng.errors.Validate;
|
||||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
|
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;
|
import static net.hostsharing.hsadminng.mapper.KeyValueMap.from;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@SecurityRequirement(name = "casTicket")
|
||||||
public class HsOfficeRelationController implements HsOfficeRelationsApi {
|
public class HsOfficeRelationController implements HsOfficeRelationsApi {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.hostsharing.hsadminng.hs.office.sepamandate;
|
package net.hostsharing.hsadminng.hs.office.sepamandate;
|
||||||
|
|
||||||
import io.micrometer.core.annotation.Timed;
|
import io.micrometer.core.annotation.Timed;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountRepository;
|
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountRepository;
|
||||||
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository;
|
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;
|
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@SecurityRequirement(name = "casTicket")
|
||||||
public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
|
public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.hostsharing.hsadminng.rbac.grant;
|
package net.hostsharing.hsadminng.rbac.grant;
|
||||||
|
|
||||||
import io.micrometer.core.annotation.Timed;
|
import io.micrometer.core.annotation.Timed;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||||
import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacGrantsApi;
|
import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacGrantsApi;
|
||||||
@ -17,6 +18,7 @@ import java.util.List;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@SecurityRequirement(name = "casTicket")
|
||||||
public class RbacGrantController implements RbacGrantsApi {
|
public class RbacGrantController implements RbacGrantsApi {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.hostsharing.hsadminng.rbac.role;
|
package net.hostsharing.hsadminng.rbac.role;
|
||||||
|
|
||||||
import io.micrometer.core.annotation.Timed;
|
import io.micrometer.core.annotation.Timed;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||||
import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacRolesApi;
|
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;
|
import java.util.List;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@SecurityRequirement(name = "casTicket")
|
||||||
public class RbacRoleController implements RbacRolesApi {
|
public class RbacRoleController implements RbacRolesApi {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.hostsharing.hsadminng.rbac.subject;
|
package net.hostsharing.hsadminng.rbac.subject;
|
||||||
|
|
||||||
import io.micrometer.core.annotation.Timed;
|
import io.micrometer.core.annotation.Timed;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||||
import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacSubjectsApi;
|
import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacSubjectsApi;
|
||||||
@ -16,6 +17,7 @@ import java.util.List;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@SecurityRequirement(name = "casTicket")
|
||||||
public class RbacSubjectController implements RbacSubjectsApi {
|
public class RbacSubjectController implements RbacSubjectsApi {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package net.hostsharing.hsadminng.rbac.test.cust;
|
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.context.Context;
|
||||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||||
import net.hostsharing.hsadminng.test.generated.api.v1.api.TestCustomersApi;
|
import net.hostsharing.hsadminng.test.generated.api.v1.api.TestCustomersApi;
|
||||||
@ -15,6 +16,7 @@ import jakarta.persistence.PersistenceContext;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@SecurityRequirement(name = "casTicket")
|
||||||
public class TestCustomerController implements TestCustomersApi {
|
public class TestCustomerController implements TestCustomersApi {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package net.hostsharing.hsadminng.rbac.test.pac;
|
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.StrictMapper;
|
||||||
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
|
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
@ -15,6 +16,7 @@ import java.util.List;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@SecurityRequirement(name = "casTicket")
|
||||||
public class TestPackageController implements TestPackagesApi {
|
public class TestPackageController implements TestPackagesApi {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@ -6,7 +6,7 @@ components:
|
|||||||
currentSubject:
|
currentSubject:
|
||||||
name: current-subject
|
name: current-subject
|
||||||
in: header
|
in: header
|
||||||
required: true
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
description: Identifying name of the current subject (e.g. user).
|
description: Identifying name of the current subject (e.g. user).
|
||||||
|
@ -4,6 +4,7 @@ get:
|
|||||||
tags:
|
tags:
|
||||||
- testCustomers
|
- testCustomers
|
||||||
operationId: listCustomers
|
operationId: listCustomers
|
||||||
|
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: 'auth.yaml#/components/parameters/currentSubject'
|
- $ref: 'auth.yaml#/components/parameters/currentSubject'
|
||||||
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
|
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
|
||||||
|
@ -17,7 +17,7 @@ management:
|
|||||||
# HOWTO: view the effective application configuration properties:
|
# HOWTO: view the effective application configuration properties:
|
||||||
# http://localhost:8081/actuator/configprops
|
# 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:
|
endpoint:
|
||||||
env:
|
env:
|
||||||
# TODO.spec: check this, maybe set to when_authorized?
|
# TODO.spec: check this, maybe set to when_authorized?
|
||||||
@ -37,6 +37,11 @@ spring:
|
|||||||
url: ${HSADMINNG_POSTGRES_JDBC_URL}
|
url: ${HSADMINNG_POSTGRES_JDBC_URL}
|
||||||
username: postgres
|
username: postgres
|
||||||
|
|
||||||
|
data:
|
||||||
|
rest:
|
||||||
|
# do NOT implicilty expose SpringData repositories as REST-controllers
|
||||||
|
detection-strategy: annotated
|
||||||
|
|
||||||
sql:
|
sql:
|
||||||
init:
|
init:
|
||||||
mode: never
|
mode: never
|
||||||
@ -49,10 +54,6 @@ spring:
|
|||||||
liquibase:
|
liquibase:
|
||||||
contexts: ${spring.profiles.active}
|
contexts: ${spring.profiles.active}
|
||||||
|
|
||||||
# keep this in sync with test/.../application.yml
|
|
||||||
springdoc:
|
|
||||||
use-management-port: true
|
|
||||||
|
|
||||||
hsadminng:
|
hsadminng:
|
||||||
postgres:
|
postgres:
|
||||||
leakproof:
|
leakproof:
|
||||||
@ -66,3 +67,9 @@ metrics:
|
|||||||
http:
|
http:
|
||||||
server:
|
server:
|
||||||
requests: true
|
requests: true
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
org:
|
||||||
|
springframework:
|
||||||
|
security: TRACE
|
||||||
|
@ -11,7 +11,9 @@ import com.tngtech.archunit.lang.ArchRule;
|
|||||||
import com.tngtech.archunit.lang.ConditionEvents;
|
import com.tngtech.archunit.lang.ConditionEvents;
|
||||||
import com.tngtech.archunit.lang.SimpleConditionEvent;
|
import com.tngtech.archunit.lang.SimpleConditionEvent;
|
||||||
import io.micrometer.core.annotation.Timed;
|
import io.micrometer.core.annotation.Timed;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import net.hostsharing.hsadminng.HsadminNgApplication;
|
import net.hostsharing.hsadminng.HsadminNgApplication;
|
||||||
|
import net.hostsharing.hsadminng.config.NoSecurityRequirement;
|
||||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem;
|
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem;
|
||||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRbacEntity;
|
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRbacEntity;
|
||||||
import net.hostsharing.hsadminng.rbac.context.ContextBasedTest;
|
import net.hostsharing.hsadminng.rbac.context.ContextBasedTest;
|
||||||
@ -352,6 +354,15 @@ public class ArchitectureTest {
|
|||||||
static final ArchRule restControllerNaming =
|
static final ArchRule restControllerNaming =
|
||||||
classes().that().areAnnotatedWith(RestController.class).should().haveSimpleNameEndingWith("Controller");
|
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
|
@ArchTest
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
static final ArchRule restControllerMethods = classes()
|
static final ArchRule restControllerMethods = classes()
|
||||||
|
@ -19,7 +19,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
import static com.github.tomakehurst.wiremock.client.WireMock.*;
|
import static com.github.tomakehurst.wiremock.client.WireMock.*;
|
||||||
|
|
||||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
@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!
|
@ActiveProfiles("wiremock") // IMPORTANT: To test prod config, do not use test profile!
|
||||||
@Tag("generalIntegrationTest")
|
@Tag("generalIntegrationTest")
|
||||||
class CasAuthenticationFilterIntegrationTest {
|
class CasAuthenticationFilterIntegrationTest {
|
||||||
@ -37,10 +37,10 @@ class CasAuthenticationFilterIntegrationTest {
|
|||||||
private WireMockServer wireMockServer;
|
private WireMockServer wireMockServer;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldAcceptRequest() {
|
public void shouldAcceptRequestWithValidCasTicket() {
|
||||||
// given
|
// given
|
||||||
final var username = "test-user-" + randomAlphanumeric(4);
|
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()
|
.willReturn(aResponse()
|
||||||
.withStatus(200)
|
.withStatus(200)
|
||||||
.withBody("""
|
.withBody("""
|
||||||
@ -56,7 +56,7 @@ class CasAuthenticationFilterIntegrationTest {
|
|||||||
final var result = restTemplate.exchange(
|
final var result = restTemplate.exchange(
|
||||||
"http://localhost:" + this.serverPort + "/api/ping",
|
"http://localhost:" + this.serverPort + "/api/ping",
|
||||||
HttpMethod.GET,
|
HttpMethod.GET,
|
||||||
new HttpEntity<>(null, headers("Authorization", "valid")),
|
new HttpEntity<>(null, headers("Authorization", "ST-valid")),
|
||||||
String.class
|
String.class
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ class CasAuthenticationFilterIntegrationTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldRejectRequest() {
|
public void shouldRejectRequestWithInvalidCasTicket() {
|
||||||
// given
|
// given
|
||||||
wireMockServer.stubFor(get(urlEqualTo("/cas/p3/serviceValidate?service=" + serviceUrl + "&ticket=invalid"))
|
wireMockServer.stubFor(get(urlEqualTo("/cas/p3/serviceValidate?service=" + serviceUrl + "&ticket=invalid"))
|
||||||
.willReturn(aResponse()
|
.willReturn(aResponse()
|
||||||
|
@ -10,14 +10,15 @@ import static org.mockito.Mockito.mock;
|
|||||||
|
|
||||||
class CasAuthenticatorUnitTest {
|
class CasAuthenticatorUnitTest {
|
||||||
|
|
||||||
final CasAuthenticator casAuthenticator = new CasAuthenticator();
|
final RealCasAuthenticator casAuthenticator = new RealCasAuthenticator();
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void bypassesAuthenticationIfNoCasServerIsConfigured() {
|
void bypassesAuthenticationIfNoCasServerIsConfigured() {
|
||||||
|
|
||||||
// given
|
// given
|
||||||
final var request = mock(HttpServletRequest.class);
|
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
|
// when
|
||||||
final var userName = casAuthenticator.authenticate(request);
|
final var userName = casAuthenticator.authenticate(request);
|
||||||
|
@ -21,7 +21,7 @@ public class DisableSecurityConfig {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@Profile("test")
|
@Profile("test")
|
||||||
public Authenticator fakeAuthenticator() {
|
public CasAuthenticator fakeAuthenticator() {
|
||||||
return new FakeAuthenticator();
|
return new FakeCasAuthenticator();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import lombok.SneakyThrows;
|
|||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
public class FakeAuthenticator implements Authenticator {
|
public class FakeCasAuthenticator implements CasAuthenticator {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SneakyThrows
|
@SneakyThrows
|
@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.config;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Tag;
|
import org.junit.jupiter.api.Tag;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
@ -18,12 +19,16 @@ import org.springframework.test.context.ActiveProfiles;
|
|||||||
import org.springframework.test.context.TestPropertySource;
|
import org.springframework.test.context.TestPropertySource;
|
||||||
|
|
||||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
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.get;
|
||||||
|
import static com.github.tomakehurst.wiremock.client.WireMock.post;
|
||||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
||||||
|
import static java.util.Map.entry;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
@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!
|
@ActiveProfiles("wiremock") // IMPORTANT: To test prod config, do not use test profile!
|
||||||
@Tag("generalIntegrationTest")
|
@Tag("generalIntegrationTest")
|
||||||
class WebSecurityConfigIntegrationTest {
|
class WebSecurityConfigIntegrationTest {
|
||||||
@ -43,71 +48,151 @@ class WebSecurityConfigIntegrationTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private WireMockServer wireMockServer;
|
private WireMockServer wireMockServer;
|
||||||
|
|
||||||
@Test
|
@BeforeEach
|
||||||
public void shouldSupportPingEndpoint() {
|
void setUp() {
|
||||||
// given
|
wireMockServer.stubFor(get(anyUrl())
|
||||||
wireMockServer.stubFor(get(urlEqualTo("/cas/p3/serviceValidate?service=" + serviceUrl + "&ticket=test-user"))
|
|
||||||
.willReturn(aResponse()
|
.willReturn(aResponse()
|
||||||
.withStatus(200)
|
.withStatus(200)
|
||||||
.withBody("""
|
.withBody("""
|
||||||
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
|
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
|
||||||
<cas:authenticationSuccess>
|
<cas:authenticationFailure/>
|
||||||
<cas:user>test-user</cas:user>
|
|
||||||
</cas:authenticationSuccess>
|
|
||||||
</cas:serviceResponse>
|
</cas:serviceResponse>
|
||||||
""")));
|
""")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
// fake Authorization header
|
void accessToApiWithValidServiceTicketSouldBePermitted() {
|
||||||
final var headers = new HttpHeaders();
|
// given
|
||||||
headers.set("Authorization", "test-user");
|
givenCasTicketValidationResponse("ST-fake-cas-ticket", "fake-user-name");
|
||||||
|
|
||||||
// http request
|
// http request
|
||||||
final var result = restTemplate.exchange(
|
final var result = restTemplate.exchange(
|
||||||
"http://localhost:" + this.serverPort + "/api/ping",
|
"http://localhost:" + this.serverPort + "/api/ping",
|
||||||
HttpMethod.GET,
|
HttpMethod.GET,
|
||||||
new HttpEntity<>(null, headers),
|
httpHeaders(entry("Authorization", "Bearer ST-fake-cas-ticket")),
|
||||||
String.class
|
String.class
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
|
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
assertThat(result.getBody()).startsWith("pong test-user");
|
assertThat(result.getBody()).startsWith("pong fake-user-name");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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(
|
final var result = this.restTemplate.getForEntity(
|
||||||
"http://localhost:" + this.managementPort + "/actuator", Map.class);
|
"http://localhost:" + this.managementPort + "/actuator", Map.class);
|
||||||
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
|
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldSupportSwaggerUi() {
|
void accessToSwaggerUiShouldBePermitted() {
|
||||||
final var result = this.restTemplate.getForEntity(
|
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);
|
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldSupportApiDocs() {
|
void accessToApiDocsEndpointShouldBePermitted() {
|
||||||
final var result = this.restTemplate.getForEntity(
|
final var result = this.restTemplate.getForEntity(
|
||||||
"http://localhost:" + this.managementPort + "/actuator/v3/api-docs/swagger-config", String.class);
|
"http://localhost:" + this.serverPort + "/v3/api-docs/swagger-config", String.class);
|
||||||
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); // permitted but not configured
|
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
assertThat(result.getBody()).contains("\"configUrl\":\"/v3/api-docs/swagger-config\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldSupportHealthEndpoint() {
|
void accessToActuatorEndpointShouldBePermitted() {
|
||||||
final var result = this.restTemplate.getForEntity(
|
final var result = this.restTemplate.getForEntity(
|
||||||
"http://localhost:" + this.managementPort + "/actuator/health", Map.class);
|
"http://localhost:" + this.managementPort + "/actuator/health", Map.class);
|
||||||
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
|
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
assertThat(result.getBody().get("status")).isEqualTo("UP");
|
assertThat(result.getBody().get("status")).isEqualTo("UP");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
private void givenCasServiceTicketForTicketGrantingTicket(final String ticketGrantingTicket, final String serviceTicket) {
|
||||||
public void shouldSupportMetricsEndpoint() {
|
wireMockServer.stubFor(post(urlEqualTo("/cas/v1/tickets/" + ticketGrantingTicket))
|
||||||
final var result = this.restTemplate.getForEntity(
|
.withFormParam("service", equalTo(serviceUrl))
|
||||||
"http://localhost:" + this.managementPort + "/actuator/metrics", Map.class);
|
.willReturn(aResponse()
|
||||||
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
|
.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("""
|
||||||
|
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
|
||||||
|
<cas:authenticationSuccess>
|
||||||
|
<cas:user>${userName}</cas:user>
|
||||||
|
</cas:authenticationSuccess>
|
||||||
|
</cas:serviceResponse>
|
||||||
|
""".replace("${userName}", userName))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SafeVarargs
|
||||||
|
private HttpEntity<?> httpHeaders(final Map.Entry<String, String>... headerValues) {
|
||||||
|
final var headers = new HttpHeaders();
|
||||||
|
for ( Map.Entry<String, String> headerValue: headerValues ) {
|
||||||
|
headers.add(headerValue.getKey(), headerValue.getValue());
|
||||||
|
}
|
||||||
|
return new HttpEntity<>(headers);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,10 +39,6 @@ spring:
|
|||||||
change-log: classpath:/db/changelog/db.changelog-master.yaml
|
change-log: classpath:/db/changelog/db.changelog-master.yaml
|
||||||
contexts: tc,test,dev,pg_stat_statements
|
contexts: tc,test,dev,pg_stat_statements
|
||||||
|
|
||||||
# keep this in sync with main/.../application.yml
|
|
||||||
springdoc:
|
|
||||||
use-management-port: true
|
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
liquibase: WARN
|
liquibase: WARN
|
||||||
|
Loading…
x
Reference in New Issue
Block a user