Skip to content

Commit

Permalink
Merge pull request #95 from scc-digitalhub/user-secrets
Browse files Browse the repository at this point in the history
User secrets
  • Loading branch information
matteo-s authored May 3, 2024
2 parents fedd1b0 + 732e001 commit 81ea99d
Show file tree
Hide file tree
Showing 12 changed files with 204 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import it.smartcommunitylabdhub.commons.exceptions.NoSuchEntityException;
import it.smartcommunitylabdhub.commons.infrastructure.RunRunnable;
import it.smartcommunitylabdhub.commons.infrastructure.Runtime;
import it.smartcommunitylabdhub.commons.infrastructure.SecuredRunnable;
import it.smartcommunitylabdhub.commons.models.base.Executable;
import it.smartcommunitylabdhub.commons.models.base.ExecutableBaseSpec;
import it.smartcommunitylabdhub.commons.models.entities.run.Run;
Expand All @@ -19,6 +20,7 @@
import it.smartcommunitylabdhub.commons.services.entities.RunService;
import it.smartcommunitylabdhub.commons.utils.MapUtils;
import it.smartcommunitylabdhub.core.components.infrastructure.factories.runtimes.RuntimeFactory;
import it.smartcommunitylabdhub.core.components.security.SecureCredentialsHelper;
import it.smartcommunitylabdhub.core.models.entities.RunEntity;
import it.smartcommunitylabdhub.core.models.entities.TaskEntity;
import it.smartcommunitylabdhub.core.models.queries.specifications.CommonSpecification;
Expand All @@ -40,6 +42,8 @@
import org.springframework.context.event.EventListener;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.scheduling.annotation.Async;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

@Slf4j
Expand Down Expand Up @@ -175,7 +179,17 @@ public Run run(@NotNull Run run) throws NoSuchEntityException, InvalidTransactio

try {
Optional<RunRunnable> runnable = fsm.goToState(State.READY, null);

runnable.ifPresent(r -> {
//extract auth from security context to inflate secured credentials
//TODO refactor properly
if (r instanceof SecuredRunnable) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) {
((SecuredRunnable) r).setCredentials(auth);
}
}

// Dispatch Runnable event to specific event listener es (serve,job,deploy...)
eventPublisher.publishEvent(r);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package it.smartcommunitylabdhub.core.components.security;

import java.io.Serializable;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.util.Assert;

public class SecureCredentialsHelper {

public static Serializable extractCredentials(Authentication auth) {
if (auth == null) {
return null;
}

if (auth instanceof JwtAuthenticationToken) {
return convertCredentials((JwtAuthenticationToken) auth);
}

return null;
}

public static Serializable convertCredentials(JwtAuthenticationToken auth) {
Assert.notNull(auth, "auth token can not be null");
Assert.notNull(auth.getToken(), "jwt token can not be null");

//token value is the credential
return auth.getToken();
}

private SecureCredentialsHelper() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package it.smartcommunitylabdhub.commons.infrastructure;

import java.io.Serializable;

public interface SecuredRunnable {
Serializable getCredentials();
void setCredentials(Serializable credentials);
}
15 changes: 15 additions & 0 deletions modules/framework-k8s/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,21 @@
<scope>compile</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
<version>${spring-security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
<version>${spring-security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
import io.kubernetes.client.openapi.models.V1EnvFromSource;
import io.kubernetes.client.openapi.models.V1EnvVar;
import io.kubernetes.client.openapi.models.V1ResourceRequirements;
import io.kubernetes.client.openapi.models.V1Secret;
import io.kubernetes.client.openapi.models.V1Toleration;
import io.kubernetes.client.openapi.models.V1Volume;
import io.kubernetes.client.openapi.models.V1VolumeMount;
import it.smartcommunitylabdhub.commons.infrastructure.Framework;
import it.smartcommunitylabdhub.commons.models.enums.State;
import it.smartcommunitylabdhub.framework.k8s.exceptions.K8sFrameworkException;
import it.smartcommunitylabdhub.framework.k8s.kubernetes.K8sBuilderHelper;
import it.smartcommunitylabdhub.framework.k8s.kubernetes.K8sSecretHelper;
import it.smartcommunitylabdhub.framework.k8s.objects.CoreLabel;
import it.smartcommunitylabdhub.framework.k8s.objects.CoreNodeSelector;
import it.smartcommunitylabdhub.framework.k8s.objects.CoreResource;
Expand All @@ -24,6 +26,7 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
Expand All @@ -42,6 +45,7 @@ public abstract class K8sBaseFramework<T extends K8sRunnable, K extends Kubernet

protected String version;
protected K8sBuilderHelper k8sBuilderHelper;
protected K8sSecretHelper k8sSecretHelper;

protected K8sBaseFramework(ApiClient apiClient) {
Assert.notNull(apiClient, "k8s api client is required");
Expand All @@ -63,6 +67,11 @@ public void setK8sBuilderHelper(K8sBuilderHelper k8sBuilderHelper) {
this.k8sBuilderHelper = k8sBuilderHelper;
}

@Autowired
public void setK8sSecretHelper(K8sSecretHelper k8sSecretHelper) {
this.k8sSecretHelper = k8sSecretHelper;
}

@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(k8sBuilderHelper, "k8s helper is required");
Expand Down Expand Up @@ -125,13 +134,23 @@ protected Map<String, String> buildLabels(T runnable) {
return labels;
}

@SuppressWarnings("null")
protected List<V1EnvVar> buildEnv(T runnable) {
//shared envs
List<V1EnvVar> sharedEnvs = k8sBuilderHelper.getV1EnvVar();

//secretd based envs
List<V1EnvVar> secretEnvs = k8sBuilderHelper.geEnvVarsFromSecrets(runnable.getSecrets());

//secrets
V1Secret secret = buildRunSecret(runnable);
List<V1EnvVar> runSecretEnvs = new LinkedList<>();
if (secret != null && secret.getStringData() != null && !secret.getStringData().isEmpty()) {
Map<String, Set<String>> runSecretKeys = Collections.singletonMap(secret.getMetadata().getName(), secret.getStringData().keySet());
runSecretEnvs.addAll(k8sBuilderHelper.geEnvVarsFromSecrets(runSecretKeys));
runSecretEnvs.add(new V1EnvVar().name("DH_RUN_SECRET_NAME").value(secret.getMetadata().getName()));
}

// function specific envs
List<V1EnvVar> functionEnvs = runnable
.getEnvs()
Expand All @@ -144,6 +163,7 @@ protected List<V1EnvVar> buildEnv(T runnable) {
sharedEnvs.forEach(e -> envs.putIfAbsent(e.getName(), e));
secretEnvs.forEach(e -> envs.putIfAbsent(e.getName(), e));
functionEnvs.forEach(e -> envs.putIfAbsent(e.getName(), e));
runSecretEnvs.forEach(e -> envs.putIfAbsent(e.getName(), e));

return envs.values().stream().toList();
}
Expand Down Expand Up @@ -258,4 +278,33 @@ protected List<String> buildCommand(T runnable) {
protected List<String> buildArgs(T runnable) {
return Optional.ofNullable(runnable.getArgs()).map(Arrays::asList).orElse(null);
}

protected V1Secret buildRunSecret(T runnable) {
if (runnable.getCredentials() != null) {
return k8sSecretHelper.convertAuthentication(
k8sSecretHelper.getSecretName(runnable.getRuntime(), runnable.getTask(), runnable.getId()),
runnable.getCredentials()
);
}

return null;
}

@SuppressWarnings("null")
protected void storeRunSecret(V1Secret secret) throws K8sFrameworkException {
try {
k8sSecretHelper.storeSecretData(secret.getMetadata().getName(), secret.getStringData());
} catch (Exception e) {
throw new K8sFrameworkException(e.getMessage());
}
}
protected void cleanRunSecret(T runnable) {
String secretName = k8sSecretHelper.getSecretName(runnable.getRuntime(), runnable.getTask(), runnable.getId());
try {
k8sSecretHelper.deleteSecret(secretName);
} catch (Exception e) {
log.warn("Failed to delete secret {}", secretName, e);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import io.kubernetes.client.openapi.models.V1Job;
import io.kubernetes.client.openapi.models.V1JobTemplateSpec;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.openapi.models.V1Secret;
import it.smartcommunitylabdhub.commons.annotations.infrastructure.FrameworkComponent;
import it.smartcommunitylabdhub.commons.models.enums.State;
import it.smartcommunitylabdhub.framework.k8s.exceptions.K8sFrameworkException;
Expand Down Expand Up @@ -44,6 +45,12 @@ public K8sCronJobRunnable run(K8sCronJobRunnable runnable) throws K8sFrameworkEx
V1CronJob job = build(runnable);
job = create(job);

//secrets
V1Secret secret = buildRunSecret(runnable);
if (secret != null) {
storeRunSecret(secret);
}

// Update runnable state..
runnable.setState(State.RUNNING.name());

Expand All @@ -70,6 +77,8 @@ public K8sCronJobRunnable delete(K8sCronJobRunnable runnable) throws K8sFramewor
runnable.setState(State.DELETED.name());
return runnable;
}
//secrets
cleanRunSecret(runnable);

delete(job);
runnable.setState(State.DELETED.name());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import io.kubernetes.client.openapi.models.V1PodSpec;
import io.kubernetes.client.openapi.models.V1PodTemplateSpec;
import io.kubernetes.client.openapi.models.V1ResourceRequirements;
import io.kubernetes.client.openapi.models.V1Secret;
import io.kubernetes.client.openapi.models.V1Volume;
import io.kubernetes.client.openapi.models.V1VolumeMount;
import it.smartcommunitylabdhub.commons.annotations.infrastructure.FrameworkComponent;
Expand Down Expand Up @@ -47,6 +48,12 @@ public K8sDeploymentFramework(ApiClient apiClient) {
@Override
public K8sDeploymentRunnable run(K8sDeploymentRunnable runnable) throws K8sFrameworkException {
V1Deployment deployment = build(runnable);
//secrets
V1Secret secret = buildRunSecret(runnable);
if (secret != null) {
storeRunSecret(secret);
}

deployment = create(deployment);
runnable.setState(State.RUNNING.name());

Expand All @@ -62,6 +69,8 @@ public K8sDeploymentRunnable delete(K8sDeploymentRunnable runnable) throws K8sFr
runnable.setState(State.DELETED.name());
return runnable;
}
//secrets
cleanRunSecret(runnable);

delete(deployment);
runnable.setState(State.DELETED.name());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import io.kubernetes.client.openapi.models.V1PodSpec;
import io.kubernetes.client.openapi.models.V1PodTemplateSpec;
import io.kubernetes.client.openapi.models.V1ResourceRequirements;
import io.kubernetes.client.openapi.models.V1Secret;
import io.kubernetes.client.openapi.models.V1Volume;
import io.kubernetes.client.openapi.models.V1VolumeMount;
import it.smartcommunitylabdhub.commons.annotations.infrastructure.FrameworkComponent;
Expand All @@ -27,6 +28,8 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;

import com.fasterxml.jackson.core.JsonProcessingException;

@Slf4j
@FrameworkComponent(framework = K8sJobFramework.FRAMEWORK)
public class K8sJobFramework extends K8sBaseFramework<K8sJobRunnable, V1Job> {
Expand Down Expand Up @@ -54,6 +57,12 @@ public void setActiveDeadlineSeconds(int activeDeadlineSeconds) {
@Override
public K8sJobRunnable run(K8sJobRunnable runnable) throws K8sFrameworkException {
V1Job job = build(runnable);

//secrets
V1Secret secret = buildRunSecret(runnable);
if (secret != null) {
storeRunSecret(secret);
}
job = create(job);

// Update runnable state..
Expand Down Expand Up @@ -82,6 +91,8 @@ public K8sJobRunnable delete(K8sJobRunnable runnable) throws K8sFrameworkExcepti
runnable.setState(State.DELETED.name());
return runnable;
}
//secrets
cleanRunSecret(runnable);

delete(job);
runnable.setState(State.DELETED.name());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.openapi.models.V1Secret;
import it.smartcommunitylabdhub.framework.k8s.annotations.ConditionalOnKubernetes;
import jakarta.annotation.Nullable;
import jakarta.validation.constraints.NotNull;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
Expand All @@ -18,7 +19,11 @@
import java.util.Map;
import java.util.Set;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

@Component
@ConditionalOnKubernetes
Expand Down Expand Up @@ -129,6 +134,37 @@ public void storeSecretData(@NotNull String secretName, Map<String, String> data
}
}

public @Nullable V1Secret convertAuthentication(String name, AbstractAuthenticationToken auth) {
if (auth instanceof JwtAuthenticationToken) {
Jwt token = ((JwtAuthenticationToken) auth).getToken();
if (token == null) {
throw new IllegalArgumentException("missing token");
}

String jwt = token.getTokenValue();
String sub = token.getSubject();
String username = token.getClaimAsString("preferred_username");

Map<String, String> data = new HashMap<>();
data.put("DIGITALHUB_CORE_TOKEN", jwt);
data.put("DIGITALHUB_CORE_AUTH_SUB", sub);
data.put("DIGITALHUB_CORE_USER", StringUtils.hasText(username) ? username : sub);

return new V1Secret()
.metadata(new V1ObjectMeta().name(name).namespace(namespace))
.apiVersion("v1")
.kind("Secret")
.stringData(data);
}

return null;
}

// Generate and return job name
public String getSecretName(String runtime, String task, String id) {
return "sec" + "-" + runtime + "-" + task + "-" + id;
}

public record PatchBody(Object value, String path) {
private static final String REPLACE_OPERATION = "replace";

Expand Down
Loading

0 comments on commit 81ea99d

Please sign in to comment.