diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/credential/DefaultCredentialProviderService.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/credential/DefaultCredentialProviderService.java index 7de096402b7e..ee04212dd265 100644 --- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/credential/DefaultCredentialProviderService.java +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/credential/DefaultCredentialProviderService.java @@ -18,17 +18,15 @@ import com.google.common.util.concurrent.AbstractIdleService; import io.cdap.cdap.common.conf.CConfiguration; -import io.cdap.cdap.common.namespace.NamespaceAdmin; -import io.cdap.cdap.proto.NamespaceMeta; import io.cdap.cdap.proto.credential.CredentialIdentity; import io.cdap.cdap.proto.credential.CredentialProfile; +import io.cdap.cdap.proto.credential.CredentialProvisionContext; import io.cdap.cdap.proto.credential.CredentialProvisioningException; import io.cdap.cdap.proto.credential.IdentityValidationException; import io.cdap.cdap.proto.credential.NotFoundException; import io.cdap.cdap.proto.credential.ProvisionedCredential; import io.cdap.cdap.proto.id.CredentialIdentityId; import io.cdap.cdap.proto.id.CredentialProfileId; -import io.cdap.cdap.proto.id.NamespaceId; import io.cdap.cdap.proto.security.StandardPermission; import io.cdap.cdap.security.spi.authorization.ContextAccessEnforcer; import io.cdap.cdap.security.spi.credential.CredentialProvider; @@ -51,21 +49,18 @@ public class DefaultCredentialProviderService extends AbstractIdleService private final Map credentialProviders; private final CredentialIdentityManager credentialIdentityManager; private final CredentialProfileManager credentialProfileManager; - private final NamespaceAdmin namespaceAdmin; @Inject DefaultCredentialProviderService(CConfiguration cConf, ContextAccessEnforcer contextAccessEnforcer, CredentialProviderLoader credentialProviderLoader, CredentialIdentityManager credentialIdentityManager, - CredentialProfileManager credentialProfileManager, - NamespaceAdmin namespaceAdmin) { + CredentialProfileManager credentialProfileManager) { this.cConf = cConf; this.contextAccessEnforcer = contextAccessEnforcer; this.credentialProviders = credentialProviderLoader.loadCredentialProviders(); this.credentialIdentityManager = credentialIdentityManager; this.credentialProfileManager = credentialProfileManager; - this.namespaceAdmin = namespaceAdmin; } @Override @@ -87,6 +82,7 @@ protected void shutDown() throws Exception { * * @param namespace The identity namespace. * @param identityName The identity name. + * @param context The context to use for provisioning. * @return A provisioned credential. * @throws CredentialProvisioningException If provisioning fails in the extension. * @throws IOException If any transport errors occur. @@ -94,48 +90,43 @@ protected void shutDown() throws Exception { */ @Override public ProvisionedCredential provision(String namespace, String identityName, - String scopes) + CredentialProvisionContext context) throws CredentialProvisioningException, IOException, NotFoundException { CredentialIdentityId identityId = new CredentialIdentityId(namespace, identityName); contextAccessEnforcer.enforce(identityId, StandardPermission.USE); - NamespaceMeta namespaceMeta; - try { - namespaceMeta = namespaceAdmin.get(new NamespaceId(namespace)); - } catch (Exception e) { - throw new IOException(String.format("Failed to get namespace '%s' metadata", - namespace), e); - } Optional optIdentity = credentialIdentityManager.get(identityId); if (!optIdentity.isPresent()) { throw new NotFoundException(String.format("Credential identity '%s' was not found.", identityId)); } CredentialIdentity identity = optIdentity.get(); - return validateAndProvisionIdentity(namespaceMeta, identity, scopes); + return validateAndProvisionIdentity(namespace, identity, context); } /** * Validates an identity. * - * @param namespaceMeta The identity namespace metadata. - * @param identity The identity to validate. + * @param namespace The identity namespace. + * @param identity The identity to validate. + * @param context The context to use for provisioning. * @throws IdentityValidationException If identity validation fails in the extension. * @throws IOException If any transport errors occur. * @throws NotFoundException If the identity or profile are not found. */ @Override - public void validateIdentity(NamespaceMeta namespaceMeta, CredentialIdentity identity) + public void validateIdentity(String namespace, CredentialIdentity identity, + CredentialProvisionContext context) throws IdentityValidationException, IOException, NotFoundException { try { - validateAndProvisionIdentity(namespaceMeta, identity, null); + validateAndProvisionIdentity(namespace, identity, context); } catch (CredentialProvisioningException e) { throw new IdentityValidationException(e); } } - private ProvisionedCredential validateAndProvisionIdentity(NamespaceMeta namespaceMeta, - CredentialIdentity identity, String scopes) + private ProvisionedCredential validateAndProvisionIdentity(String namespace, + CredentialIdentity identity, CredentialProvisionContext context) throws CredentialProvisioningException, IOException, NotFoundException { CredentialProfileId profileId = new CredentialProfileId(identity.getProfileNamespace(), identity.getProfileName()); @@ -153,7 +144,6 @@ private ProvisionedCredential validateAndProvisionIdentity(NamespaceMeta namespa String.format("Unsupported credential provider type '%s'", providerType)); } // Provision and return the credential. - return credentialProviders.get(providerType).provision(namespaceMeta, profile, - identity, scopes); + return credentialProviders.get(providerType).provision(namespace, profile, identity, context); } } diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/credential/RemoteCredentialProvider.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/credential/RemoteCredentialProvider.java index 5491452d9d60..1c82a7e9aae0 100644 --- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/credential/RemoteCredentialProvider.java +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/credential/RemoteCredentialProvider.java @@ -24,25 +24,26 @@ import io.cdap.cdap.common.internal.remote.RemoteClient; import io.cdap.cdap.common.internal.remote.RemoteClientFactory; import io.cdap.cdap.proto.BasicThrowable; -import io.cdap.cdap.proto.NamespaceMeta; import io.cdap.cdap.proto.codec.BasicThrowableCodec; import io.cdap.cdap.proto.credential.CredentialIdentity; import io.cdap.cdap.proto.credential.CredentialProvider; +import io.cdap.cdap.proto.credential.CredentialProvisionContext; import io.cdap.cdap.proto.credential.CredentialProvisioningException; import io.cdap.cdap.proto.credential.IdentityValidationException; import io.cdap.cdap.proto.credential.NotFoundException; import io.cdap.cdap.proto.credential.ProvisionedCredential; +import io.cdap.cdap.proto.credential.ValidateIdentityRequest; import io.cdap.common.http.HttpMethod; import io.cdap.common.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import java.io.IOException; -import joptsimple.internal.Strings; /** - * Remote implementation for {@link CredentialProvider} used in - * {@link io.cdap.cdap.common.conf.Constants.ArtifactLocalizer}. + * Remote implementation for {@link CredentialProvider} used in {@link + * io.cdap.cdap.common.conf.Constants.ArtifactLocalizer}. */ public class RemoteCredentialProvider implements CredentialProvider { + private static final Gson GSON = new GsonBuilder().registerTypeAdapter(BasicThrowable.class, new BasicThrowableCodec()).create(); private final RemoteClient remoteClient; @@ -50,7 +51,7 @@ public class RemoteCredentialProvider implements CredentialProvider { /** * Construct the {@link RemoteCredentialProvider}. * - * @param remoteClientFactory A factory to create {@link RemoteClient}. + * @param remoteClientFactory A factory to create {@link RemoteClient}. * @param internalAuthenticator An authenticator to propagate internal identity headers. */ public RemoteCredentialProvider(RemoteClientFactory remoteClientFactory, @@ -64,24 +65,22 @@ public RemoteCredentialProvider(RemoteClientFactory remoteClientFactory, /** * Provisions a short-lived credential for the provided identity using the provided identity. * - * @param namespace The identity namespace. + * @param namespace The identity namespace. * @param identityName The identity name. - * @param scopes A comma separated list of OAuth scopes requested. + * @param context The context to use for provisioning. * @return A short-lived credential. * @throws CredentialProvisioningException If provisioning the credential fails. - * @throws IOException If any transport errors occur. - * @throws NotFoundException If the profile or identity are not found. + * @throws IOException If any transport errors occur. + * @throws NotFoundException If the profile or identity are not found. */ @Override public ProvisionedCredential provision(String namespace, String identityName, - String scopes) throws CredentialProvisioningException, IOException, NotFoundException { + CredentialProvisionContext context) + throws CredentialProvisioningException, IOException, NotFoundException { String url = String.format("namespaces/%s/credentials/identities/%s/provision", namespace, identityName); - if (!Strings.isNullOrEmpty(scopes)) { - url = String.format("%s?scopes=%s", url, scopes); - } io.cdap.common.http.HttpRequest tokenRequest = - remoteClient.requestBuilder(HttpMethod.GET, url).build(); + remoteClient.requestBuilder(HttpMethod.POST, url).withBody(GSON.toJson(context)).build(); HttpResponse response = remoteClient.execute(tokenRequest, Idempotency.NONE); if (response.getResponseCode() == HttpResponseStatus.NOT_FOUND.code()) { @@ -102,20 +101,21 @@ public ProvisionedCredential provision(String namespace, String identityName, /** * Validates the provided identity. * - * @param namespaceMeta The identity namespace metadata. - * @param identity The identity to validate. + * @param namespace The identity namespace. + * @param identity The identity to validate. + * @param context The context to use for provisioning. * @throws IdentityValidationException If validation fails. - * @throws IOException If any transport errors occur. - * @throws NotFoundException If the profile is not found. + * @throws IOException If any transport errors occur. + * @throws NotFoundException If the profile is not found. */ @Override - public void validateIdentity(NamespaceMeta namespaceMeta, CredentialIdentity identity) + public void validateIdentity(String namespace, CredentialIdentity identity, + CredentialProvisionContext context) throws IdentityValidationException, IOException, NotFoundException { - String url = String.format("namespaces/%s/credentials/identities/validate", - namespaceMeta.getNamespaceId().getNamespace()); + String url = String.format("namespaces/%s/credentials/identities/validate", namespace); io.cdap.common.http.HttpRequest tokenRequest = remoteClient.requestBuilder(HttpMethod.POST, url) - .withBody(GSON.toJson(identity)).build(); + .withBody(GSON.toJson(new ValidateIdentityRequest(identity, context))).build(); HttpResponse response = remoteClient.execute(tokenRequest, Idempotency.NONE); if (response.getResponseCode() == HttpResponseStatus.NOT_FOUND.code()) { throw new NotFoundException(String.format("Credential Profile %s Not Found.", diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/credential/handler/CredentialProviderHttpHandler.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/credential/handler/CredentialProviderHttpHandler.java index d6fb6fcff74b..df63577e1e1a 100644 --- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/credential/handler/CredentialProviderHttpHandler.java +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/credential/handler/CredentialProviderHttpHandler.java @@ -30,13 +30,13 @@ import io.cdap.cdap.common.namespace.NamespaceQueryAdmin; import io.cdap.cdap.internal.credential.CredentialIdentityManager; import io.cdap.cdap.internal.credential.CredentialProfileManager; -import io.cdap.cdap.proto.NamespaceMeta; import io.cdap.cdap.proto.credential.CreateCredentialIdentityRequest; import io.cdap.cdap.proto.credential.CreateCredentialProfileRequest; import io.cdap.cdap.proto.credential.CredentialIdentity; import io.cdap.cdap.proto.credential.CredentialProfile; import io.cdap.cdap.proto.credential.CredentialProvider; import io.cdap.cdap.proto.credential.IdentityValidationException; +import io.cdap.cdap.proto.credential.ValidateIdentityRequest; import io.cdap.cdap.proto.element.EntityType; import io.cdap.cdap.proto.id.CredentialIdentityId; import io.cdap.cdap.proto.id.CredentialProfileId; @@ -120,16 +120,11 @@ public void listProviders(HttpRequest request, HttpResponder responder) { public void validateIdentity(FullHttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespace) throws BadRequestException, NotFoundException, IOException { - CredentialIdentity identity = deserializeRequestContent(request, CredentialIdentity.class); - NamespaceMeta namespaceMeta; - try { - namespaceMeta = namespaceQueryAdmin.get(new NamespaceId(namespace)); - } catch (Exception e) { - throw new IOException(String.format("Failed to get namespace '%s' metadata", - namespace), e); - } + ValidateIdentityRequest identityValidationRequest = deserializeRequestContent(request, + ValidateIdentityRequest.class); + CredentialIdentity identity = identityValidationRequest.getIdentity(); if (Strings.isNullOrEmpty(identity.getIdentity())) { - throw new BadRequestException("Identity cannot be null or empty."); + throw new BadRequestException("Identity cannot be null or empty."); } if (!identity.getProfileNamespace().equals(namespace) && !identity.getProfileNamespace().equals(NamespaceId.SYSTEM.getNamespace())) { @@ -137,7 +132,8 @@ public void validateIdentity(FullHttpRequest request, HttpResponder responder, + "associated with a profile in a different namespace."); } try { - credentialProvider.validateIdentity(namespaceMeta, identity); + credentialProvider + .validateIdentity(namespace, identity, identityValidationRequest.getContext()); } catch (IdentityValidationException e) { throw new BadRequestException(String.format("Identity failed validation with error: %s", e.getMessage()), e); diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/credential/handler/CredentialProviderHttpHandlerInternal.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/credential/handler/CredentialProviderHttpHandlerInternal.java index 569f2ebca2cb..3f85944d6fa4 100644 --- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/credential/handler/CredentialProviderHttpHandlerInternal.java +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/credential/handler/CredentialProviderHttpHandlerInternal.java @@ -18,24 +18,30 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; import com.google.inject.Singleton; +import io.cdap.cdap.common.BadRequestException; import io.cdap.cdap.common.NotFoundException; import io.cdap.cdap.common.conf.Constants; import io.cdap.cdap.proto.BasicThrowable; import io.cdap.cdap.proto.codec.BasicThrowableCodec; import io.cdap.cdap.proto.credential.CredentialProvider; +import io.cdap.cdap.proto.credential.CredentialProvisionContext; import io.cdap.cdap.proto.credential.CredentialProvisioningException; import io.cdap.http.AbstractHttpHandler; import io.cdap.http.HttpHandler; import io.cdap.http.HttpResponder; -import io.netty.handler.codec.http.HttpRequest; +import io.netty.buffer.ByteBufInputStream; +import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpResponseStatus; import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; import javax.inject.Inject; -import javax.ws.rs.GET; +import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; -import javax.ws.rs.QueryParam; /** * Internal {@link HttpHandler} for credential providers. @@ -61,22 +67,37 @@ public class CredentialProviderHttpHandlerInternal extends AbstractHttpHandler { * @param responder The HTTP responder. * @param namespace The namespace of the identity for which to provision a credential. * @param identityName The name of the identity for which to provision a credential. - * @param scopes A comma separated list of OAuth scopes requested. * @throws CredentialProvisioningException If provisioning fails. * @throws IOException If transport errors occur. * @throws NotFoundException If the identity or associated profile are not found. */ - @GET + @POST @Path("/namespaces/{namespace-id}/credentials/identities/{identity-name}/provision") - public void provisionCredential(HttpRequest request, HttpResponder responder, - @PathParam("namespace-id") String namespace, @PathParam("identity-name") String identityName, - @QueryParam("scopes") String scopes) - throws CredentialProvisioningException, IOException, NotFoundException { + public void provisionCredential(FullHttpRequest request, HttpResponder responder, + @PathParam("namespace-id") String namespace, @PathParam("identity-name") String identityName) + throws BadRequestException, CredentialProvisioningException, IOException, NotFoundException { + CredentialProvisionContext context = deserializeRequestContent(request, + CredentialProvisionContext.class); try { responder.sendJson(HttpResponseStatus.OK, - GSON.toJson(credentialProvider.provision(namespace, identityName, scopes))); + GSON.toJson(credentialProvider.provision(namespace, identityName, context))); } catch (io.cdap.cdap.proto.credential.NotFoundException e) { throw new NotFoundException(e.getMessage()); } } + + private T deserializeRequestContent(FullHttpRequest request, Class clazz) + throws BadRequestException { + try (Reader reader = new InputStreamReader(new ByteBufInputStream(request.content()), + StandardCharsets.UTF_8)) { + T content = GSON.fromJson(reader, clazz); + if (content == null) { + throw new BadRequestException("No request object provided; expected class " + + clazz.getName()); + } + return content; + } catch (JsonSyntaxException | IOException e) { + throw new BadRequestException("Unable to parse request: " + e.getMessage(), e); + } + } } diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/namespace/credential/DefaultNamespaceCredentialProviderService.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/namespace/credential/DefaultNamespaceCredentialProviderService.java index da15d7355a83..b5651f65c01c 100644 --- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/namespace/credential/DefaultNamespaceCredentialProviderService.java +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/namespace/credential/DefaultNamespaceCredentialProviderService.java @@ -28,9 +28,12 @@ import io.cdap.cdap.master.environment.MasterEnvironments; import io.cdap.cdap.master.spi.environment.MasterEnvironment; import io.cdap.cdap.proto.NamespaceMeta; +import io.cdap.cdap.proto.credential.CredentialIdentity; import io.cdap.cdap.proto.credential.CredentialProfile; import io.cdap.cdap.proto.credential.CredentialProvider; +import io.cdap.cdap.proto.credential.CredentialProvisionContext; import io.cdap.cdap.proto.credential.CredentialProvisioningException; +import io.cdap.cdap.proto.credential.IdentityValidationException; import io.cdap.cdap.proto.credential.NotFoundException; import io.cdap.cdap.proto.credential.ProvisionedCredential; import io.cdap.cdap.proto.id.CredentialProfileId; @@ -71,11 +74,11 @@ public class DefaultNamespaceCredentialProviderService extends AbstractIdleServi * Provisions a short-lived credential for the provided identity using the provided identity. * * @param namespace The identity namespace. - * @param scopes A comma separated list of OAuth scopes requested. + * @param scopes A comma separated list of OAuth scopes requested. * @return A short-lived credential. * @throws CredentialProvisioningException If provisioning the credential fails. - * @throws IOException If any transport errors occur. - * @throws NotFoundException If the profile or identity are not found. + * @throws IOException If any transport errors occur. + * @throws NotFoundException If the profile or identity are not found. */ @Override public ProvisionedCredential provision(String namespace, String scopes) @@ -83,16 +86,44 @@ public ProvisionedCredential provision(String namespace, String scopes) contextAccessEnforcer.enforce(new NamespaceId(namespace), NamespacePermission.PROVISION_CREDENTIAL); NamespaceMeta namespaceMeta; + NamespaceId namespaceId = new NamespaceId(namespace); try { - namespaceMeta = namespaceAdmin.get(new NamespaceId(namespace)); + namespaceMeta = namespaceAdmin.get(namespaceId); } catch (Exception e) { throw new IOException(String.format("Failed to get namespace '%s' metadata", namespace), e); } String identityName = - GcpWorkloadIdentityUtil.getWorkloadIdentityName(namespaceMeta.getIdentity()); + GcpWorkloadIdentityUtil.getWorkloadIdentityName(namespaceId); switchToInternalUser(); - return credentialProvider.provision(namespace, identityName, scopes); + return credentialProvider.provision(NamespaceId.SYSTEM.getNamespace(), identityName, + new CredentialProvisionContext( + GcpWorkloadIdentityUtil.createProvisionPropertiesMap(scopes, namespaceMeta))); + } + + @Override + public void validateIdentity(String namespace, String serviceAccount) + throws IdentityValidationException, IOException { + NamespaceMeta namespaceMeta; + try { + namespaceMeta = namespaceAdmin.get(new NamespaceId(namespace)); + } catch (Exception e) { + throw new IOException(String.format("Failed to get namespace '%s' metadata", + namespace), e); + } + CredentialIdentity credentialIdentity = new CredentialIdentity( + NamespaceId.SYSTEM.getNamespace(), GcpWorkloadIdentityUtil.SYSTEM_PROFILE_NAME, + namespaceMeta.getIdentity(), serviceAccount); + try { + credentialProvider + .validateIdentity(NamespaceId.SYSTEM.getNamespace(), credentialIdentity, + new CredentialProvisionContext( + GcpWorkloadIdentityUtil.createProvisionPropertiesMap(null, namespaceMeta))); + } catch (NotFoundException e) { + // This should never happen. + throw new IdentityValidationException("Could not find GCP workload identity provider profile", + e); + } } private void switchToInternalUser() { diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/namespace/credential/GcpWorkloadIdentityUtil.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/namespace/credential/GcpWorkloadIdentityUtil.java index 8899da769744..761d727e010b 100644 --- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/namespace/credential/GcpWorkloadIdentityUtil.java +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/namespace/credential/GcpWorkloadIdentityUtil.java @@ -16,11 +16,17 @@ package io.cdap.cdap.internal.namespace.credential; +import com.google.common.hash.Hashing; +import io.cdap.cdap.proto.NamespaceMeta; import io.cdap.cdap.proto.credential.NamespaceWorkloadIdentity; +import io.cdap.cdap.proto.id.NamespaceId; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nullable; /** - * Utility class for {@link NamespaceWorkloadIdentity} associated with - * the namespace. + * Utility class for {@link NamespaceWorkloadIdentity} associated with the namespace. */ public final class GcpWorkloadIdentityUtil { @@ -28,13 +34,59 @@ public final class GcpWorkloadIdentityUtil { public static final String SYSTEM_PROFILE_NAME = "ns-gcp-wi"; + private static final String GCP_OAUTH_SCOPES_PROPERTY = "gcp.oauth.scopes"; + private static final String K8S_NAMESPACE_PROPERTY = "k8s.namespace"; + private static final String WRAPPED_CDAP_NAMESPACE_PROPERTY = "gcp.wrapped.cdap.namespace"; + /** * Returns the namespace workload identity name. * - * @param identityName The name of identity provided. + * @param namespaceId The namespace which the identity is attached to. * @return namespace workload identity name. */ - public static String getWorkloadIdentityName(String identityName) { - return String.format("%s-%s", NAMESPACE_IDENTITY_NAME_PREFIX, identityName); + public static String getWorkloadIdentityName(NamespaceId namespaceId) { + return String.format("%s-%s", NAMESPACE_IDENTITY_NAME_PREFIX, + computeLengthLimitedIdentity(namespaceId)); + } + + /** + * Computes unique namespace identity name by lowercase namespace id truncated to 15 characters + * appended with the first 15 hex characters of the namespace's SHA256. + * + * @param namespaceId the {@link NamespaceId} of the namespace. + * @return namespace unique identity name. + */ + private static String computeLengthLimitedIdentity(NamespaceId namespaceId) { + if (NamespaceId.DEFAULT.equals(namespaceId)) { + return namespaceId.getNamespace(); + } + String namespace = namespaceId.getNamespace(); + String sha256Hex = Hashing.sha256().hashString(namespace, StandardCharsets.UTF_8).toString(); + sha256Hex = sha256Hex.length() > 15 ? sha256Hex.substring(0, 15) : sha256Hex; + namespace = namespace.length() > 15 ? namespace.substring(0, 15) : namespace; + return String.format("%s-%s", namespace, sha256Hex).toLowerCase().replace('_', '-'); + } + + /** + * Constructs the provisioning properties to pass to the credential provider. + * + * @param scopes The GCP OAuth scopes to request. + * @param namespaceMeta The metadata for the namespace. + * @return A property map for credential provisioning. + */ + public static Map createProvisionPropertiesMap(@Nullable String scopes, + NamespaceMeta namespaceMeta) { + Map properties = new HashMap<>(); + if (scopes != null) { + properties.put(GCP_OAUTH_SCOPES_PROPERTY, scopes); + } + // Add property for k8s namespace tied to a CDAP namespace. This is used in Hybrid mode. + properties + .put(K8S_NAMESPACE_PROPERTY, namespaceMeta.getConfig().getConfig(K8S_NAMESPACE_PROPERTY)); + // Add namespace property indicating which CDAP namespace the parent resource was operated on. + // This is necessary because the namespace field will be the SYSTEM namespace in which all + // managed workload identity resources are stored. + properties.put(WRAPPED_CDAP_NAMESPACE_PROPERTY, namespaceMeta.getNamespaceId().getNamespace()); + return properties; } } diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/namespace/credential/RemoteNamespaceCredentialProvider.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/namespace/credential/RemoteNamespaceCredentialProvider.java index f7b03351483c..6c4d1a13d784 100644 --- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/namespace/credential/RemoteNamespaceCredentialProvider.java +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/namespace/credential/RemoteNamespaceCredentialProvider.java @@ -27,7 +27,9 @@ import io.cdap.cdap.proto.BasicThrowable; import io.cdap.cdap.proto.codec.BasicThrowableCodec; import io.cdap.cdap.proto.credential.CredentialProvisioningException; +import io.cdap.cdap.proto.credential.IdentityValidationException; import io.cdap.cdap.proto.credential.NamespaceCredentialProvider; +import io.cdap.cdap.proto.credential.NamespaceWorkloadIdentity; import io.cdap.cdap.proto.credential.NotFoundException; import io.cdap.cdap.proto.credential.ProvisionedCredential; import io.cdap.common.http.HttpMethod; @@ -37,10 +39,11 @@ import joptsimple.internal.Strings; /** - * Remote implementation for {@link NamespaceCredentialProvider} used in - * {@link io.cdap.cdap.common.conf.Constants.ArtifactLocalizer}. + * Remote implementation for {@link NamespaceCredentialProvider} used in {@link + * io.cdap.cdap.common.conf.Constants.ArtifactLocalizer}. */ public class RemoteNamespaceCredentialProvider implements NamespaceCredentialProvider { + private static final Gson GSON = new GsonBuilder().registerTypeAdapter(BasicThrowable.class, new BasicThrowableCodec()).create(); private final RemoteClient remoteClient; @@ -48,7 +51,7 @@ public class RemoteNamespaceCredentialProvider implements NamespaceCredentialPro /** * Construct the {@link RemoteCredentialProvider}. * - * @param remoteClientFactory A factory to create {@link RemoteClient}. + * @param remoteClientFactory A factory to create {@link RemoteClient}. * @param internalAuthenticator An authenticator to propagate internal identity headers. */ public RemoteNamespaceCredentialProvider(RemoteClientFactory remoteClientFactory, @@ -63,11 +66,11 @@ public RemoteNamespaceCredentialProvider(RemoteClientFactory remoteClientFactory * Provisions a short-lived credential for the provided identity using the provided identity. * * @param namespace The identity namespace. - * @param scopes A comma separated list of OAuth scopes requested. + * @param scopes A comma separated list of OAuth scopes requested. * @return A short-lived credential. * @throws CredentialProvisioningException If provisioning the credential fails. - * @throws IOException If any transport errors occur. - * @throws NotFoundException If the profile or identity are not found. + * @throws IOException If any transport errors occur. + * @throws NotFoundException If the profile or identity are not found. */ @Override public ProvisionedCredential provision(String namespace, String scopes) @@ -95,4 +98,22 @@ public ProvisionedCredential provision(String namespace, String scopes) return GSON.fromJson(response.getResponseBodyAsString(), ProvisionedCredential.class); } + + @Override + public void validateIdentity(String namespace, String serviceAccount) + throws IdentityValidationException, IOException { + String url = String.format("namespaces/%s/credentials/workloadIdentity/validate", + namespace); + io.cdap.common.http.HttpRequest tokenRequest = + remoteClient.requestBuilder(HttpMethod.POST, url) + .withBody(GSON.toJson(new NamespaceWorkloadIdentity(serviceAccount))).build(); + HttpResponse response = remoteClient.execute(tokenRequest, Idempotency.NONE); + + if (response.getResponseCode() != HttpResponseStatus.OK.code()) { + throw new IdentityValidationException(String.format( + "Failed to provision credential with response code: %s and error: %s", + response.getResponseCode(), + response.getResponseBodyAsString())); + } + } } diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/namespace/credential/handler/GcpWorkloadIdentityHttpHandler.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/namespace/credential/handler/GcpWorkloadIdentityHttpHandler.java index 374a0800e0b2..c13492915bc5 100644 --- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/namespace/credential/handler/GcpWorkloadIdentityHttpHandler.java +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/namespace/credential/handler/GcpWorkloadIdentityHttpHandler.java @@ -30,8 +30,8 @@ import io.cdap.cdap.internal.namespace.credential.GcpWorkloadIdentityUtil; import io.cdap.cdap.proto.NamespaceMeta; import io.cdap.cdap.proto.credential.CredentialIdentity; -import io.cdap.cdap.proto.credential.CredentialProvider; import io.cdap.cdap.proto.credential.IdentityValidationException; +import io.cdap.cdap.proto.credential.NamespaceCredentialProvider; import io.cdap.cdap.proto.credential.NamespaceWorkloadIdentity; import io.cdap.cdap.proto.id.CredentialIdentityId; import io.cdap.cdap.proto.id.NamespaceId; @@ -64,18 +64,19 @@ @Singleton @Path(Gateway.API_VERSION_3) public class GcpWorkloadIdentityHttpHandler extends AbstractHttpHandler { + private static final Gson GSON = new Gson(); private final ContextAccessEnforcer accessEnforcer; private final NamespaceQueryAdmin namespaceQueryAdmin; private final CredentialIdentityManager credentialIdentityManager; - private final CredentialProvider credentialProvider; + private final NamespaceCredentialProvider credentialProvider; @Inject GcpWorkloadIdentityHttpHandler(ContextAccessEnforcer accessEnforcer, NamespaceQueryAdmin namespaceQueryAdmin, CredentialIdentityManager credentialIdentityManager, - CredentialProvider credentialProvider) { + NamespaceCredentialProvider credentialProvider) { this.accessEnforcer = accessEnforcer; this.namespaceQueryAdmin = namespaceQueryAdmin; this.credentialIdentityManager = credentialIdentityManager; @@ -98,19 +99,13 @@ public void validateIdentity(FullHttpRequest request, HttpResponder responder, accessEnforcer.enforce(new NamespaceId(namespace), NamespacePermission.PROVISION_CREDENTIAL); NamespaceWorkloadIdentity namespaceWorkloadIdentity = deserializeRequestContent(request, NamespaceWorkloadIdentity.class); - NamespaceMeta namespaceMeta = getNamespaceMeta(namespace); - CredentialIdentity credentialIdentity = new CredentialIdentity( - NamespaceId.SYSTEM.getNamespace(), GcpWorkloadIdentityUtil.SYSTEM_PROFILE_NAME, - namespaceMeta.getIdentity(), - namespaceWorkloadIdentity.getServiceAccount()); switchToInternalUser(); try { - credentialProvider.validateIdentity(namespaceMeta, credentialIdentity); + credentialProvider + .validateIdentity(namespace, namespaceWorkloadIdentity.getServiceAccount()); } catch (IdentityValidationException e) { throw new BadRequestException(String.format("Identity validation failed with error: %s", e.getCause() == null ? e.getMessage() : e.getCause().getMessage()), e); - } catch (io.cdap.cdap.proto.credential.NotFoundException e) { - throw new NotFoundException(e.getMessage()); } responder.sendJson(HttpResponseStatus.OK, "Namespace identity validated successfully"); } @@ -118,9 +113,9 @@ public void validateIdentity(FullHttpRequest request, HttpResponder responder, /** * Fetches a credential identity. * - * @param request The HTTP request. - * @param responder The HTTP responder. - * @param namespace The identity namespace. + * @param request The HTTP request. + * @param responder The HTTP responder. + * @param namespace The identity namespace. * @throws BadRequestException If the identity name is invalid. * @throws IOException If transport errors occur. * @throws NotFoundException If the namespace or identity are not found. @@ -130,8 +125,9 @@ public void validateIdentity(FullHttpRequest request, HttpResponder responder, public void getIdentity(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespace) throws Exception { NamespaceMeta namespaceMeta = getNamespaceMeta(namespace); - CredentialIdentityId credentialIdentityId = createIdentityIdOrPropagate(namespace, - GcpWorkloadIdentityUtil.getWorkloadIdentityName(namespaceMeta.getIdentity())); + CredentialIdentityId credentialIdentityId = createIdentityIdOrPropagate( + NamespaceId.SYSTEM.getNamespace(), + GcpWorkloadIdentityUtil.getWorkloadIdentityName(namespaceMeta.getNamespaceId())); switchToInternalUser(); Optional identity = credentialIdentityManager.get(credentialIdentityId); if (!identity.isPresent()) { @@ -161,8 +157,9 @@ public void createIdentity(FullHttpRequest request, HttpResponder responder, NamespaceWorkloadIdentity namespaceWorkloadIdentity = deserializeRequestContent(request, NamespaceWorkloadIdentity.class); NamespaceMeta namespaceMeta = getNamespaceMeta(namespace); - CredentialIdentityId credentialIdentityId = createIdentityIdOrPropagate(namespace, - GcpWorkloadIdentityUtil.getWorkloadIdentityName(namespaceMeta.getIdentity())); + CredentialIdentityId credentialIdentityId = createIdentityIdOrPropagate( + NamespaceId.SYSTEM.getNamespace(), + GcpWorkloadIdentityUtil.getWorkloadIdentityName(namespaceMeta.getNamespaceId())); switchToInternalUser(); Optional identity = credentialIdentityManager.get(credentialIdentityId); CredentialIdentity credentialIdentity = new CredentialIdentity( @@ -179,9 +176,9 @@ public void createIdentity(FullHttpRequest request, HttpResponder responder, /** * Deletes an identity. * - * @param request The HTTP request. - * @param responder The HTTP responder. - * @param namespace The identity namespace. + * @param request The HTTP request. + * @param responder The HTTP responder. + * @param namespace The identity namespace. * @throws BadRequestException If the identity name is invalid. * @throws IOException If transport errors occur. * @throws NotFoundException If the namespace or identity are not found. @@ -192,8 +189,9 @@ public void deleteIdentity(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespace) throws Exception { accessEnforcer.enforce(new NamespaceId(namespace), NamespacePermission.UNSET_SERVICE_ACCOUNT); NamespaceMeta namespaceMeta = getNamespaceMeta(namespace); - CredentialIdentityId credentialIdentityId = createIdentityIdOrPropagate(namespace, - GcpWorkloadIdentityUtil.getWorkloadIdentityName(namespaceMeta.getIdentity())); + CredentialIdentityId credentialIdentityId = createIdentityIdOrPropagate( + NamespaceId.SYSTEM.getNamespace(), + GcpWorkloadIdentityUtil.getWorkloadIdentityName(namespaceMeta.getNamespaceId())); switchToInternalUser(); credentialIdentityManager.delete(credentialIdentityId); responder.sendStatus(HttpResponseStatus.OK); diff --git a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/credential/DefaultCredentialProviderServiceTest.java b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/credential/DefaultCredentialProviderServiceTest.java index 17a3a909d6e5..62613ffd989a 100644 --- a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/credential/DefaultCredentialProviderServiceTest.java +++ b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/credential/DefaultCredentialProviderServiceTest.java @@ -20,6 +20,7 @@ import io.cdap.cdap.common.namespace.InMemoryNamespaceAdmin; import io.cdap.cdap.proto.NamespaceMeta; import io.cdap.cdap.proto.credential.CredentialIdentity; +import io.cdap.cdap.proto.credential.CredentialProvisionContext; import io.cdap.cdap.proto.credential.CredentialProvisioningException; import io.cdap.cdap.proto.credential.IdentityValidationException; import io.cdap.cdap.proto.credential.NotFoundException; @@ -41,7 +42,7 @@ public class DefaultCredentialProviderServiceTest extends CredentialProviderTest public static void startup() { credentialProviderService = new DefaultCredentialProviderService(CConfiguration.create(), contextAccessEnforcer, new MockCredentialProviderLoader(), credentialIdentityManager, - credentialProfileManager, namespaceAdmin); + credentialProfileManager); } @Test @@ -101,7 +102,7 @@ public void testProvisionFailureThrowsException() throws Exception { profileId.getName(), "some-identity", "some-secure-value"); credentialIdentityManager.create(id, identity); Assert.assertEquals(RETURNED_TOKEN, credentialProviderService.provision(namespace, - identityName, null)); + identityName, new CredentialProvisionContext())); } @Test @@ -109,17 +110,12 @@ public void testIdentityValidationSuccess() throws Exception { // Create a new profile. String identityName = "some-identity"; String namespace = "testIdentityValidationSuccess"; - NamespaceMeta namespaceMeta = new - NamespaceMeta.Builder() - .setName(namespace) - .setIdentity(identityName) - .buildWithoutKeytabUriVersion(); CredentialProfileId profileId = createDummyProfile(CREDENTIAL_PROVIDER_TYPE_SUCCESS, namespace, "test-profile"); CredentialIdentity identity = new CredentialIdentity(profileId.getNamespace(), profileId.getName(), identityName, "some-secure-value"); - credentialProviderService.validateIdentity(namespaceMeta, identity); + credentialProviderService.validateIdentity(namespace, identity, new CredentialProvisionContext()); } @Test(expected = IdentityValidationException.class) @@ -127,30 +123,20 @@ public void testIdentityValidationOnProvisionFailureThrowsException() throws Exc // Create a new profile. String namespace = "testIdentityValidationFailureThrowsException"; String identityName = "some-identity"; - NamespaceMeta namespaceMeta = new - NamespaceMeta.Builder() - .setName(namespace) - .setIdentity(identityName) - .buildWithoutKeytabUriVersion(); CredentialProfileId profileId = createDummyProfile(CREDENTIAL_PROVIDER_TYPE_PROVISION_FAILURE, namespace, "test-profile"); CredentialIdentity identity = new CredentialIdentity(profileId.getNamespace(), profileId.getName(), identityName, "some-secure-value"); - credentialProviderService.validateIdentity(namespaceMeta, identity); + credentialProviderService.validateIdentity(namespace, identity, new CredentialProvisionContext()); } @Test(expected = NotFoundException.class) public void testIdentityValidationWithNotFoundProfileThrowsException() throws Exception { String namespace = "testIdentityValidationWithNotFoundProfileThrowsException"; String identityName = "some-identity"; - NamespaceMeta namespaceMeta = new - NamespaceMeta.Builder() - .setName(namespace) - .setIdentity(identityName) - .buildWithoutKeytabUriVersion(); CredentialIdentity identity = new CredentialIdentity(namespace, "does-not-exist", identityName, "some-secure-value"); - credentialProviderService.validateIdentity(namespaceMeta, identity); + credentialProviderService.validateIdentity(namespace, identity, new CredentialProvisionContext()); } } diff --git a/cdap-credential-ext-gcp-wi/src/main/java/io/cdap/cdap/security/spi/credential/GcpWorkloadIdentityCredentialProvider.java b/cdap-credential-ext-gcp-wi/src/main/java/io/cdap/cdap/security/spi/credential/GcpWorkloadIdentityCredentialProvider.java index a0fd5c42a5c5..f74594cf58a5 100644 --- a/cdap-credential-ext-gcp-wi/src/main/java/io/cdap/cdap/security/spi/credential/GcpWorkloadIdentityCredentialProvider.java +++ b/cdap-credential-ext-gcp-wi/src/main/java/io/cdap/cdap/security/spi/credential/GcpWorkloadIdentityCredentialProvider.java @@ -26,10 +26,10 @@ import com.google.gson.GsonBuilder; import io.cdap.cdap.api.retry.RetryableException; import io.cdap.cdap.proto.BasicThrowable; -import io.cdap.cdap.proto.NamespaceMeta; import io.cdap.cdap.proto.codec.BasicThrowableCodec; import io.cdap.cdap.proto.credential.CredentialIdentity; import io.cdap.cdap.proto.credential.CredentialProfile; +import io.cdap.cdap.proto.credential.CredentialProvisionContext; import io.cdap.cdap.proto.credential.CredentialProvisioningException; import io.cdap.cdap.proto.credential.ProvisionedCredential; import io.cdap.cdap.proto.id.NamespaceId; @@ -59,7 +59,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; -import javax.annotation.Nullable; import javax.ws.rs.HttpMethod; import javax.ws.rs.core.HttpHeaders; import okhttp3.OkHttpClient; @@ -101,6 +100,23 @@ public class GcpWorkloadIdentityCredentialProvider implements CredentialProvider private static final String PROVISIONING_FAILURE_ERROR_MESSAGE_FORMAT = "Failed to provision credential with identity '%s'"; + // Property key configurations + /** + * A comma-separated string of OAuth scopes supported for Google OAuth2 access tokens. + */ + static final String GCP_OAUTH_SCOPES_PROPERTY = "gcp.oauth.scopes"; + /** + * Represents the CDAP namespace for which this property is actually being queried for. This is + * necessary because the CDAP namespace may not be the same as the identity's storage namespace. + * Property name is defined in GcpWorkloadIdentityUtil.java. + */ + static final String GCP_WRAPPED_CDAP_NAMESPACE_PROPERTY = "gcp.wrapped.cdap.namespace"; + /** + * When namespace creation is enabled, represents the Kubernetes namespace this k8s service + * account exists in. If namespace creation is disabled, assume it's in the default namespace. + */ + static final String K8S_NAMESPACE_PROPERTY = "k8s.namespace"; + /** * Constructs the {@link GcpWorkloadIdentityCredentialProvider}. */ @@ -112,7 +128,7 @@ public GcpWorkloadIdentityCredentialProvider() { @Override public ProvisionedCredential load(ProvisionedCredentialCacheKey provisionedCredentialCacheKey) throws Exception { - return getProvisionedCredential(provisionedCredentialCacheKey.getNamespaceMeta(), + return getProvisionedCredential(provisionedCredentialCacheKey.getK8sNamespace(), provisionedCredentialCacheKey.getCredentialIdentity(), provisionedCredentialCacheKey.getScopes()); } @@ -165,9 +181,14 @@ public ApiClient getApiClient() throws IOException { } @Override - public ProvisionedCredential provision(NamespaceMeta namespaceMeta, - CredentialProfile profile, CredentialIdentity identity, @Nullable String scopes) + public ProvisionedCredential provision(String namespace, + CredentialProfile profile, CredentialIdentity identity, CredentialProvisionContext context) throws CredentialProvisioningException { + if (!NamespaceId.SYSTEM.getNamespace().equals(namespace)) { + throw new CredentialProvisioningException(String + .format("Provisioning tokens for credential identities in non-system namespace '%s' is " + + "disallowed.", namespace)); + } // Provision the credential with exponential delay on retryable failure. long delay = Long.parseLong( @@ -185,8 +206,17 @@ public ProvisionedCredential provision(NamespaceMeta namespaceMeta, try { while (stopWatch.elapsed(TimeUnit.SECONDS) < timeout) { try { - return getCredentialLoadingCache().get(new ProvisionedCredentialCacheKey(namespaceMeta, - identity, scopes)); + // Get k8s namespace from namespace metadata if using a non-default namespace and + // namespace creation hook is enabled. + String k8sNamespace = NamespaceId.DEFAULT.getNamespace(); + String cdapWrappedNamespace = context.getProperties().get( + GCP_WRAPPED_CDAP_NAMESPACE_PROPERTY); + if (!cdapWrappedNamespace.equals(NamespaceId.DEFAULT.getNamespace()) + && credentialProviderContext.isNamespaceCreationHookEnabled()) { + k8sNamespace = context.getProperties().get(K8S_NAMESPACE_PROPERTY); + } + return getCredentialLoadingCache().get(new ProvisionedCredentialCacheKey(k8sNamespace, + identity, context.getProperties().get(GCP_OAUTH_SCOPES_PROPERTY))); } catch (Exception e) { if (!(e.getCause() instanceof RetryableException)) { throw e; @@ -215,16 +245,8 @@ public ProvisionedCredential provision(NamespaceMeta namespaceMeta, } @VisibleForTesting - ProvisionedCredential getProvisionedCredential(NamespaceMeta namespaceMeta, - CredentialIdentity identity, @Nullable String scopes) throws IOException, ApiException { - - // Get k8s namespace from namespace metadata if using a non-default namespace and namespace - // creation hook is enabled. - String k8sNamespace = NamespaceId.DEFAULT.getNamespace(); - if (!namespaceMeta.getName().equals(NamespaceId.DEFAULT.getNamespace()) - && credentialProviderContext.isNamespaceCreationHookEnabled()) { - k8sNamespace = namespaceMeta.getConfig().getConfigs().get("k8s.namespace"); - } + ProvisionedCredential getProvisionedCredential(String k8sNamespace, CredentialIdentity identity, + String scopes) throws IOException, ApiException { try { String workloadIdentityPool = @@ -334,6 +356,7 @@ private String exchangeTokenViaSts(String token, String scopes, String audience) @VisibleForTesting interface ConnectionProvider { + HttpURLConnection getConnection() throws IOException; } diff --git a/cdap-credential-ext-gcp-wi/src/main/java/io/cdap/cdap/security/spi/credential/ProvisionedCredentialCacheKey.java b/cdap-credential-ext-gcp-wi/src/main/java/io/cdap/cdap/security/spi/credential/ProvisionedCredentialCacheKey.java index 7a0c8c80dc53..f25441ae7e31 100644 --- a/cdap-credential-ext-gcp-wi/src/main/java/io/cdap/cdap/security/spi/credential/ProvisionedCredentialCacheKey.java +++ b/cdap-credential-ext-gcp-wi/src/main/java/io/cdap/cdap/security/spi/credential/ProvisionedCredentialCacheKey.java @@ -16,16 +16,15 @@ package io.cdap.cdap.security.spi.credential; -import io.cdap.cdap.proto.NamespaceMeta; import io.cdap.cdap.proto.credential.CredentialIdentity; import java.util.Objects; /** - * Defines the contents of key used for - * caching {@link io.cdap.cdap.proto.credential.ProvisionedCredential}. + * Defines the contents of key used for caching {@link io.cdap.cdap.proto.credential.ProvisionedCredential}. */ public final class ProvisionedCredentialCacheKey { - private final NamespaceMeta namespaceMeta; + + private final String k8sNamespace; private final CredentialIdentity credentialIdentity; private final String scopes; private transient Integer hashCode; @@ -33,19 +32,19 @@ public final class ProvisionedCredentialCacheKey { /** * Constructs the {@link ProvisionedCredentialCacheKey}. * - * @param namespaceMeta the {@link NamespaceMeta} + * @param k8sNamespace the namespace. * @param credentialIdentity the {@link CredentialIdentity} - * @param scopes the comma separated list of OAuth scopes. + * @param scopes the comma separated list of OAuth scopes. */ - public ProvisionedCredentialCacheKey(NamespaceMeta namespaceMeta, - CredentialIdentity credentialIdentity, String scopes) { - this.namespaceMeta = namespaceMeta; + public ProvisionedCredentialCacheKey(String k8sNamespace, CredentialIdentity credentialIdentity, + String scopes) { + this.k8sNamespace = k8sNamespace; this.credentialIdentity = credentialIdentity; this.scopes = scopes; } - public NamespaceMeta getNamespaceMeta() { - return namespaceMeta; + public String getK8sNamespace() { + return k8sNamespace; } public CredentialIdentity getCredentialIdentity() { @@ -59,11 +58,12 @@ public String getScopes() { @Override public boolean equals(Object o) { if (!(o instanceof ProvisionedCredentialCacheKey)) { - return false; + return false; } ProvisionedCredentialCacheKey that = (ProvisionedCredentialCacheKey) o; - return Objects.equals(namespaceMeta.getNamespaceId().getNamespace(), - that.namespaceMeta.getNamespaceId().getNamespace()) + return Objects.equals(k8sNamespace, that.k8sNamespace) + && Objects.equals(credentialIdentity.getIdentity(), + that.getCredentialIdentity().getIdentity()) && Objects.equals(credentialIdentity.getSecureValue(), that.getCredentialIdentity().getSecureValue()) && Objects.equals(scopes, that.scopes); @@ -73,8 +73,9 @@ public boolean equals(Object o) { public int hashCode() { Integer hashCode = this.hashCode; if (hashCode == null) { - this.hashCode = hashCode = Objects.hash(namespaceMeta.getNamespaceId().getNamespace(), - credentialIdentity.getSecureValue(), scopes); + this.hashCode = hashCode = Objects + .hash(k8sNamespace, credentialIdentity.getIdentity(), credentialIdentity.getSecureValue(), + scopes); } return hashCode; } diff --git a/cdap-credential-ext-gcp-wi/src/test/java/io/cdap/cdap/security/spi/credential/GcpWorkloadIdentityCredentialProviderTest.java b/cdap-credential-ext-gcp-wi/src/test/java/io/cdap/cdap/security/spi/credential/GcpWorkloadIdentityCredentialProviderTest.java index 8f4846e0cee3..e5cf0a6c42e6 100644 --- a/cdap-credential-ext-gcp-wi/src/test/java/io/cdap/cdap/security/spi/credential/GcpWorkloadIdentityCredentialProviderTest.java +++ b/cdap-credential-ext-gcp-wi/src/test/java/io/cdap/cdap/security/spi/credential/GcpWorkloadIdentityCredentialProviderTest.java @@ -36,6 +36,7 @@ import io.cdap.cdap.proto.codec.BasicThrowableCodec; import io.cdap.cdap.proto.credential.CredentialIdentity; import io.cdap.cdap.proto.credential.CredentialProfile; +import io.cdap.cdap.proto.credential.CredentialProvisionContext; import io.cdap.cdap.proto.credential.ProvisionedCredential; import io.cdap.cdap.proto.id.NamespaceId; import io.cdap.cdap.security.spi.credential.SecurityTokenServiceRequest.TokenType; @@ -92,7 +93,7 @@ public void testProvisioningCredentialWithRetries() throws Exception { public ProvisionedCredential load(ProvisionedCredentialCacheKey provisionedCredentialCacheKey) throws Exception { return mockedCredentialProvider.getProvisionedCredential( - provisionedCredentialCacheKey.getNamespaceMeta(), + provisionedCredentialCacheKey.getK8sNamespace(), provisionedCredentialCacheKey.getCredentialIdentity(), provisionedCredentialCacheKey.getScopes()); } @@ -110,16 +111,21 @@ public ProvisionedCredential load(ProvisionedCredentialCacheKey CredentialProfile credentialProfile = new CredentialProfile( GcpWorkloadIdentityCredentialProvider.NAME, "profile", Collections.emptyMap()); - CredentialIdentity credentialIdentity = new CredentialIdentity( - NamespaceId.DEFAULT.getNamespace(), "default", - NamespaceMeta.DEFAULT.getIdentity(), "secureVal"); + Map properties = new HashMap<>(); + properties.put(GcpWorkloadIdentityCredentialProvider.K8S_NAMESPACE_PROPERTY, "default"); + properties + .put(GcpWorkloadIdentityCredentialProvider.GCP_WRAPPED_CDAP_NAMESPACE_PROPERTY, "default"); // validate profile mockedCredentialProvider.validateProfile(credentialProfile); + + CredentialIdentity credentialIdentity = new CredentialIdentity( + NamespaceId.DEFAULT.getNamespace(), "default", + NamespaceMeta.DEFAULT.getIdentity(), "secureVal"); // provision credential ProvisionedCredential credential = - mockedCredentialProvider.provision(NamespaceMeta.DEFAULT, credentialProfile, - credentialIdentity, null); + mockedCredentialProvider.provision(NamespaceId.SYSTEM.getNamespace(), credentialProfile, + credentialIdentity, new CredentialProvisionContext(properties)); Assert.assertEquals(credential.get(), IAM_TOKEN); Assert.assertEquals(credential.getExpiration().toString(), EXPIRES_IN); @@ -196,7 +202,7 @@ public void testInvalidProfile() throws ProfileValidationException { } @Test(expected = IOException.class) - public void testExecuteHttpPostRequestHandlesHTTPErrorResponse() throws Exception { + public void testExecuteHttpPostRequestHandlesHttpErrorResponse() throws Exception { InputStream errorMessageStream = new ByteArrayInputStream( "Some error here".getBytes(StandardCharsets.UTF_8)); OutputStream outputStream = new ByteArrayOutputStream(); diff --git a/cdap-proto/src/main/java/io/cdap/cdap/proto/credential/CredentialProvider.java b/cdap-proto/src/main/java/io/cdap/cdap/proto/credential/CredentialProvider.java index 5aa72823f9b1..1b9ec7619109 100644 --- a/cdap-proto/src/main/java/io/cdap/cdap/proto/credential/CredentialProvider.java +++ b/cdap-proto/src/main/java/io/cdap/cdap/proto/credential/CredentialProvider.java @@ -16,7 +16,6 @@ package io.cdap.cdap.proto.credential; -import io.cdap.cdap.proto.NamespaceMeta; import java.io.IOException; /** @@ -27,26 +26,29 @@ public interface CredentialProvider { /** * Provisions a short-lived credential for the provided identity using the provided identity. * - * @param namespace The identity namespace. + * @param namespace The identity namespace. * @param identityName The identity name. - * @param scopes A comma separated list of OAuth scopes requested. + * @param context The context to use for provisioning. * @return A short-lived credential. * @throws CredentialProvisioningException If provisioning the credential fails. * @throws IOException If any transport errors occur. * @throws NotFoundException If the profile or identity are not found. */ - ProvisionedCredential provision(String namespace, String identityName, String scopes) + ProvisionedCredential provision(String namespace, String identityName, + CredentialProvisionContext context) throws CredentialProvisioningException, IOException, NotFoundException; /** * Validates the provided identity. * - * @param namespaceMeta The identity namespace metadata. - * @param identity The identity to validate. + * @param namespace The identity namespace. + * @param identity The identity to validate. + * @param context The context to use for provisioning. * @throws IdentityValidationException If validation fails. * @throws IOException If any transport errors occur. * @throws NotFoundException If the profile is not found. */ - void validateIdentity(NamespaceMeta namespaceMeta, CredentialIdentity identity) + void validateIdentity(String namespace, CredentialIdentity identity, + CredentialProvisionContext context) throws IdentityValidationException, IOException, NotFoundException; } diff --git a/cdap-proto/src/main/java/io/cdap/cdap/proto/credential/CredentialProvisionContext.java b/cdap-proto/src/main/java/io/cdap/cdap/proto/credential/CredentialProvisionContext.java new file mode 100644 index 000000000000..c48bd8e53730 --- /dev/null +++ b/cdap-proto/src/main/java/io/cdap/cdap/proto/credential/CredentialProvisionContext.java @@ -0,0 +1,44 @@ +/* + * Copyright © 2023 Cask Data, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package io.cdap.cdap.proto.credential; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Context passed to the Credential Provider during credential provisioning. + */ +public class CredentialProvisionContext { + + /** + * A set of properties to pass to the CredentialProvider for provisioning. + */ + private final Map properties; + + public CredentialProvisionContext() { + this.properties = Collections.emptyMap(); + } + + public CredentialProvisionContext(Map properties) { + this.properties = Collections.unmodifiableMap(new HashMap<>(properties)); + } + + public Map getProperties() { + return properties; + } +} diff --git a/cdap-proto/src/main/java/io/cdap/cdap/proto/credential/IdentityValidationException.java b/cdap-proto/src/main/java/io/cdap/cdap/proto/credential/IdentityValidationException.java index 8d682f7d8756..4077cb7b957e 100644 --- a/cdap-proto/src/main/java/io/cdap/cdap/proto/credential/IdentityValidationException.java +++ b/cdap-proto/src/main/java/io/cdap/cdap/proto/credential/IdentityValidationException.java @@ -38,4 +38,14 @@ public IdentityValidationException(Throwable cause) { public IdentityValidationException(String message) { super(message); } + + /** + * Creates a new identity validation exception. + * + * @param message The message of identity validation failure. + * @param cause The cause of identity validation failure. + */ + public IdentityValidationException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/cdap-proto/src/main/java/io/cdap/cdap/proto/credential/NamespaceCredentialProvider.java b/cdap-proto/src/main/java/io/cdap/cdap/proto/credential/NamespaceCredentialProvider.java index 644ed5676df5..db4f047059a1 100644 --- a/cdap-proto/src/main/java/io/cdap/cdap/proto/credential/NamespaceCredentialProvider.java +++ b/cdap-proto/src/main/java/io/cdap/cdap/proto/credential/NamespaceCredentialProvider.java @@ -27,7 +27,7 @@ public interface NamespaceCredentialProvider { * Provisions a short-lived credential for the provided identity using the provided identity. * * @param namespace The identity namespace. - * @param scopes A comma separated list of OAuth scopes requested. + * @param scopes A comma separated list of OAuth scopes requested. * @return A short-lived credential. * @throws CredentialProvisioningException If provisioning the credential fails. * @throws IOException If any transport errors occur. @@ -35,4 +35,15 @@ public interface NamespaceCredentialProvider { */ ProvisionedCredential provision(String namespace, String scopes) throws CredentialProvisioningException, IOException, NotFoundException; + + /** + * Validates the provided identity. + * + * @param namespace The identity namespace. + * @param serviceAccount The service account to validate. + * @throws IdentityValidationException If validation fails. + * @throws IOException If any transport errors occur. + */ + void validateIdentity(String namespace, String serviceAccount) + throws IdentityValidationException, IOException; } diff --git a/cdap-proto/src/main/java/io/cdap/cdap/proto/credential/ValidateIdentityRequest.java b/cdap-proto/src/main/java/io/cdap/cdap/proto/credential/ValidateIdentityRequest.java new file mode 100644 index 000000000000..89a1ee166c57 --- /dev/null +++ b/cdap-proto/src/main/java/io/cdap/cdap/proto/credential/ValidateIdentityRequest.java @@ -0,0 +1,38 @@ +/* + * Copyright © 2023 Cask Data, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package io.cdap.cdap.proto.credential; + +/** + * Represents a validation request for a {@link CredentialIdentity}. + */ +public class ValidateIdentityRequest { + private final CredentialIdentity identity; + private final CredentialProvisionContext context; + + public ValidateIdentityRequest(CredentialIdentity identity, CredentialProvisionContext context) { + this.identity = identity; + this.context = context; + } + + public CredentialIdentity getIdentity() { + return identity; + } + + public CredentialProvisionContext getContext() { + return context; + } +} diff --git a/cdap-security-spi/src/main/java/io/cdap/cdap/security/spi/credential/CredentialProvider.java b/cdap-security-spi/src/main/java/io/cdap/cdap/security/spi/credential/CredentialProvider.java index 5e2afd27300d..4b9f2196c890 100644 --- a/cdap-security-spi/src/main/java/io/cdap/cdap/security/spi/credential/CredentialProvider.java +++ b/cdap-security-spi/src/main/java/io/cdap/cdap/security/spi/credential/CredentialProvider.java @@ -16,12 +16,11 @@ package io.cdap.cdap.security.spi.credential; -import io.cdap.cdap.proto.NamespaceMeta; import io.cdap.cdap.proto.credential.CredentialIdentity; import io.cdap.cdap.proto.credential.CredentialProfile; +import io.cdap.cdap.proto.credential.CredentialProvisionContext; import io.cdap.cdap.proto.credential.CredentialProvisioningException; import io.cdap.cdap.proto.credential.ProvisionedCredential; -import javax.annotation.Nullable; /** * Defines an SPI for provisioning a credential. @@ -47,15 +46,15 @@ public interface CredentialProvider { * Provisions a short-lived credential for the provided identity using the provided credential * profile. * - * @param namespaceMeta The credential identity namespace metadata. - * @param profile The credential profile to use. - * @param identity The credential identity to use. - * @param scopes A comma separated list of OAuth scopes requested. + * @param namespace The credential identity namespace. + * @param profile The credential profile to use. + * @param identity The credential identity to use. + * @param context The context to use for provisioning. * @return A credential provisioned using the specified profile and identity. * @throws CredentialProvisioningException If the credential provisioning fails. */ - ProvisionedCredential provision(NamespaceMeta namespaceMeta, CredentialProfile profile, - CredentialIdentity identity, @Nullable String scopes) + ProvisionedCredential provision(String namespace, CredentialProfile profile, + CredentialIdentity identity, CredentialProvisionContext context) throws CredentialProvisioningException; /**