This is an automated email from the ASF dual-hosted git repository.
jshao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 94c6a724d [#5842] feat(core): supports credential REST endpoint in
Gravitino server (#5841)
94c6a724d is described below
commit 94c6a724dd94bce383c00cf238f87f506e9a160f
Author: FANNG <[email protected]>
AuthorDate: Tue Dec 17 19:11:01 2024 +0800
[#5842] feat(core): supports credential REST endpoint in Gravitino server
(#5841)
### What changes were proposed in this pull request?
add credential REST endpoint in Gravitino server
### Why are the changes needed?
Fix: #5842
### Does this PR introduce _any_ user-facing change?
no
### How was this patch tested?
add UT and run test in POC
---------
Co-authored-by: Jerry Shao <[email protected]>
---
.../java/org/apache/gravitino/GravitinoEnv.java | 14 ++
.../apache/gravitino/catalog/CatalogManager.java | 4 +
.../gravitino/catalog/CredentialManager.java | 53 +++++++
.../apache/gravitino/server/GravitinoServer.java | 2 +
.../server/web/rest/ExceptionHandlers.java | 38 +++++
.../rest/MetadataObjectCredentialOperations.java | 100 +++++++++++++
.../TestMetadataObjectCredentialOperations.java | 164 +++++++++++++++++++++
7 files changed, 375 insertions(+)
diff --git a/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
b/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
index 1cad967a9..db6ddc235 100644
--- a/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
+++ b/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
@@ -28,6 +28,7 @@ import org.apache.gravitino.auxiliary.AuxiliaryServiceManager;
import org.apache.gravitino.catalog.CatalogDispatcher;
import org.apache.gravitino.catalog.CatalogManager;
import org.apache.gravitino.catalog.CatalogNormalizeDispatcher;
+import org.apache.gravitino.catalog.CredentialManager;
import org.apache.gravitino.catalog.FilesetDispatcher;
import org.apache.gravitino.catalog.FilesetNormalizeDispatcher;
import org.apache.gravitino.catalog.FilesetOperationDispatcher;
@@ -105,6 +106,8 @@ public class GravitinoEnv {
private MetalakeDispatcher metalakeDispatcher;
+ private CredentialManager credentialManager;
+
private AccessControlDispatcher accessControlDispatcher;
private IdGenerator idGenerator;
@@ -257,6 +260,15 @@ public class GravitinoEnv {
return metalakeDispatcher;
}
+ /**
+ * Get the {@link CredentialManager} associated with the Gravitino
environment.
+ *
+ * @return The {@link CredentialManager} instance.
+ */
+ public CredentialManager credentialManager() {
+ return credentialManager;
+ }
+
/**
* Get the IdGenerator associated with the Gravitino environment.
*
@@ -417,6 +429,8 @@ public class GravitinoEnv {
new CatalogNormalizeDispatcher(catalogHookDispatcher);
this.catalogDispatcher = new CatalogEventDispatcher(eventBus,
catalogNormalizeDispatcher);
+ this.credentialManager = new CredentialManager(catalogManager,
entityStore, idGenerator);
+
SchemaOperationDispatcher schemaOperationDispatcher =
new SchemaOperationDispatcher(catalogManager, entityStore,
idGenerator);
SchemaHookDispatcher schemaHookDispatcher = new
SchemaHookDispatcher(schemaOperationDispatcher);
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
b/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
index da79ff702..2e77b8e16 100644
--- a/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
+++ b/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
@@ -133,6 +133,10 @@ public class CatalogManager implements CatalogDispatcher,
Closeable {
this.classLoader = classLoader;
}
+ public BaseCatalog catalog() {
+ return catalog;
+ }
+
public <R> R doWithSchemaOps(ThrowableFunction<SupportsSchemas, R> fn)
throws Exception {
return classLoader.withClassLoader(
cl -> {
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/CredentialManager.java
b/core/src/main/java/org/apache/gravitino/catalog/CredentialManager.java
new file mode 100644
index 000000000..808fc96fb
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/catalog/CredentialManager.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.gravitino.catalog;
+
+import java.util.List;
+import org.apache.commons.lang3.NotImplementedException;
+import org.apache.gravitino.EntityStore;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.connector.BaseCatalog;
+import org.apache.gravitino.credential.Credential;
+import org.apache.gravitino.exceptions.NoSuchCatalogException;
+import org.apache.gravitino.storage.IdGenerator;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+
+/** Get credentials with the specific catalog classloader. */
+public class CredentialManager extends OperationDispatcher {
+
+ public CredentialManager(
+ CatalogManager catalogManager, EntityStore store, IdGenerator
idGenerator) {
+ super(catalogManager, store, idGenerator);
+ }
+
+ public List<Credential> getCredentials(NameIdentifier identifier) {
+ return doWithCatalog(
+ NameIdentifierUtil.getCatalogIdentifier(identifier),
+ c -> getCredentials(c.catalog(), identifier),
+ NoSuchCatalogException.class);
+ }
+
+ private List<Credential> getCredentials(BaseCatalog catalog, NameIdentifier
identifier) {
+ throw new NotImplementedException(
+ String.format(
+ "Load credentials is not implemented for catalog: %s, identifier:
%s",
+ catalog.name(), identifier));
+ }
+}
diff --git
a/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
b/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
index 36c112f00..554791fff 100644
--- a/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
+++ b/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
@@ -26,6 +26,7 @@ import javax.servlet.Servlet;
import org.apache.gravitino.Configs;
import org.apache.gravitino.GravitinoEnv;
import org.apache.gravitino.catalog.CatalogDispatcher;
+import org.apache.gravitino.catalog.CredentialManager;
import org.apache.gravitino.catalog.FilesetDispatcher;
import org.apache.gravitino.catalog.PartitionDispatcher;
import org.apache.gravitino.catalog.SchemaDispatcher;
@@ -114,6 +115,7 @@ public class GravitinoServer extends ResourceConfig {
bind(gravitinoEnv.filesetDispatcher()).to(FilesetDispatcher.class).ranked(1);
bind(gravitinoEnv.topicDispatcher()).to(TopicDispatcher.class).ranked(1);
bind(gravitinoEnv.tagManager()).to(TagManager.class).ranked(1);
+
bind(gravitinoEnv.credentialManager()).to(CredentialManager.class).ranked(1);
}
});
register(JsonProcessingExceptionMapper.class);
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
b/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
index 8d1ba8565..faf94f506 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
@@ -121,6 +121,11 @@ public class ExceptionHandlers {
return TagExceptionHandler.INSTANCE.handle(op, tag, parent, e);
}
+ public static Response handleCredentialException(
+ OperationType op, String metadataObjectName, Exception e) {
+ return CredentialExceptionHandler.INSTANCE.handle(op, metadataObjectName,
"", e);
+ }
+
public static Response handleTestConnectionException(Exception e) {
ErrorResponse response;
if (e instanceof IllegalArgumentException) {
@@ -369,6 +374,7 @@ public class ExceptionHandlers {
}
private static class FilesetExceptionHandler extends BaseExceptionHandler {
+
private static final ExceptionHandler INSTANCE = new
FilesetExceptionHandler();
private static String getFilesetErrorMsg(
@@ -520,6 +526,7 @@ public class ExceptionHandlers {
}
private static class TopicExceptionHandler extends BaseExceptionHandler {
+
private static final ExceptionHandler INSTANCE = new
TopicExceptionHandler();
private static String getTopicErrorMsg(
@@ -558,6 +565,7 @@ public class ExceptionHandlers {
private static class UserPermissionOperationExceptionHandler
extends BasePermissionExceptionHandler {
+
private static final ExceptionHandler INSTANCE = new
UserPermissionOperationExceptionHandler();
@Override
@@ -622,6 +630,35 @@ public class ExceptionHandlers {
}
}
+ private static class CredentialExceptionHandler extends BaseExceptionHandler
{
+
+ private static final ExceptionHandler INSTANCE = new
CredentialExceptionHandler();
+
+ private static String getCredentialErrorMsg(String parent, String reason) {
+ return String.format(
+ "Failed to get credentials under object [%s], reason [%s]", parent,
reason);
+ }
+
+ @Override
+ public Response handle(OperationType op, String credential, String parent,
Exception e) {
+ String errorMsg = getCredentialErrorMsg(parent, getErrorMsg(e));
+ LOG.warn(errorMsg, e);
+
+ if (e instanceof IllegalArgumentException) {
+ return Utils.illegalArguments(errorMsg, e);
+
+ } else if (e instanceof NotFoundException) {
+ return Utils.notFound(errorMsg, e);
+
+ } else if (e instanceof NotInUseException) {
+ return Utils.notInUse(errorMsg, e);
+
+ } else {
+ return super.handle(op, credential, parent, e);
+ }
+ }
+ }
+
private static class TagExceptionHandler extends BaseExceptionHandler {
private static final ExceptionHandler INSTANCE = new TagExceptionHandler();
@@ -661,6 +698,7 @@ public class ExceptionHandlers {
}
private static class OwnerExceptionHandler extends BaseExceptionHandler {
+
private static final ExceptionHandler INSTANCE = new
OwnerExceptionHandler();
private static String getOwnerErrorMsg(
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectCredentialOperations.java
b/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectCredentialOperations.java
new file mode 100644
index 000000000..7c6ea4a8e
--- /dev/null
+++
b/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectCredentialOperations.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.gravitino.server.web.rest;
+
+import com.codahale.metrics.annotation.ResponseMetered;
+import com.codahale.metrics.annotation.Timed;
+import java.util.List;
+import java.util.Locale;
+import javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.catalog.CredentialManager;
+import org.apache.gravitino.credential.Credential;
+import org.apache.gravitino.dto.credential.CredentialDTO;
+import org.apache.gravitino.dto.responses.CredentialResponse;
+import org.apache.gravitino.dto.util.DTOConverters;
+import org.apache.gravitino.metrics.MetricNames;
+import org.apache.gravitino.server.web.Utils;
+import org.apache.gravitino.utils.MetadataObjectUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Path("/metalakes/{metalake}/objects/{type}/{fullName}/credentials")
+public class MetadataObjectCredentialOperations {
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(MetadataObjectCredentialOperations.class);
+
+ private CredentialManager credentialManager;
+
+ @SuppressWarnings("unused")
+ @Context
+ private HttpServletRequest httpRequest;
+
+ @Inject
+ public MetadataObjectCredentialOperations(CredentialManager dispatcher) {
+ this.credentialManager = dispatcher;
+ }
+
+ @GET
+ @Produces("application/vnd.gravitino.v1+json")
+ @Timed(name = "get-credentials." + MetricNames.HTTP_PROCESS_DURATION,
absolute = true)
+ @ResponseMetered(name = "get-credentials", absolute = true)
+ public Response getCredentials(
+ @PathParam("metalake") String metalake,
+ @PathParam("type") String type,
+ @PathParam("fullName") String fullName) {
+ LOG.info(
+ "Received get credentials request for object type: {}, full name: {}
under metalake: {}",
+ type,
+ fullName,
+ metalake);
+
+ try {
+ return Utils.doAs(
+ httpRequest,
+ () -> {
+ MetadataObject object =
+ MetadataObjects.parse(
+ fullName,
MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT)));
+
+ NameIdentifier identifier =
MetadataObjectUtil.toEntityIdent(metalake, object);
+ List<Credential> credentials =
credentialManager.getCredentials(identifier);
+ if (credentials == null) {
+ return Utils.ok(new CredentialResponse(new CredentialDTO[0]));
+ }
+ return Utils.ok(
+ new CredentialResponse(
+ DTOConverters.toDTO(credentials.toArray(new
Credential[credentials.size()]))));
+ });
+ } catch (Exception e) {
+ return ExceptionHandlers.handleCredentialException(OperationType.GET,
fullName, e);
+ }
+ }
+}
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectCredentialOperations.java
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectCredentialOperations.java
new file mode 100644
index 000000000..1ac5d3813
--- /dev/null
+++
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectCredentialOperations.java
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.gravitino.server.web.rest;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.Arrays;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.catalog.CredentialManager;
+import org.apache.gravitino.credential.Credential;
+import org.apache.gravitino.credential.S3SecretKeyCredential;
+import org.apache.gravitino.dto.responses.CredentialResponse;
+import org.apache.gravitino.dto.responses.ErrorConstants;
+import org.apache.gravitino.dto.responses.ErrorResponse;
+import org.apache.gravitino.dto.util.DTOConverters;
+import org.apache.gravitino.exceptions.NoSuchCredentialException;
+import org.apache.gravitino.rest.RESTUtils;
+import org.glassfish.jersey.internal.inject.AbstractBinder;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.glassfish.jersey.test.TestProperties;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestMetadataObjectCredentialOperations extends JerseyTest {
+
+ private static class MockServletRequestFactory extends
ServletRequestFactoryBase {
+
+ @Override
+ public HttpServletRequest get() {
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ when(request.getRemoteUser()).thenReturn(null);
+ return request;
+ }
+ }
+
+ private CredentialManager credentialManager = mock(CredentialManager.class);
+
+ private String metalake = "test_metalake";
+
+ @Override
+ protected Application configure() {
+ try {
+ forceSet(
+ TestProperties.CONTAINER_PORT,
String.valueOf(RESTUtils.findAvailablePort(2000, 3000)));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ ResourceConfig resourceConfig = new ResourceConfig();
+ resourceConfig.register(MetadataObjectCredentialOperations.class);
+ resourceConfig.register(
+ new AbstractBinder() {
+ @Override
+ protected void configure() {
+ bind(credentialManager).to(CredentialManager.class).ranked(2);
+
bindFactory(MockServletRequestFactory.class).to(HttpServletRequest.class);
+ }
+ });
+
+ return resourceConfig;
+ }
+
+ @Test
+ public void testGetCredentialsForCatalog() {
+ testGetCredentialsForObject(MetadataObjects.parse("catalog",
MetadataObject.Type.CATALOG));
+ }
+
+ @Test
+ public void testGetCredentialsForFileset() {
+ testGetCredentialsForObject(
+ MetadataObjects.parse("catalog.schema.fileset",
MetadataObject.Type.FILESET));
+ }
+
+ private void testGetCredentialsForObject(MetadataObject metadataObject) {
+
+ S3SecretKeyCredential credential = new S3SecretKeyCredential("access-id",
"secret-key");
+ // Test return one credential
+
when(credentialManager.getCredentials(any())).thenReturn(Arrays.asList(credential));
+ Response response =
+ target(basePath(metalake))
+ .path(metadataObject.type().toString())
+ .path(metadataObject.fullName())
+ .path("/credentials")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
response.getStatus());
+ CredentialResponse credentialResponse =
response.readEntity(CredentialResponse.class);
+ Assertions.assertEquals(0, credentialResponse.getCode());
+ Assertions.assertEquals(1, credentialResponse.getCredentials().length);
+ Credential credentialToTest =
DTOConverters.fromDTO(credentialResponse.getCredentials()[0]);
+ Assertions.assertTrue(credentialToTest instanceof S3SecretKeyCredential);
+ Assertions.assertEquals("access-id", ((S3SecretKeyCredential)
credentialToTest).accessKeyId());
+ Assertions.assertEquals(
+ "secret-key", ((S3SecretKeyCredential)
credentialToTest).secretAccessKey());
+ Assertions.assertEquals(0, credentialToTest.expireTimeInMs());
+
+ // Test doesn't return credential
+ when(credentialManager.getCredentials(any())).thenReturn(null);
+ response =
+ target(basePath(metalake))
+ .path(metadataObject.type().toString())
+ .path(metadataObject.fullName())
+ .path("/credentials")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
response.getStatus());
+ credentialResponse = response.readEntity(CredentialResponse.class);
+ Assertions.assertEquals(0, credentialResponse.getCode());
+ Assertions.assertEquals(0, credentialResponse.getCredentials().length);
+
+ // Test throws NoSuchCredentialException
+ doThrow(new NoSuchCredentialException("mock error"))
+ .when(credentialManager)
+ .getCredentials(any());
+ response =
+ target(basePath(metalake))
+ .path(metadataObject.type().toString())
+ .path(metadataObject.fullName())
+ .path("/credentials")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(),
response.getStatus());
+ ErrorResponse errorResponse = response.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE,
errorResponse.getCode());
+ Assertions.assertEquals(
+ NoSuchCredentialException.class.getSimpleName(),
errorResponse.getType());
+ }
+
+ private String basePath(String metalake) {
+ return "/metalakes/" + metalake + "/objects";
+ }
+}