Skip to content

Commit

Permalink
[CDAP-20893] Move created workload identities to system namespace
Browse files Browse the repository at this point in the history
[CDAP-20893] Disallow provisioning access token in non-system credential identity namespaces

[CDAP-20893] Use identity validation in NamespaceCredentialProvider

[CDAP-20893] Include identity in ProvisionedCredentialCacheKEy hashcode

[CDAP-20893] Fix usage of namespaceMeta.getIdentity when creating credential identities for workload identity
  • Loading branch information
dli357 committed Nov 28, 2023
1 parent 9ef153f commit 4a71b42
Show file tree
Hide file tree
Showing 18 changed files with 412 additions and 183 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -51,21 +49,18 @@ public class DefaultCredentialProviderService extends AbstractIdleService
private final Map<String, CredentialProvider> 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
Expand All @@ -87,55 +82,51 @@ 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.
* @throws NotFoundException If the identity or profile are not found.
*/
@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<CredentialIdentity> 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());
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,33 +24,34 @@
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;

/**
* 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,
Expand All @@ -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()) {
Expand All @@ -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.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -120,24 +120,20 @@ 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())) {
throw new BadRequestException("Cannot validate identity in a namespace that is "
+ "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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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> T deserializeRequestContent(FullHttpRequest request, Class<T> 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);
}
}
}
Loading

0 comments on commit 4a71b42

Please sign in to comment.