diff --git a/.github/workflows/workflows.yaml b/.github/workflows/workflows.yaml deleted file mode 100644 index 17d70d6f..00000000 --- a/.github/workflows/workflows.yaml +++ /dev/null @@ -1,77 +0,0 @@ -name: Tests - -on: - push: - workflow_dispatch: - -env: - ITEST_ECS_REGION: dummy-region - ITEST_ECS_NAME: dummy-name - ITEST_ECS_SECURITY_GROUPS: dummy-sg - ITEST_ECS_SUBNETS: dummy-subnets - -jobs: - tests: - runs-on: ubuntu-latest - strategy: - matrix: - java: - - 17 - kubernetes: - - 'v1.24.17' - - 'v1.25.16' - - 'v1.26.15' - - 'v1.27.13' - - 'v1.28.9' - - 'v1.29.4' - - 'v1.30.0' - - steps: - - uses: actions/checkout@v2 - - name: Set up JDK - uses: actions/setup-java@v1 - with: - java-version: ${{ matrix.java }} - - name: Cache Maven packages - uses: actions/cache@v2 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - name: Setup Minikube - uses: manusa/actions-setup-minikube@v2.7.2 - with: - minikube version: 'v1.33.0' - kubernetes version: ${{ matrix.kubernetes }} - github token: ${{ secrets.GITHUB_TOKEN }} - container runtime: containerd - driver: docker - - name: Setup Docker - run: sudo apt-get -qq -y install conntrack socat ; nohup socat TCP-LISTEN:2375,reuseaddr,fork UNIX-CONNECT:/var/run/docker.sock & - - name: Setup Docker Swarm - run: docker swarm init - - name: Pull Image - run: docker pull openanalytics/shinyproxy-integration-test-app - - name: Run redis - run: docker run -d -p 6379:6379 redis - - name: Build with Maven - run: mvn -B -U clean install -DskipTests - - name: Run Tests - run: mvn -B test - - dependency: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up JDK - uses: actions/setup-java@v1 - with: - java-version: 17 - - name: Run Dependency Check - run: mvn -B -Powasp-dependency-check verify -DskipTests - - name: Archive code coverage results - uses: actions/upload-artifact@v2 - with: - name: dependency-check-report - path: target/dependency-check-report.html diff --git a/.gitignore b/.gitignore index f66ec70e..6f1559f1 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ logs .project .classpath .settings +.vscode diff --git a/src/main/java/eu/openanalytics/containerproxy/auth/AuthenticationBackendFactory.java b/src/main/java/eu/openanalytics/containerproxy/auth/AuthenticationBackendFactory.java index 6c1f5111..c159d46a 100644 --- a/src/main/java/eu/openanalytics/containerproxy/auth/AuthenticationBackendFactory.java +++ b/src/main/java/eu/openanalytics/containerproxy/auth/AuthenticationBackendFactory.java @@ -26,6 +26,7 @@ import eu.openanalytics.containerproxy.auth.impl.SAMLAuthenticationBackend; import eu.openanalytics.containerproxy.auth.impl.SimpleAuthenticationBackend; import eu.openanalytics.containerproxy.auth.impl.WebServiceAuthenticationBackend; +import eu.openanalytics.containerproxy.auth.impl.CustomHeaderAuthenticationBackend; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.AbstractFactoryBean; import org.springframework.context.ApplicationContext; @@ -72,6 +73,7 @@ protected IAuthenticationBackend createInstance() { case LDAPAuthenticationBackend.NAME -> backend = new LDAPAuthenticationBackend(); case OpenIDAuthenticationBackend.NAME -> backend = new OpenIDAuthenticationBackend(); case WebServiceAuthenticationBackend.NAME -> backend = new WebServiceAuthenticationBackend(environment); + case CustomHeaderAuthenticationBackend.NAME -> backend = new CustomHeaderAuthenticationBackend(environment); case SAMLAuthenticationBackend.NAME -> { return samlBackend; } diff --git a/src/main/java/eu/openanalytics/containerproxy/auth/impl/CustomHeaderAuthenticationBackend.java b/src/main/java/eu/openanalytics/containerproxy/auth/impl/CustomHeaderAuthenticationBackend.java new file mode 100644 index 00000000..ce724a4f --- /dev/null +++ b/src/main/java/eu/openanalytics/containerproxy/auth/impl/CustomHeaderAuthenticationBackend.java @@ -0,0 +1,92 @@ +/** + * ContainerProxy + * + * Copyright (C) 2016-2024 Open Analytics + * + * =========================================================================== + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License as published by + * The Apache Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Apache License for more details. + * + * You should have received a copy of the Apache License + * along with this program. If not, see + */ +package eu.openanalytics.containerproxy.auth.impl; + +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer.AuthorizedUrl; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.stereotype.Component; + +import eu.openanalytics.containerproxy.auth.IAuthenticationBackend; +import eu.openanalytics.containerproxy.auth.impl.customHeader.CustomHeaderAuthenticationFilter; +import eu.openanalytics.containerproxy.auth.impl.customHeader.CustomHeaderAuthenticationToken; + +@Component +public class CustomHeaderAuthenticationBackend implements IAuthenticationBackend{ + + public final static String NAME = "customHeader"; + + @Override + public String getName() { + return NAME; + } + + @Override + public boolean hasAuthorization() { + return false; + } + + @Override + public void configureHttpSecurity(HttpSecurity http) throws Exception { + http.formLogin().disable(); + + http.addFilterBefore(new CustomHeaderAuthenticationFilter(), BasicAuthenticationFilter.class); + } + + @Override + public void configureAuthenticationManagerBuilder(AuthenticationManagerBuilder auth) throws Exception { + // Configure a custom Authentication Provider + CustomHeaderAuthenticationProvider authenticationProvider = new CustomHeaderAuthenticationProvider(); + + auth.authenticationProvider(authenticationProvider); + } + + + public class CustomHeaderAuthenticationProvider implements + org.springframework.security.authentication.AuthenticationProvider, + org.springframework.beans.factory.InitializingBean { + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + CustomHeaderAuthenticationToken token = (CustomHeaderAuthenticationToken) authentication; + if (token.isValid()) + return new CustomHeaderAuthenticationToken(token.getPrincipal().toString()); + + throw new BadCredentialsException("Invalid username"); + + } + + @Override + public boolean supports(Class authentication) { + return false; + } + + @Override + public void afterPropertiesSet() throws Exception { + + } + } + +} diff --git a/src/main/java/eu/openanalytics/containerproxy/auth/impl/customHeader/CustomHeaderAuthenticationFilter.java b/src/main/java/eu/openanalytics/containerproxy/auth/impl/customHeader/CustomHeaderAuthenticationFilter.java new file mode 100644 index 00000000..15fe646b --- /dev/null +++ b/src/main/java/eu/openanalytics/containerproxy/auth/impl/customHeader/CustomHeaderAuthenticationFilter.java @@ -0,0 +1,72 @@ +/** + * ContainerProxy + * + * Copyright (C) 2016-2024 Open Analytics + * + * =========================================================================== + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License as published by + * The Apache Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Apache License for more details. + * + * You should have received a copy of the Apache License + * along with this program. If not, see + */ + +package eu.openanalytics.containerproxy.auth.impl.customHeader; + +import java.io.IOException; + +import javax.annotation.Nonnull; +import javax.inject.Inject; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.springframework.context.annotation.Lazy; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.OrRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class CustomHeaderAuthenticationFilter extends OncePerRequestFilter { + + private final Logger log = LogManager.getLogger(CustomHeaderAuthenticationFilter.class); + + private final RequestMatcher requestMatcher = new OrRequestMatcher( + new AntPathRequestMatcher("/app/**"), + new AntPathRequestMatcher("/app_i/**"), + new AntPathRequestMatcher("/**")); + + @Override + protected void doFilterInternal(@Nonnull HttpServletRequest request, @Nonnull HttpServletResponse response, + @Nonnull FilterChain chain) throws ServletException, IOException { + + log.debug(String.format("CustomHeaderAuthenticationFilter CALLED")); + if (requestMatcher.matches(request)) { + String remoteUser = request.getHeader("REMOTE_USER"); + log.debug(String.format("CustomHeaderAuthenticationFilter REMOTE_USER: %s", remoteUser)); + try { + Authentication authRequest = new CustomHeaderAuthenticationToken(remoteUser); + SecurityContextHolder.getContext().setAuthentication(authRequest); + } catch (AuthenticationException e) { + throw e; + } + + } + chain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/eu/openanalytics/containerproxy/auth/impl/customHeader/CustomHeaderAuthenticationToken.java b/src/main/java/eu/openanalytics/containerproxy/auth/impl/customHeader/CustomHeaderAuthenticationToken.java new file mode 100644 index 00000000..0b843a46 --- /dev/null +++ b/src/main/java/eu/openanalytics/containerproxy/auth/impl/customHeader/CustomHeaderAuthenticationToken.java @@ -0,0 +1,57 @@ +/** + * ContainerProxy + * + * Copyright (C) 2016-2024 Open Analytics + * + * =========================================================================== + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License as published by + * The Apache Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Apache License for more details. + * + * You should have received a copy of the Apache License + * along with this program. If not, see + */ +package eu.openanalytics.containerproxy.auth.impl.customHeader; + +import org.springframework.security.authentication.AbstractAuthenticationToken; + +public class CustomHeaderAuthenticationToken extends AbstractAuthenticationToken{ + + private final String remoteUser; + + public CustomHeaderAuthenticationToken(String userName) { + super(null); + this.remoteUser = userName; + } + + public boolean isValid() { + if (remoteUser != null && !remoteUser.isEmpty()) + return true; + return false; + } + + + @Override + public Object getPrincipal() { + return remoteUser; + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public String getName() { + return this.remoteUser; + } + +} + diff --git a/src/main/java/eu/openanalytics/containerproxy/auth/impl/customHeader/CustomHeaderConfiguration.java b/src/main/java/eu/openanalytics/containerproxy/auth/impl/customHeader/CustomHeaderConfiguration.java new file mode 100644 index 00000000..ea9217b9 --- /dev/null +++ b/src/main/java/eu/openanalytics/containerproxy/auth/impl/customHeader/CustomHeaderConfiguration.java @@ -0,0 +1,41 @@ +/** + * ContainerProxy + * + * Copyright (C) 2016-2024 Open Analytics + * + * =========================================================================== + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License as published by + * The Apache Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Apache License for more details. + * + * You should have received a copy of the Apache License + * along with this program. If not, see + */ +package eu.openanalytics.containerproxy.auth.impl.customHeader; + + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnProperty(name = "proxy.authentication", havingValue = "customHeader") +public class CustomHeaderConfiguration { + + public static final String REG_ID = "shinyproxy"; + public static final String PROP_CUSTOM_HEADER = "proxy.customHeader"; + + + @Bean + public CustomHeaderAuthenticationFilter customHeaderAuthorizeFilter() { + return new CustomHeaderAuthenticationFilter(); + } + +} diff --git a/src/test/resources/application-test-customHeader-auth.yml b/src/test/resources/application-test-customHeader-auth.yml new file mode 100644 index 00000000..d1c02c73 --- /dev/null +++ b/src/test/resources/application-test-customHeader-auth.yml @@ -0,0 +1,18 @@ +spring: + session: + store-type: none + data: + redis: + repositories: + enabled: false +proxy: + authentication: customHeader + customHeader: REMOTE_USER + specs: + - id: 01_hello + container-specs: + - image: "openanalytics/shinyproxy-demo" + cmd: ["R", "-e", "shinyproxy::run_01_hello()"] + port-mapping: + - name: default + port: 3838