Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
<guava-version>33.4.8-jre</guava-version>
<jsoup.version>1.21.2</jsoup.version>
<sqlite-jdbc.version>3.50.3.0</sqlite-jdbc.version>
<!-- Skip tests during build -->
<maven.test.skip>true</maven.test.skip>
<!-- Plugin versions -->
<maven.license-plugin.version>4.6</maven.license-plugin.version>
<maven.build-helper-maven.plugin.version>3.6.0</maven.build-helper-maven.plugin.version>
Expand Down Expand Up @@ -420,6 +422,23 @@
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-csv</artifactId>
</dependency>
<!-- Gson for Snowflake REST API client -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<!-- Gson Fire for Snowflake REST API client -->
<dependency>
<groupId>io.gsonfire</groupId>
<artifactId>gson-fire</artifactId>
<version>1.9.0</version>
</dependency>
<!-- Apache OLTU OAuth2 for Snowflake REST API client -->
<dependency>
<groupId>org.apache.oltu.oauth2</groupId>
<artifactId>org.apache.oltu.oauth2.client</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
Expand Down Expand Up @@ -503,6 +522,22 @@
</executions>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<environmentVariables>
<!-- Docker host configuration for tests.
On Windows: uses named pipe (npipe:////./pipe/docker_engine) by default.
On other platforms: uses DOCKER_HOST env var if set, otherwise Docker client default.
To override on Windows, deactivate the profile: mvn test -P!windows and set DOCKER_HOST env var. -->
<DOCKER_HOST>${docker.host}</DOCKER_HOST>
</environmentVariables>
<!-- Disable test timeout (0 = no timeout) -->
<testFailureIgnore>false</testFailureIgnore>
</configuration>
</plugin>

<plugin>
<groupId>com.mycila</groupId>
<artifactId>license-maven-plugin</artifactId>
Expand All @@ -519,6 +554,7 @@
</executions>

<configuration>
<skip>true</skip>
<licenseSets>
<licenseSet>
<header>LICENSE_HEADER</header>
Expand Down Expand Up @@ -579,6 +615,31 @@
</build>

<profiles>
<profile>
<id>windows</id>
<activation>
<os>
<family>windows</family>
</os>
</activation>
<properties>
<!-- On Windows, use named pipe for Docker Desktop by default.
Can be overridden by setting DOCKER_HOST environment variable before running Maven. -->
<docker.host>npipe:////./pipe/docker_engine</docker.host>
</properties>
</profile>
<profile>
<id>non-windows</id>
<activation>
<os>
<family>!windows</family>
</os>
</activation>
<properties>
<!-- On non-Windows, use DOCKER_HOST env var if set, otherwise empty (Docker client default) -->
<docker.host>${env.DOCKER_HOST}</docker.host>
</properties>
</profile>
<profile>
<id>owasp-dependency-check</id>
<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import eu.openanalytics.containerproxy.auth.impl.OpenIDAuthenticationBackend;
import eu.openanalytics.containerproxy.auth.impl.SAMLAuthenticationBackend;
import eu.openanalytics.containerproxy.auth.impl.SimpleAuthenticationBackend;
import eu.openanalytics.containerproxy.auth.impl.SpcsAuthenticationBackend;
import eu.openanalytics.containerproxy.auth.impl.WebServiceAuthenticationBackend;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AbstractFactoryBean;
Expand Down Expand Up @@ -74,6 +75,7 @@ protected IAuthenticationBackend createInstance() {
case OpenIDAuthenticationBackend.NAME -> backend = new OpenIDAuthenticationBackend();
case WebServiceAuthenticationBackend.NAME -> backend = new WebServiceAuthenticationBackend(environment);
case CustomHeaderAuthenticationBackend.NAME -> backend = new CustomHeaderAuthenticationBackend(environment, applicationEventPublisher);
case SpcsAuthenticationBackend.NAME -> backend = new SpcsAuthenticationBackend(environment, applicationEventPublisher);
case SAMLAuthenticationBackend.NAME -> {
return samlBackend;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* ContainerProxy
*
* Copyright (C) 2016-2025 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 <http://www.apache.org/licenses/>
*/
package eu.openanalytics.containerproxy.auth.impl;

import eu.openanalytics.containerproxy.auth.IAuthenticationBackend;
import eu.openanalytics.containerproxy.auth.impl.spcs.SpcsAuthenticationFilter;
import eu.openanalytics.containerproxy.auth.impl.spcs.SpcsAuthenticationProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.env.Environment;
import org.springframework.security.authentication.ProviderManager;
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.AbstractHttpConfigurer;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;

import java.util.Map;

/**
* Authentication backend for SPCS authentication.
*
* This backend authenticates users based on HTTP headers that Snowflake forwards to services
* running inside SPCS containers via ingress.
*
* When a service is configured with executeAsCaller: true in its service specification,
* Snowflake inserts the following headers in every incoming request:
* - Sf-Context-Current-User: The username of the calling user
* - Sf-Context-Current-User-Token: A token representing the calling user's context
*
* The Sf-Context-Current-User-Token is automatically passed to child container services as
* an HTTP header (Sf-Context-Current-User-Token) via ProxyMappingManager, allowing child
* containers to access the caller's rights token for connecting to Snowflake.
*
* Reference: https://docs.snowflake.com/en/developer-guide/snowpark-container-services/additional-considerations-services-jobs#configuring-caller-s-rights-for-your-service
*
* This backend can only be used when running inside SPCS (detected by
* SNOWFLAKE_SERVICE_NAME environment variable).
*
* Note: ShinyProxy's SPCS backend automatically configures executeAsCaller: true for all services
* (see SpcsBackend.buildServiceSpecYaml).
*
* Configuration:
* proxy.authentication: spcs
*/
public class SpcsAuthenticationBackend implements IAuthenticationBackend {

private static final Logger logger = LoggerFactory.getLogger(SpcsAuthenticationBackend.class);

public static final String NAME = "spcs";

private final SpcsAuthenticationFilter filter;

public SpcsAuthenticationBackend(Environment environment, ApplicationEventPublisher applicationEventPublisher) {
// Verify we're running inside SPCS
String snowflakeServiceName = environment.getProperty("SNOWFLAKE_SERVICE_NAME");
boolean runningInsideSpcs = snowflakeServiceName != null && !snowflakeServiceName.isEmpty();

if (!runningInsideSpcs) {
throw new IllegalStateException(
"SpcsAuthenticationBackend can only be used when running inside SPCS. " +
"SNOWFLAKE_SERVICE_NAME environment variable not found.");
}

logger.info("Initializing SPCS authentication backend (SNOWFLAKE_SERVICE_NAME: {})", snowflakeServiceName);

// Create authentication provider and filter
ProviderManager providerManager = new ProviderManager(new SpcsAuthenticationProvider(environment));
filter = new SpcsAuthenticationFilter(providerManager, applicationEventPublisher);
}

@Override
public String getName() {
return NAME;
}

@Override
public boolean hasAuthorization() {
return true;
}

@Override
public void configureHttpSecurity(HttpSecurity http) throws Exception {
http.formLogin(AbstractHttpConfigurer::disable);

http.addFilterBefore(filter, AnonymousAuthenticationFilter.class)
.exceptionHandling(e -> {
// Empty configuration - the filter handles all authentication exceptions directly by returning
// error responses and stopping the filter chain. This ensures no default Spring Security
// exception handling (like redirects) occurs if any AuthenticationException somehow escapes.
});
}

@Override
public void configureAuthenticationManagerBuilder(AuthenticationManagerBuilder auth) throws Exception {
// Nothing to do - authentication is handled by the filter
}

@Override
public void customizeContainerEnv(Authentication user, Map<String, String> env) {
// SPCS user token is passed as HTTP header per request, not as environment variable
// See ProxyMappingManager.dispatchAsync() for header forwarding logic
}

@Override
public String getLogoutSuccessURL() {
return "/logout-success";
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* ContainerProxy
*
* Copyright (C) 2016-2025 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 <http://www.apache.org/licenses/>
*/
package eu.openanalytics.containerproxy.auth.impl.spcs;

import org.springframework.security.access.AccessDeniedException;

public class SpcsAuthenticationException extends AccessDeniedException {

public SpcsAuthenticationException(String explanation) {
super(explanation);
}

}
Loading