This is an automated email from the ASF dual-hosted git repository.
roryqi 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 8fb593a47d [#11834] feat(core): Add group external ID and
delete-by-external-id APIs (#11914)
8fb593a47d is described below
commit 8fb593a47da29e7da5287affc9c9d64b520247ae
Author: MaSai <[email protected]>
AuthorDate: Tue Jul 7 15:09:40 2026 +0800
[#11834] feat(core): Add group external ID and delete-by-external-id APIs
(#11914)
### What changes were proposed in this pull request?
- Add `externalId` support to Group entity, DTO, PO, and relational
storage (mapper/SQL).
- Add group external-id APIs: `addGroup(metalake, group, externalId)`,
`getGroupByExternalId`, and `removeGroupByExternalId`.
- Wire external-id operations through `UserGroupExternalManager`,
`SupportsExternalIdOperations`, and `GroupMetaService`.
- Align group external-id semantics with merged user SCIM APIs in #11848
(NameIdentifier-based lookup, `removeGroupByExternalId` returns `false`
when missing).
Fix: #11834
Part of epic #11830. User external-id work is already in #11848.
### Why are the changes needed?
SCIM and external identity providers need stable group identifiers
independent of Gravitino group names. This completes the group side of
the core platform prerequisites for user/group access control.
### Does this PR introduce _any_ user-facing change?
1. New `Group.externalId()` API field.
2. New `AccessControlDispatcher` methods:
- `addGroup(String metalake, String group, String externalId)`
- `getGroupByExternalId(String metalake, String externalId)`
- `removeGroupByExternalId(String metalake, String externalId)`
### How was this patch tested?
```bash
export
JAVA_HOME=/Library/Java/JavaVirtualMachines/microsoft-17.jdk/Contents/Home
./gradlew :core:test \
--tests "org.apache.gravitino.authorization.TestAccessControlManager" \
--tests
"org.apache.gravitino.storage.relational.service.TestGroupMetaService" \
--tests "org.apache.gravitino.authorization.TestAuthorizationUtils" \
--tests "org.apache.gravitino.storage.memory.TestMemoryEntityStore" \
-PskipITs
```
Made with [Cursor](https://cursor.com)
---------
Co-authored-by: Cursor <[email protected]>
---
.../org/apache/gravitino/authorization/Group.java | 16 +++++
.../apache/gravitino/cli/commands/ListGroups.java | 5 ++
.../gravitino/dto/authorization/GroupDTO.java | 30 +++++++++-
.../apache/gravitino/dto/util/DTOConverters.java | 1 +
.../src/main/java/org/apache/gravitino/Entity.java | 11 ++++
.../authorization/AccessControlDispatcher.java | 43 ++++++++++++++
.../authorization/AccessControlManager.java | 27 +++++++++
.../authorization/AuthorizationUtils.java | 44 ++++++++++++++
.../gravitino/authorization/PermissionManager.java | 2 +
.../authorization/UserGroupExternalManager.java | 69 +++++++++++++++++++++-
.../hook/AccessControlHookDispatcher.java | 18 ++++++
.../listener/AccessControlEventDispatcher.java | 21 +++++++
.../org/apache/gravitino/meta/GroupEntity.java | 24 +++++++-
.../gravitino/storage/relational/JDBCBackend.java | 4 +-
.../storage/relational/mapper/GroupMetaMapper.java | 6 ++
.../mapper/GroupMetaSQLProviderFactory.java | 5 ++
.../provider/base/GroupMetaBaseSQLProvider.java | 43 ++++++++++----
.../mapper/provider/h2/GroupMetaH2Provider.java | 2 +
.../postgresql/GroupMetaPostgreSQLProvider.java | 8 ++-
.../gravitino/storage/relational/po/GroupPO.java | 12 ++++
.../relational/service/GroupMetaService.java | 28 +++++++++
.../storage/relational/utils/POConverters.java | 4 ++
.../authorization/TestAccessControlManager.java | 36 +++++++++++
.../authorization/TestAuthorizationUtils.java | 23 ++++++++
.../storage/memory/TestMemoryEntityStore.java | 37 ++++++++----
.../provider/h2/TestGroupMetaH2Provider.java | 7 ++-
.../relational/service/TestGroupMetaService.java | 60 +++++++++++++++++++
27 files changed, 556 insertions(+), 30 deletions(-)
diff --git a/api/src/main/java/org/apache/gravitino/authorization/Group.java
b/api/src/main/java/org/apache/gravitino/authorization/Group.java
index c1cd72fbab..307380d4b9 100644
--- a/api/src/main/java/org/apache/gravitino/authorization/Group.java
+++ b/api/src/main/java/org/apache/gravitino/authorization/Group.java
@@ -19,6 +19,7 @@
package org.apache.gravitino.authorization;
import java.util.List;
+import javax.annotation.Nullable;
import org.apache.gravitino.Auditable;
import org.apache.gravitino.annotation.Evolving;
@@ -33,6 +34,21 @@ public interface Group extends Auditable {
*/
String name();
+ /**
+ * The stable identifier assigned by an upstream identity system (for
example, SCIM, LDAP, or
+ * IAM), or null if not set.
+ *
+ * <p>Gravitino {@link Group#name() group names} may differ from upstream
ids or be unknown at
+ * sync time. External id lets integrators look up and delete groups without
relying on the
+ * Gravitino group name.
+ *
+ * @return The upstream external identifier, or null if not set.
+ */
+ @Nullable
+ default String externalId() {
+ return null;
+ }
+
/**
* The roles of the group.
*
diff --git
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListGroups.java
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListGroups.java
index 8950bde792..588f30393e 100644
---
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListGroups.java
+++
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListGroups.java
@@ -73,6 +73,11 @@ public class ListGroups extends Command {
return name;
}
+ @Override
+ public String externalId() {
+ return null;
+ }
+
@Override
public List<String> roles() {
return null;
diff --git
a/common/src/main/java/org/apache/gravitino/dto/authorization/GroupDTO.java
b/common/src/main/java/org/apache/gravitino/dto/authorization/GroupDTO.java
index e8ff49804e..61fbc814df 100644
--- a/common/src/main/java/org/apache/gravitino/dto/authorization/GroupDTO.java
+++ b/common/src/main/java/org/apache/gravitino/dto/authorization/GroupDTO.java
@@ -22,6 +22,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Preconditions;
import java.util.Collections;
import java.util.List;
+import javax.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.Audit;
import org.apache.gravitino.authorization.Group;
@@ -33,6 +34,10 @@ public class GroupDTO implements Group {
@JsonProperty("name")
private String name;
+ @Nullable
+ @JsonProperty("externalId")
+ private String externalId;
+
@JsonProperty("audit")
private AuditDTO audit;
@@ -46,11 +51,13 @@ public class GroupDTO implements Group {
* Creates a new instance of GroupDTO.
*
* @param name The name of the Group DTO.
+ * @param externalId The external id of the Group DTO.
* @param roles The roles of the Group DTO.
* @param audit The audit information of the Group DTO.
*/
- protected GroupDTO(String name, List<String> roles, AuditDTO audit) {
+ protected GroupDTO(String name, String externalId, List<String> roles,
AuditDTO audit) {
this.name = name;
+ this.externalId = externalId;
this.audit = audit;
this.roles = roles;
}
@@ -63,6 +70,11 @@ public class GroupDTO implements Group {
return name;
}
+ @Override
+ public String externalId() {
+ return externalId;
+ }
+
/**
* The roles of the group. A group can have multiple roles. Every role binds
several privileges.
*
@@ -100,6 +112,9 @@ public class GroupDTO implements Group {
/** The name of the group. */
protected String name;
+ /** The external id of the group. */
+ protected String externalId;
+
/** The roles of the group. */
protected List<String> roles = Collections.emptyList();
@@ -117,6 +132,17 @@ public class GroupDTO implements Group {
return (S) this;
}
+ /**
+ * Sets the external id of the group.
+ *
+ * @param externalId The external id of the group.
+ * @return The builder instance.
+ */
+ public S withExternalId(String externalId) {
+ this.externalId = externalId;
+ return (S) this;
+ }
+
/**
* Sets the roles of the group.
*
@@ -151,7 +177,7 @@ public class GroupDTO implements Group {
public GroupDTO build() {
Preconditions.checkArgument(StringUtils.isNotBlank(name), "name cannot
be null or empty");
Preconditions.checkArgument(audit != null, "audit cannot be null");
- return new GroupDTO(name, roles, audit);
+ return new GroupDTO(name, externalId, roles, audit);
}
}
}
diff --git
a/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java
b/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java
index e5843aa689..2279048a91 100644
--- a/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java
+++ b/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java
@@ -514,6 +514,7 @@ public class DTOConverters {
return GroupDTO.builder()
.withName(group.name())
+ .withExternalId(group.externalId())
.withRoles(group.roles())
.withAudit(toDTO(group.auditInfo()))
.build();
diff --git a/core/src/main/java/org/apache/gravitino/Entity.java
b/core/src/main/java/org/apache/gravitino/Entity.java
index 555bd2b3a3..29bae55637 100644
--- a/core/src/main/java/org/apache/gravitino/Entity.java
+++ b/core/src/main/java/org/apache/gravitino/Entity.java
@@ -55,6 +55,17 @@ public interface Entity extends Serializable {
/** The group schema name in the system catalog. */
String GROUP_SCHEMA_NAME = "group";
+ /**
+ * A virtual schema name used only for {@link
org.apache.gravitino.lock.TreeLockUtils} lock paths
+ * when operating on groups by external id (for example,
get/delete-by-external-id).
+ *
+ * <p>This is not a real metadata schema and does not store entities. It
forms part of a synthetic
+ * {@link org.apache.gravitino.NameIdentifier} such as {@code {metalake,
system,
+ * group-external-id, <externalId>}} so that concurrent operations on the
same external id are
+ * serialized without requiring the Gravitino group name.
+ */
+ String GROUP_EXTERNAL_ID_SCHEMA_NAME = "group-external-id";
+
/** The role schema name in the system catalog. */
String ROLE_SCHEMA_NAME = "role";
diff --git
a/core/src/main/java/org/apache/gravitino/authorization/AccessControlDispatcher.java
b/core/src/main/java/org/apache/gravitino/authorization/AccessControlDispatcher.java
index 258f20a6af..b657700bc9 100644
---
a/core/src/main/java/org/apache/gravitino/authorization/AccessControlDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/authorization/AccessControlDispatcher.java
@@ -176,6 +176,21 @@ public interface AccessControlDispatcher {
Group addGroup(String metalake, String group)
throws GroupAlreadyExistsException, NoSuchMetalakeException;
+ /**
+ * Adds a new Group.
+ *
+ * @param metalake The Metalake of the Group.
+ * @param group The name of the Group.
+ * @param externalId The external identifier, or null if unset.
+ * @return The Added Group instance.
+ * @throws GroupAlreadyExistsException If a Group with the same name or
external id already
+ * exists.
+ * @throws NoSuchMetalakeException If the Metalake with the given name does
not exist.
+ * @throws RuntimeException If adding the Group encounters storage issues.
+ */
+ Group addGroup(String metalake, String group, String externalId)
+ throws GroupAlreadyExistsException, NoSuchMetalakeException;
+
/**
* Removes a Group.
*
@@ -188,6 +203,20 @@ public interface AccessControlDispatcher {
*/
boolean removeGroup(String metalake, String group) throws
NoSuchMetalakeException;
+ /**
+ * Removes a Group by external identifier.
+ *
+ * @param metalake The Metalake of the Group.
+ * @param externalId The external identifier of the Group.
+ * @return True if the Group was successfully removed, false only when
there's no such group,
+ * otherwise it will throw an exception.
+ * @throws IllegalArgumentException If externalId is null or blank.
+ * @throws NoSuchMetalakeException If the Metalake with the given name does
not exist.
+ * @throws RuntimeException If removing the Group encounters storage issues.
+ */
+ boolean removeGroupByExternalId(String metalake, String externalId)
+ throws NoSuchMetalakeException;
+
/**
* Gets a Group.
*
@@ -201,6 +230,20 @@ public interface AccessControlDispatcher {
Group getGroup(String metalake, String group)
throws NoSuchGroupException, NoSuchMetalakeException;
+ /**
+ * Gets a Group by external identifier.
+ *
+ * @param metalake The Metalake of the Group.
+ * @param externalId The external identifier of the Group.
+ * @return The getting Group instance.
+ * @throws IllegalArgumentException If externalId is null or blank.
+ * @throws NoSuchGroupException If the Group with the given external id does
not exist.
+ * @throws NoSuchMetalakeException If the Metalake with the given name does
not exist.
+ * @throws RuntimeException If getting the Group encounters storage issues.
+ */
+ Group getGroupByExternalId(String metalake, String externalId)
+ throws NoSuchGroupException, NoSuchMetalakeException;
+
/**
* List groups
*
diff --git
a/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java
b/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java
index 25e40e18f9..2d21c4eb8a 100644
---
a/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java
+++
b/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java
@@ -156,6 +156,15 @@ public class AccessControlManager implements
AccessControlDispatcher {
() -> userGroupManager.addGroup(metalake, group));
}
+ @Override
+ public Group addGroup(String metalake, String group, String externalId)
+ throws GroupAlreadyExistsException, NoSuchMetalakeException {
+ return TreeLockUtils.doWithTreeLock(
+
NameIdentifier.of(AuthorizationUtils.ofGroupNamespace(metalake).levels()),
+ LockType.WRITE,
+ () -> userGroupExternalManager.addGroup(metalake, group, externalId));
+ }
+
@Override
public boolean removeGroup(String metalake, String group) throws
NoSuchMetalakeException {
return TreeLockUtils.doWithTreeLock(
@@ -164,6 +173,15 @@ public class AccessControlManager implements
AccessControlDispatcher {
() -> userGroupManager.removeGroup(metalake, group));
}
+ @Override
+ public boolean removeGroupByExternalId(String metalake, String externalId)
+ throws NoSuchMetalakeException {
+ return TreeLockUtils.doWithTreeLock(
+ AuthorizationUtils.ofGroupExternalId(metalake, externalId),
+ LockType.WRITE,
+ () -> userGroupExternalManager.removeGroupByExternalId(metalake,
externalId));
+ }
+
@Override
public Group getGroup(String metalake, String group)
throws NoSuchGroupException, NoSuchMetalakeException {
@@ -173,6 +191,15 @@ public class AccessControlManager implements
AccessControlDispatcher {
() -> userGroupManager.getGroup(metalake, group));
}
+ @Override
+ public Group getGroupByExternalId(String metalake, String externalId)
+ throws NoSuchGroupException, NoSuchMetalakeException {
+ return TreeLockUtils.doWithTreeLock(
+ AuthorizationUtils.ofGroupExternalId(metalake, externalId),
+ LockType.READ,
+ () -> userGroupExternalManager.getGroupByExternalId(metalake,
externalId));
+ }
+
@Override
public Group[] listGroups(String metalake) throws NoSuchMetalakeException {
return TreeLockUtils.doWithTreeLock(
diff --git
a/core/src/main/java/org/apache/gravitino/authorization/AuthorizationUtils.java
b/core/src/main/java/org/apache/gravitino/authorization/AuthorizationUtils.java
index d768df1743..c16d6a3beb 100644
---
a/core/src/main/java/org/apache/gravitino/authorization/AuthorizationUtils.java
+++
b/core/src/main/java/org/apache/gravitino/authorization/AuthorizationUtils.java
@@ -67,6 +67,8 @@ public class AuthorizationUtils {
static final String USER_WITH_EXTERNAL_ID_DOES_NOT_EXIST_MSG =
"User with external id %s does not exist in the metalake %s";
static final String GROUP_DOES_NOT_EXIST_MSG = "Group %s does not exist in
the metalake %s";
+ static final String GROUP_WITH_EXTERNAL_ID_DOES_NOT_EXIST_MSG =
+ "Group with external id %s does not exist in the metalake %s";
static final String ROLE_DOES_NOT_EXIST_MSG = "Role %s does not exist in the
metalake %s";
/**
@@ -163,6 +165,26 @@ public class AuthorizationUtils {
externalId);
}
+ /**
+ * Creates a synthetic {@link NameIdentifier} used only as a {@link
+ * org.apache.gravitino.lock.TreeLockUtils} lock path for group operations
keyed by external id.
+ *
+ * <p>This is <strong>not</strong> the entity's storage identifier. Group
entities are stored and
+ * retrieved by Gravitino group name via {@link #ofGroup(String, String)}.
At lock time the group
+ * name may be unknown, so external-id operations need a dedicated lock path.
+ *
+ * @param metalake the metalake name
+ * @param externalId the external id of the group
+ * @return a synthetic name identifier for tree locking only
+ */
+ public static NameIdentifier ofGroupExternalId(String metalake, String
externalId) {
+ return NameIdentifier.of(
+ metalake,
+ Entity.SYSTEM_CATALOG_RESERVED_NAME,
+ Entity.GROUP_EXTERNAL_ID_SCHEMA_NAME,
+ externalId);
+ }
+
public static Namespace ofRoleNamespace(String metalake) {
return Namespace.of(metalake, Entity.SYSTEM_CATALOG_RESERVED_NAME,
Entity.ROLE_SCHEMA_NAME);
}
@@ -180,6 +202,11 @@ public class AuthorizationUtils {
metalake, Entity.SYSTEM_CATALOG_RESERVED_NAME,
Entity.USER_EXTERNAL_ID_SCHEMA_NAME);
}
+ public static Namespace ofGroupExternalIdNamespace(String metalake) {
+ return Namespace.of(
+ metalake, Entity.SYSTEM_CATALOG_RESERVED_NAME,
Entity.GROUP_EXTERNAL_ID_SCHEMA_NAME);
+ }
+
public static void checkUser(NameIdentifier ident) {
NameIdentifier.check(ident != null, "User identifier must not be null");
checkUserNamespace(ident.namespace());
@@ -200,6 +227,16 @@ public class AuthorizationUtils {
checkGroupNamespace(ident.namespace());
}
+ /**
+ * Validates that the name identifier refers to a group external id in a
metalake.
+ *
+ * @param ident the external id name identifier to validate
+ */
+ public static void checkGroupExternalId(NameIdentifier ident) {
+ NameIdentifier.check(ident != null, "External id identifier must not be
null");
+ checkGroupExternalIdNamespace(ident.namespace());
+ }
+
public static void checkRole(NameIdentifier ident) {
NameIdentifier.check(ident != null, "Role identifier must not be null");
checkRoleNamespace(ident.namespace());
@@ -219,6 +256,13 @@ public class AuthorizationUtils {
namespace);
}
+ public static void checkGroupExternalIdNamespace(Namespace namespace) {
+ Namespace.check(
+ namespace != null && namespace.length() == 3,
+ "Group external id namespace must have 3 levels, the input namespace
is %s",
+ namespace);
+ }
+
public static void checkGroupNamespace(Namespace namespace) {
Namespace.check(
namespace != null && namespace.length() == 3,
diff --git
a/core/src/main/java/org/apache/gravitino/authorization/PermissionManager.java
b/core/src/main/java/org/apache/gravitino/authorization/PermissionManager.java
index c864d69051..f732eff9f7 100644
---
a/core/src/main/java/org/apache/gravitino/authorization/PermissionManager.java
+++
b/core/src/main/java/org/apache/gravitino/authorization/PermissionManager.java
@@ -209,6 +209,7 @@ class PermissionManager {
.withId(groupEntity.id())
.withNamespace(groupEntity.namespace())
.withName(groupEntity.name())
+ .withExternalId(groupEntity.externalId())
.withRoleNames(roleNames)
.withRoleIds(roleIds)
.withAuditInfo(auditInfo)
@@ -297,6 +298,7 @@ class PermissionManager {
.withNamespace(groupEntity.namespace())
.withId(groupEntity.id())
.withName(groupEntity.name())
+ .withExternalId(groupEntity.externalId())
.withRoleNames(roleNames)
.withRoleIds(roleIds)
.withAuditInfo(auditInfo)
diff --git
a/core/src/main/java/org/apache/gravitino/authorization/UserGroupExternalManager.java
b/core/src/main/java/org/apache/gravitino/authorization/UserGroupExternalManager.java
index fe850a66f8..32475f6a64 100644
---
a/core/src/main/java/org/apache/gravitino/authorization/UserGroupExternalManager.java
+++
b/core/src/main/java/org/apache/gravitino/authorization/UserGroupExternalManager.java
@@ -24,17 +24,20 @@ import java.time.Instant;
import org.apache.gravitino.Entity;
import org.apache.gravitino.EntityAlreadyExistsException;
import org.apache.gravitino.EntityStore;
+import org.apache.gravitino.exceptions.GroupAlreadyExistsException;
import org.apache.gravitino.exceptions.NoSuchEntityException;
+import org.apache.gravitino.exceptions.NoSuchGroupException;
import org.apache.gravitino.exceptions.NoSuchUserException;
import org.apache.gravitino.exceptions.UserAlreadyExistsException;
import org.apache.gravitino.meta.AuditInfo;
+import org.apache.gravitino.meta.GroupEntity;
import org.apache.gravitino.meta.UserEntity;
import org.apache.gravitino.storage.IdGenerator;
import org.apache.gravitino.utils.PrincipalUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-/** Manages user operations keyed by external id within a metalake. */
+/** Manages user and group operations keyed by external id within a metalake.
*/
class UserGroupExternalManager extends UserGroupManager {
private static final Logger LOG =
LoggerFactory.getLogger(UserGroupExternalManager.class);
@@ -157,4 +160,68 @@ class UserGroupExternalManager extends UserGroupManager {
throw new RuntimeException(ioe);
}
}
+
+ Group addGroup(String metalake, String group, String externalId)
+ throws GroupAlreadyExistsException {
+ try {
+ GroupEntity groupEntity =
+ GroupEntity.builder()
+ .withId(idGenerator.nextId())
+ .withName(group)
+ .withNamespace(AuthorizationUtils.ofGroupNamespace(metalake))
+ .withRoleNames(Lists.newArrayList())
+ .withExternalId(externalId)
+ .withAuditInfo(
+ AuditInfo.builder()
+
.withCreator(PrincipalUtils.getCurrentPrincipal().getName())
+ .withCreateTime(Instant.now())
+ .build())
+ .build();
+ store.put(groupEntity, false /* overwritten */);
+ return groupEntity;
+ } catch (EntityAlreadyExistsException e) {
+ LOG.warn("Group {} in the metalake {} already exists", group, metalake,
e);
+ throw new GroupAlreadyExistsException(
+ "Group %s in the metalake %s already exists", group, metalake);
+ } catch (IOException ioe) {
+ LOG.error(
+ "Adding group {} failed in the metalake {} due to storage issues",
group, metalake, ioe);
+ throw new RuntimeException(ioe);
+ }
+ }
+
+ boolean removeGroupByExternalId(String metalake, String externalId) {
+ try {
+ return store
+ .externalIdOperations()
+ .deleteByExternalId(
+ AuthorizationUtils.ofGroupExternalId(metalake, externalId),
Entity.EntityType.GROUP);
+ } catch (IOException ioe) {
+ LOG.error(
+ "Removing group with external id {} in the metalake {} failed due to
storage issues",
+ externalId,
+ metalake,
+ ioe);
+ throw new RuntimeException(ioe);
+ }
+ }
+
+ Group getGroupByExternalId(String metalake, String externalId) throws
NoSuchGroupException {
+ try {
+ return store
+ .externalIdOperations()
+ .getByExternalId(
+ AuthorizationUtils.ofGroupExternalId(metalake, externalId),
+ Entity.EntityType.GROUP,
+ GroupEntity.class);
+ } catch (NoSuchEntityException e) {
+ LOG.warn(
+ "Group with external id {} does not exist in the metalake {}",
externalId, metalake, e);
+ throw new NoSuchGroupException(
+ AuthorizationUtils.GROUP_WITH_EXTERNAL_ID_DOES_NOT_EXIST_MSG,
externalId, metalake);
+ } catch (IOException ioe) {
+ LOG.error("Getting group with external id {} failed due to storage
issues", externalId, ioe);
+ throw new RuntimeException(ioe);
+ }
+ }
}
diff --git
a/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java
b/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java
index 9c7bb5aba1..3c1d8d7949 100644
---
a/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java
@@ -126,17 +126,35 @@ public class AccessControlHookDispatcher implements
AccessControlDispatcher {
return dispatcher.addGroup(metalake, group);
}
+ @Override
+ public Group addGroup(String metalake, String group, String externalId)
+ throws GroupAlreadyExistsException, NoSuchMetalakeException {
+ return dispatcher.addGroup(metalake, group, externalId);
+ }
+
@Override
public boolean removeGroup(String metalake, String group) throws
NoSuchMetalakeException {
return dispatcher.removeGroup(metalake, group);
}
+ @Override
+ public boolean removeGroupByExternalId(String metalake, String externalId)
+ throws NoSuchMetalakeException {
+ return dispatcher.removeGroupByExternalId(metalake, externalId);
+ }
+
@Override
public Group getGroup(String metalake, String group)
throws NoSuchGroupException, NoSuchMetalakeException {
return dispatcher.getGroup(metalake, group);
}
+ @Override
+ public Group getGroupByExternalId(String metalake, String externalId)
+ throws NoSuchGroupException, NoSuchMetalakeException {
+ return dispatcher.getGroupByExternalId(metalake, externalId);
+ }
+
@Override
public Group[] listGroups(String metalake) throws NoSuchMetalakeException {
return dispatcher.listGroups(metalake);
diff --git
a/core/src/main/java/org/apache/gravitino/listener/AccessControlEventDispatcher.java
b/core/src/main/java/org/apache/gravitino/listener/AccessControlEventDispatcher.java
index db12c51d8b..e91819e3fd 100644
---
a/core/src/main/java/org/apache/gravitino/listener/AccessControlEventDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/listener/AccessControlEventDispatcher.java
@@ -269,6 +269,13 @@ public class AccessControlEventDispatcher implements
AccessControlDispatcher {
}
}
+ /** {@inheritDoc} */
+ @Override
+ public Group addGroup(String metalake, String group, String externalId)
+ throws GroupAlreadyExistsException, NoSuchMetalakeException {
+ return dispatcher.addGroup(metalake, group, externalId);
+ }
+
/** {@inheritDoc} */
@Override
public boolean removeGroup(String metalake, String group) throws
NoSuchMetalakeException {
@@ -286,6 +293,13 @@ public class AccessControlEventDispatcher implements
AccessControlDispatcher {
}
}
+ /** {@inheritDoc} */
+ @Override
+ public boolean removeGroupByExternalId(String metalake, String externalId)
+ throws NoSuchMetalakeException {
+ return dispatcher.removeGroupByExternalId(metalake, externalId);
+ }
+
/** {@inheritDoc} */
@Override
public Group getGroup(String metalake, String group)
@@ -304,6 +318,13 @@ public class AccessControlEventDispatcher implements
AccessControlDispatcher {
}
}
+ /** {@inheritDoc} */
+ @Override
+ public Group getGroupByExternalId(String metalake, String externalId)
+ throws NoSuchGroupException, NoSuchMetalakeException {
+ return dispatcher.getGroupByExternalId(metalake, externalId);
+ }
+
/** {@inheritDoc} */
@Override
public Group[] listGroups(String metalake) {
diff --git a/core/src/main/java/org/apache/gravitino/meta/GroupEntity.java
b/core/src/main/java/org/apache/gravitino/meta/GroupEntity.java
index b8cac5ba4c..6dcf7c2f11 100644
--- a/core/src/main/java/org/apache/gravitino/meta/GroupEntity.java
+++ b/core/src/main/java/org/apache/gravitino/meta/GroupEntity.java
@@ -39,6 +39,9 @@ public class GroupEntity implements Group, Entity, Auditable,
HasIdentifier {
public static final Field NAME =
Field.required("name", String.class, "The name of the group entity.");
+ public static final Field EXTERNAL_ID =
+ Field.optional("external_id", String.class, "The external id of the
group entity.");
+
public static final Field ROLE_NAMES =
Field.optional("role_names", List.class, "The role names of the group
entity.");
@@ -50,6 +53,7 @@ public class GroupEntity implements Group, Entity, Auditable,
HasIdentifier {
private Long id;
private String name;
+ private String externalId;
private AuditInfo auditInfo;
private List<String> roleNames;
private List<Long> roleIds;
@@ -67,6 +71,7 @@ public class GroupEntity implements Group, Entity, Auditable,
HasIdentifier {
Map<Field, Object> fields = Maps.newHashMap();
fields.put(ID, id);
fields.put(NAME, name);
+ fields.put(EXTERNAL_ID, externalId);
fields.put(AUDIT_INFO, auditInfo);
fields.put(ROLE_NAMES, roleNames);
fields.put(ROLE_IDS, roleIds);
@@ -84,6 +89,11 @@ public class GroupEntity implements Group, Entity,
Auditable, HasIdentifier {
return name;
}
+ @Override
+ public String externalId() {
+ return externalId;
+ }
+
/**
* Returns the unique id of the group.
*
@@ -162,13 +172,14 @@ public class GroupEntity implements Group, Entity,
Auditable, HasIdentifier {
&& Objects.equals(name, that.name)
&& Objects.equals(namespace, that.namespace)
&& Objects.equals(auditInfo, that.auditInfo)
+ && Objects.equals(externalId, that.externalId)
&& CollectionUtils.isEqualCollection(roleNames, that.roleNames)
&& CollectionUtils.isEqualCollection(roleIds, that.roleIds);
}
@Override
public int hashCode() {
- return Objects.hash(id, name, auditInfo, roleNames, roleIds);
+ return Objects.hash(id, name, auditInfo, externalId, roleNames, roleIds);
}
public static Builder builder() {
@@ -204,6 +215,17 @@ public class GroupEntity implements Group, Entity,
Auditable, HasIdentifier {
return this;
}
+ /**
+ * Sets the external id of the group entity.
+ *
+ * @param externalId The external id of the group entity.
+ * @return The builder instance.
+ */
+ public Builder withExternalId(String externalId) {
+ groupEntity.externalId = externalId;
+ return this;
+ }
+
/**
* Sets the audit details of the group entity.
*
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java
b/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java
index e564146ecd..922f324cd4 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java
@@ -319,6 +319,8 @@ public class JDBCBackend implements RelationalBackend {
switch (entityType) {
case USER:
return (E) UserMetaService.getInstance().getUserByExternalId(ident);
+ case GROUP:
+ return (E) GroupMetaService.getInstance().getGroupByExternalId(ident);
default:
throw new UnsupportedEntityTypeException(
"Unsupported entity type: %s for get by external id operation",
entityType);
@@ -334,7 +336,7 @@ public class JDBCBackend implements RelationalBackend {
return (E) UserMetaService.getInstance().updateUserByExternalId(ident,
updater);
default:
throw new UnsupportedEntityTypeException(
- "Unsupported entity type: %s for update by external id operation",
entityType);
+ "Unsupported entity type: %s for update enabled by external id
operation", entityType);
}
}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaMapper.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaMapper.java
index 6b0ac15da9..c86df5a90e 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaMapper.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaMapper.java
@@ -102,4 +102,10 @@ public interface GroupMetaMapper {
@SelectProvider(type = GroupMetaSQLProviderFactory.class, method =
"getGroupUpdatedAt")
GroupUpdatedAt getGroupUpdatedAt(
@Param("metalakeName") String metalakeName, @Param("groupName") String
groupName);
+
+ @SelectProvider(
+ type = GroupMetaSQLProviderFactory.class,
+ method = "selectGroupMetaByMetalakeNameAndExternalId")
+ GroupPO selectGroupMetaByMetalakeNameAndExternalId(
+ @Param("metalakeName") String metalakeName, @Param("externalId") String
externalId);
}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaSQLProviderFactory.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaSQLProviderFactory.java
index a846b6545f..21d0d4df8e 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaSQLProviderFactory.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaSQLProviderFactory.java
@@ -110,4 +110,9 @@ public class GroupMetaSQLProviderFactory {
@Param("metalakeName") String metalakeName, @Param("groupName") String
groupName) {
return getProvider().getGroupUpdatedAt(metalakeName, groupName);
}
+
+ public static String selectGroupMetaByMetalakeNameAndExternalId(
+ @Param("metalakeName") String metalakeName, @Param("externalId") String
externalId) {
+ return
getProvider().selectGroupMetaByMetalakeNameAndExternalId(metalakeName,
externalId);
+ }
}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/GroupMetaBaseSQLProvider.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/GroupMetaBaseSQLProvider.java
index 51523d8e6f..9e8229ec23 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/GroupMetaBaseSQLProvider.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/GroupMetaBaseSQLProvider.java
@@ -39,6 +39,7 @@ public class GroupMetaBaseSQLProvider {
public String listGroupPOsByMetalake(@Param("metalakeName") String
metalakeName) {
return "SELECT gt.group_id as groupId, gt.group_name as groupName,
gt.metalake_id as metalakeId,"
+ + " gt.external_id as externalId,"
+ " gt.audit_info as auditInfo, gt.current_version as currentVersion,
gt.last_version as lastVersion,"
+ " gt.deleted_at as deletedAt FROM "
+ GROUP_TABLE_NAME
@@ -51,6 +52,7 @@ public class GroupMetaBaseSQLProvider {
public String listExtendedGroupPOsByMetalakeId(@Param("metalakeId") Long
metalakeId) {
return "SELECT gt.group_id as groupId, gt.group_name as groupName,"
+ " gt.metalake_id as metalakeId,"
+ + " gt.external_id as externalId,"
+ " gt.audit_info as auditInfo,"
+ " gt.current_version as currentVersion, gt.last_version as
lastVersion,"
+ " gt.deleted_at as deletedAt,"
@@ -78,20 +80,38 @@ public class GroupMetaBaseSQLProvider {
@Param("metalakeId") Long metalakeId, @Param("groupName") String name) {
return "SELECT group_id as groupId, group_name as groupName,"
+ " metalake_id as metalakeId,"
- + " audit_info as auditInfo,"
- + " current_version as currentVersion, last_version as lastVersion,"
- + " deleted_at as deletedAt"
+ + " external_id as externalId,"
+ + " audit_info as auditInfo, current_version as currentVersion,"
+ + " last_version as lastVersion, deleted_at as deletedAt"
+ " FROM "
+ GROUP_TABLE_NAME
+ " WHERE metalake_id = #{metalakeId} AND group_name = #{groupName}"
+ " AND deleted_at = 0";
}
+ public String selectGroupMetaByMetalakeNameAndExternalId(
+ @Param("metalakeName") String metalakeName, @Param("externalId") String
externalId) {
+ return "SELECT gt.group_id as groupId, gt.group_name as groupName,"
+ + " gt.metalake_id as metalakeId,"
+ + " gt.external_id as externalId,"
+ + " gt.audit_info as auditInfo, gt.current_version as currentVersion,"
+ + " gt.last_version as lastVersion, gt.deleted_at as deletedAt"
+ + " FROM "
+ + GROUP_TABLE_NAME
+ + " gt JOIN "
+ + MetalakeMetaMapper.TABLE_NAME
+ + " mt ON gt.metalake_id = mt.metalake_id"
+ + " WHERE mt.metalake_name = #{metalakeName}"
+ + " AND gt.external_id = #{externalId}"
+ + " AND gt.deleted_at = 0 AND mt.deleted_at = 0";
+ }
+
public String listExtendedGroupPOsByMetalakeIdAndNames(
@Param("metalakeId") Long metalakeId, @Param("groupNames") List<String>
groupNames) {
return "<script>"
+ "SELECT gt.group_id as groupId, gt.group_name as groupName,"
+ " gt.metalake_id as metalakeId,"
+ + " gt.external_id as externalId,"
+ " gt.audit_info as auditInfo,"
+ " gt.current_version as currentVersion, gt.last_version as
lastVersion,"
+ " gt.deleted_at as deletedAt,"
@@ -117,7 +137,7 @@ public class GroupMetaBaseSQLProvider {
+ "#{groupName}"
+ "</foreach>"
+ " )"
- + " GROUP BY gt.group_id, gt.group_name, gt.metalake_id,
gt.audit_info,"
+ + " GROUP BY gt.group_id, gt.group_name, gt.metalake_id,
gt.external_id, gt.audit_info,"
+ " gt.current_version, gt.last_version, gt.deleted_at"
+ "</script>";
}
@@ -125,13 +145,13 @@ public class GroupMetaBaseSQLProvider {
public String insertGroupMeta(@Param("groupMeta") GroupPO groupPO) {
return "INSERT INTO "
+ GROUP_TABLE_NAME
- + " (group_id, group_name,"
- + " metalake_id, audit_info,"
- + " current_version, last_version, deleted_at)"
+ + " (group_id, group_name, metalake_id, external_id,"
+ + " audit_info, current_version, last_version, deleted_at)"
+ " VALUES ("
+ " #{groupMeta.groupId},"
+ " #{groupMeta.groupName},"
+ " #{groupMeta.metalakeId},"
+ + " #{groupMeta.externalId},"
+ " #{groupMeta.auditInfo},"
+ " #{groupMeta.currentVersion},"
+ " #{groupMeta.lastVersion},"
@@ -142,13 +162,13 @@ public class GroupMetaBaseSQLProvider {
public String insertGroupMetaOnDuplicateKeyUpdate(@Param("groupMeta")
GroupPO groupPO) {
return "INSERT INTO "
+ GROUP_TABLE_NAME
- + " (group_id, group_name,"
- + " metalake_id, audit_info,"
- + " current_version, last_version, deleted_at)"
+ + " (group_id, group_name, metalake_id, external_id,"
+ + " audit_info, current_version, last_version, deleted_at)"
+ " VALUES ("
+ " #{groupMeta.groupId},"
+ " #{groupMeta.groupName},"
+ " #{groupMeta.metalakeId},"
+ + " #{groupMeta.externalId},"
+ " #{groupMeta.auditInfo},"
+ " #{groupMeta.currentVersion},"
+ " #{groupMeta.lastVersion},"
@@ -158,6 +178,7 @@ public class GroupMetaBaseSQLProvider {
+ " group_name = #{groupMeta.groupName},"
+ " metalake_id = #{groupMeta.metalakeId},"
+ " audit_info = #{groupMeta.auditInfo},"
+ + " external_id = #{groupMeta.externalId},"
+ " current_version = #{groupMeta.currentVersion},"
+ " last_version = #{groupMeta.lastVersion},"
+ " deleted_at = #{groupMeta.deletedAt}";
@@ -186,6 +207,7 @@ public class GroupMetaBaseSQLProvider {
+ " SET group_name = #{newGroupMeta.groupName},"
+ " metalake_id = #{newGroupMeta.metalakeId},"
+ " audit_info = #{newGroupMeta.auditInfo},"
+ + " external_id = #{newGroupMeta.externalId},"
+ " current_version = #{newGroupMeta.currentVersion},"
+ " last_version = #{newGroupMeta.lastVersion},"
+ " deleted_at = #{newGroupMeta.deletedAt}"
@@ -201,6 +223,7 @@ public class GroupMetaBaseSQLProvider {
public String listGroupsByRoleId(@Param("roleId") Long roleId) {
return "SELECT gr.group_id as groupId, gr.group_name as groupName,"
+ " gr.metalake_id as metalakeId,"
+ + " gr.external_id as externalId,"
+ " gr.audit_info as auditInfo, gr.current_version as currentVersion,"
+ " gr.last_version as lastVersion, gr.deleted_at as deletedAt"
+ " FROM "
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/h2/GroupMetaH2Provider.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/h2/GroupMetaH2Provider.java
index 2cec58f6c6..6f0920de42 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/h2/GroupMetaH2Provider.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/h2/GroupMetaH2Provider.java
@@ -31,6 +31,7 @@ public class GroupMetaH2Provider extends
GroupMetaBaseSQLProvider {
public String listExtendedGroupPOsByMetalakeId(@Param("metalakeId") Long
metalakeId) {
return "SELECT gt.group_id as groupId, gt.group_name as groupName,"
+ " gt.metalake_id as metalakeId,"
+ + " gt.external_id as externalId,"
+ " gt.audit_info as auditInfo,"
+ " gt.current_version as currentVersion, gt.last_version as
lastVersion,"
+ " gt.deleted_at as deletedAt,"
@@ -72,6 +73,7 @@ public class GroupMetaH2Provider extends
GroupMetaBaseSQLProvider {
return "<script>"
+ "SELECT gt.group_id as groupId, gt.group_name as groupName,"
+ " gt.metalake_id as metalakeId,"
+ + " gt.external_id as externalId,"
+ " gt.audit_info as auditInfo,"
+ " gt.current_version as currentVersion, gt.last_version as
lastVersion,"
+ " gt.deleted_at as deletedAt,"
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/GroupMetaPostgreSQLProvider.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/GroupMetaPostgreSQLProvider.java
index c70dee00f9..4f617f98b5 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/GroupMetaPostgreSQLProvider.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/GroupMetaPostgreSQLProvider.java
@@ -48,13 +48,14 @@ public class GroupMetaPostgreSQLProvider extends
GroupMetaBaseSQLProvider {
public String insertGroupMetaOnDuplicateKeyUpdate(GroupPO groupPO) {
return "INSERT INTO "
+ GROUP_TABLE_NAME
- + " (group_id, group_name,"
- + " metalake_id, audit_info,"
+ + " (group_id, group_name, metalake_id, external_id,"
+ + " audit_info,"
+ " current_version, last_version, deleted_at)"
+ " VALUES ("
+ " #{groupMeta.groupId},"
+ " #{groupMeta.groupName},"
+ " #{groupMeta.metalakeId},"
+ + " #{groupMeta.externalId},"
+ " #{groupMeta.auditInfo},"
+ " #{groupMeta.currentVersion},"
+ " #{groupMeta.lastVersion},"
@@ -63,6 +64,7 @@ public class GroupMetaPostgreSQLProvider extends
GroupMetaBaseSQLProvider {
+ " ON CONFLICT(group_id) DO UPDATE SET"
+ " group_name = #{groupMeta.groupName},"
+ " metalake_id = #{groupMeta.metalakeId},"
+ + " external_id = #{groupMeta.externalId},"
+ " audit_info = #{groupMeta.auditInfo},"
+ " current_version = #{groupMeta.currentVersion},"
+ " last_version = #{groupMeta.lastVersion},"
@@ -73,6 +75,7 @@ public class GroupMetaPostgreSQLProvider extends
GroupMetaBaseSQLProvider {
public String listExtendedGroupPOsByMetalakeId(@Param("metalakeId") Long
metalakeId) {
return "SELECT gt.group_id as groupId, gt.group_name as groupName,"
+ " gt.metalake_id as metalakeId,"
+ + " gt.external_id as externalId,"
+ " gt.audit_info as auditInfo,"
+ " gt.current_version as currentVersion, gt.last_version as
lastVersion,"
+ " gt.deleted_at as deletedAt,"
@@ -102,6 +105,7 @@ public class GroupMetaPostgreSQLProvider extends
GroupMetaBaseSQLProvider {
return "<script>"
+ "SELECT gt.group_id as groupId, gt.group_name as groupName,"
+ " gt.metalake_id as metalakeId,"
+ + " gt.external_id as externalId,"
+ " gt.audit_info as auditInfo,"
+ " gt.current_version as currentVersion, gt.last_version as
lastVersion,"
+ " gt.deleted_at as deletedAt,"
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/po/GroupPO.java
b/core/src/main/java/org/apache/gravitino/storage/relational/po/GroupPO.java
index 478e9e774c..6e01137164 100644
--- a/core/src/main/java/org/apache/gravitino/storage/relational/po/GroupPO.java
+++ b/core/src/main/java/org/apache/gravitino/storage/relational/po/GroupPO.java
@@ -25,6 +25,7 @@ public class GroupPO {
private Long groupId;
private String groupName;
private Long metalakeId;
+ private String externalId;
private String auditInfo;
private Long currentVersion;
private Long lastVersion;
@@ -42,6 +43,10 @@ public class GroupPO {
return metalakeId;
}
+ public String getExternalId() {
+ return externalId;
+ }
+
public String getAuditInfo() {
return auditInfo;
}
@@ -70,6 +75,7 @@ public class GroupPO {
return Objects.equal(getGroupId(), tablePO.getGroupId())
&& Objects.equal(getGroupName(), tablePO.getGroupName())
&& Objects.equal(getMetalakeId(), tablePO.getMetalakeId())
+ && Objects.equal(getExternalId(), tablePO.getExternalId())
&& Objects.equal(getAuditInfo(), tablePO.getAuditInfo())
&& Objects.equal(getCurrentVersion(), tablePO.getCurrentVersion())
&& Objects.equal(getLastVersion(), tablePO.getLastVersion())
@@ -82,6 +88,7 @@ public class GroupPO {
getGroupId(),
getGroupName(),
getMetalakeId(),
+ getExternalId(),
getAuditInfo(),
getCurrentVersion(),
getLastVersion(),
@@ -110,6 +117,11 @@ public class GroupPO {
return this;
}
+ public Builder withExternalId(String externalId) {
+ groupPO.externalId = externalId;
+ return this;
+ }
+
public Builder withAuditInfo(String auditInfo) {
groupPO.auditInfo = auditInfo;
return this;
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/service/GroupMetaService.java
b/core/src/main/java/org/apache/gravitino/storage/relational/service/GroupMetaService.java
index 7a7914f223..75c3a0f5c5 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/service/GroupMetaService.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/service/GroupMetaService.java
@@ -365,4 +365,32 @@ public class GroupMetaService {
return groupDeletedCount[0] + groupRoleRelDeletedCount[0];
}
+
+ private GroupPO getGroupPOByMetalakeNameAndExternalId(String metalakeName,
String externalId) {
+ GroupPO groupPO =
+ SessionUtils.getWithoutCommit(
+ GroupMetaMapper.class,
+ mapper ->
mapper.selectGroupMetaByMetalakeNameAndExternalId(metalakeName, externalId));
+
+ if (groupPO == null) {
+ throw new NoSuchEntityException(
+ NoSuchEntityException.NO_SUCH_ENTITY_MESSAGE,
+ Entity.EntityType.GROUP.name().toLowerCase(),
+ externalId);
+ }
+ return groupPO;
+ }
+
+ @Monitored(
+ metricsSource = GRAVITINO_RELATIONAL_STORE_METRIC_NAME,
+ baseMetricName = "getGroupByExternalId")
+ public GroupEntity getGroupByExternalId(NameIdentifier ident) {
+ AuthorizationUtils.checkGroupExternalId(ident);
+ String metalake = ident.namespace().level(0);
+ String externalId = ident.name();
+ GroupPO groupPO = getGroupPOByMetalakeNameAndExternalId(metalake,
externalId);
+ List<RolePO> rolePOs =
RoleMetaService.getInstance().listRolesByGroupId(groupPO.getGroupId());
+ return POConverters.fromGroupPO(
+ groupPO, rolePOs, AuthorizationUtils.ofGroupNamespace(metalake));
+ }
}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/utils/POConverters.java
b/core/src/main/java/org/apache/gravitino/storage/relational/utils/POConverters.java
index 48eb78dfc1..fcb05a6f46 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/utils/POConverters.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/utils/POConverters.java
@@ -1104,6 +1104,7 @@ public class POConverters {
.withId(groupPO.getGroupId())
.withName(groupPO.getGroupName())
.withNamespace(namespace)
+ .withExternalId(groupPO.getExternalId())
.withAuditInfo(
JsonUtils.anyFieldMapper().readValue(groupPO.getAuditInfo(),
AuditInfo.class));
if (!roleNames.isEmpty()) {
@@ -1132,6 +1133,7 @@ public class POConverters {
.withId(groupPO.getGroupId())
.withName(groupPO.getGroupName())
.withNamespace(namespace)
+ .withExternalId(groupPO.getExternalId())
.withAuditInfo(
JsonUtils.anyFieldMapper().readValue(groupPO.getAuditInfo(),
AuditInfo.class));
@@ -1237,6 +1239,7 @@ public class POConverters {
return builder
.withGroupId(groupEntity.id())
.withGroupName(groupEntity.name())
+ .withExternalId(groupEntity.externalId())
.withAuditInfo(JsonUtils.anyFieldMapper().writeValueAsString(groupEntity.auditInfo()))
.withCurrentVersion(INIT_VERSION)
.withLastVersion(INIT_VERSION)
@@ -1263,6 +1266,7 @@ public class POConverters {
return GroupPO.builder()
.withGroupId(oldGroupPO.getGroupId())
.withGroupName(newGroup.name())
+ .withExternalId(newGroup.externalId())
.withMetalakeId(oldGroupPO.getMetalakeId())
.withAuditInfo(JsonUtils.anyFieldMapper().writeValueAsString(newGroup.auditInfo()))
.withCurrentVersion(nextVersion)
diff --git
a/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java
b/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java
index 1763c2b8c6..10813b5e64 100644
---
a/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java
+++
b/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java
@@ -494,9 +494,14 @@ public class TestAccessControlManager {
public void testMissingExt() {
assertInvalidExt(() -> accessControlManager.getUserByExternalId(METALAKE,
null));
assertInvalidExt(() -> accessControlManager.getUserByExternalId(METALAKE,
""));
+ assertInvalidExt(() -> accessControlManager.getGroupByExternalId(METALAKE,
null));
+ assertInvalidExt(() -> accessControlManager.getGroupByExternalId(METALAKE,
""));
assertMissingExt(
NoSuchUserException.class,
() -> accessControlManager.getUserByExternalId(METALAKE,
"missing-ext-id"));
+ assertMissingExt(
+ NoSuchGroupException.class,
+ () -> accessControlManager.getGroupByExternalId(METALAKE,
"missing-ext-id"));
assertMissingExt(
NoSuchUserException.class,
() -> accessControlManager.disableUser(METALAKE, "missing-ext-id"));
@@ -512,6 +517,12 @@ public class TestAccessControlManager {
UserAlreadyExistsException.class,
() -> accessControlManager.addUser(METALAKE, "u2", "dup-ext", true));
accessControlManager.removeUser(METALAKE, "u1");
+
+ accessControlManager.addGroup(METALAKE, "g1", "dup-ext");
+ assertThrowsExt(
+ GroupAlreadyExistsException.class,
+ () -> accessControlManager.addGroup(METALAKE, "g2", "dup-ext"));
+ accessControlManager.removeGroup(METALAKE, "g1");
}
@Test
@@ -526,6 +537,20 @@ public class TestAccessControlManager {
Assertions.assertFalse(accessControlManager.removeUserByExternalId(METALAKE,
"missing-ext-id"));
}
+ @Test
+ public void testGroupExtDel() {
+ String extId = "ext-remove-group";
+ accessControlManager.addGroup(METALAKE, "remove_group", extId);
+
Assertions.assertTrue(accessControlManager.removeGroupByExternalId(METALAKE,
extId));
+ assertMissingExt(
+ NoSuchGroupException.class,
+ () -> accessControlManager.getGroupByExternalId(METALAKE, extId));
+ Assertions.assertThrows(
+ NoSuchGroupException.class, () ->
accessControlManager.getGroup(METALAKE, "remove_group"));
+ Assertions.assertFalse(
+ accessControlManager.removeGroupByExternalId(METALAKE,
"missing-ext-id"));
+ }
+
@Test
public void testExtCache() {
String extId = "ext-cache-user";
@@ -536,6 +561,17 @@ public class TestAccessControlManager {
accessControlManager.removeUser(METALAKE, "cache_user");
}
+ @Test
+ public void testGroupExtId() {
+ String group = "ext_group";
+ String extId = "ext-group-1";
+ Group added = accessControlManager.addGroup(METALAKE, group, extId);
+ Assertions.assertEquals(extId, added.externalId());
+ Assertions.assertEquals(
+ group, accessControlManager.getGroupByExternalId(METALAKE,
extId).name());
+ accessControlManager.removeGroup(METALAKE, group);
+ }
+
private void createCatalogRole(String role) {
accessControlManager.createRole(
METALAKE,
diff --git
a/core/src/test/java/org/apache/gravitino/authorization/TestAuthorizationUtils.java
b/core/src/test/java/org/apache/gravitino/authorization/TestAuthorizationUtils.java
index 5017f4211c..278ebc10c8 100644
---
a/core/src/test/java/org/apache/gravitino/authorization/TestAuthorizationUtils.java
+++
b/core/src/test/java/org/apache/gravitino/authorization/TestAuthorizationUtils.java
@@ -55,6 +55,7 @@ class TestAuthorizationUtils {
NameIdentifier group = AuthorizationUtils.ofGroup(metalake, "group");
NameIdentifier role = AuthorizationUtils.ofRole(metalake, "role");
NameIdentifier userExt = AuthorizationUtils.ofUserExternalId(metalake,
"ext-1");
+ NameIdentifier groupExt = AuthorizationUtils.ofGroupExternalId(metalake,
"ext-1");
Assertions.assertEquals(AuthorizationUtils.ofUserNamespace(metalake),
user.namespace());
Assertions.assertEquals("user", user.name());
@@ -65,10 +66,16 @@ class TestAuthorizationUtils {
Assertions.assertEquals(
AuthorizationUtils.ofUserExternalIdNamespace(metalake),
userExt.namespace());
Assertions.assertEquals("ext-1", userExt.name());
+ Assertions.assertEquals(
+ AuthorizationUtils.ofGroupExternalIdNamespace(metalake),
groupExt.namespace());
+ Assertions.assertEquals("ext-1", groupExt.name());
Assertions.assertNotEquals(user, userExt);
+ Assertions.assertNotEquals(group, groupExt);
Assertions.assertNotEquals(AuthorizationUtils.ofUser(metalake, "ext-1"),
userExt);
assertInvalidExternalId(() ->
AuthorizationUtils.ofUserExternalId(metalake, null));
assertInvalidExternalId(() ->
AuthorizationUtils.ofUserExternalId(metalake, ""));
+ assertInvalidExternalId(() ->
AuthorizationUtils.ofGroupExternalId(metalake, null));
+ assertInvalidExternalId(() ->
AuthorizationUtils.ofGroupExternalId(metalake, ""));
}
private void
assertInvalidExternalId(org.junit.jupiter.api.function.Executable executable) {
@@ -133,11 +140,13 @@ class TestAuthorizationUtils {
NameIdentifier user = AuthorizationUtils.ofUser(metalake, "user");
NameIdentifier userExternalId =
AuthorizationUtils.ofUserExternalId(metalake, "ext-1");
NameIdentifier group = AuthorizationUtils.ofGroup(metalake, "group");
+ NameIdentifier groupExternalId =
AuthorizationUtils.ofGroupExternalId(metalake, "ext-1");
NameIdentifier role = AuthorizationUtils.ofRole(metalake, "role");
Assertions.assertDoesNotThrow(() -> AuthorizationUtils.checkUser(user));
Assertions.assertDoesNotThrow(() ->
AuthorizationUtils.checkUserExternalId(userExternalId));
Assertions.assertDoesNotThrow(() -> AuthorizationUtils.checkGroup(group));
+ Assertions.assertDoesNotThrow(() ->
AuthorizationUtils.checkGroupExternalId(groupExternalId));
Assertions.assertDoesNotThrow(() -> AuthorizationUtils.checkRole(role));
Assertions.assertThrows(
@@ -146,6 +155,8 @@ class TestAuthorizationUtils {
IllegalNameIdentifierException.class, () ->
AuthorizationUtils.checkUserExternalId(null));
Assertions.assertThrows(
IllegalNameIdentifierException.class, () ->
AuthorizationUtils.checkGroup(null));
+ Assertions.assertThrows(
+ IllegalNameIdentifierException.class, () ->
AuthorizationUtils.checkGroupExternalId(null));
Assertions.assertThrows(
IllegalNameIdentifierException.class, () ->
AuthorizationUtils.checkRole(null));
Assertions.assertThrows(
@@ -157,6 +168,9 @@ class TestAuthorizationUtils {
Assertions.assertThrows(
IllegalNameIdentifierException.class,
() -> AuthorizationUtils.checkGroup(NameIdentifier.of("")));
+ Assertions.assertThrows(
+ IllegalNameIdentifierException.class,
+ () -> AuthorizationUtils.checkGroupExternalId(NameIdentifier.of("")));
Assertions.assertThrows(
IllegalNameIdentifierException.class,
() -> AuthorizationUtils.checkRole(NameIdentifier.of("")));
@@ -167,12 +181,15 @@ class TestAuthorizationUtils {
Namespace userNamespace = AuthorizationUtils.ofUserNamespace(metalake);
Namespace userExternalIdNamespace =
AuthorizationUtils.ofUserExternalIdNamespace(metalake);
Namespace groupNamespace = AuthorizationUtils.ofGroupNamespace(metalake);
+ Namespace groupExternalIdNamespace =
AuthorizationUtils.ofGroupExternalIdNamespace(metalake);
Namespace roleNamespace = AuthorizationUtils.ofRoleNamespace(metalake);
Assertions.assertDoesNotThrow(() ->
AuthorizationUtils.checkUserNamespace(userNamespace));
Assertions.assertDoesNotThrow(
() ->
AuthorizationUtils.checkUserExternalIdNamespace(userExternalIdNamespace));
Assertions.assertDoesNotThrow(() ->
AuthorizationUtils.checkGroupNamespace(groupNamespace));
+ Assertions.assertDoesNotThrow(
+ () ->
AuthorizationUtils.checkGroupExternalIdNamespace(groupExternalIdNamespace));
Assertions.assertDoesNotThrow(() ->
AuthorizationUtils.checkRoleNamespace(roleNamespace));
Assertions.assertThrows(
@@ -182,6 +199,9 @@ class TestAuthorizationUtils {
() -> AuthorizationUtils.checkUserExternalIdNamespace(null));
Assertions.assertThrows(
IllegalNamespaceException.class, () ->
AuthorizationUtils.checkGroupNamespace(null));
+ Assertions.assertThrows(
+ IllegalNamespaceException.class,
+ () -> AuthorizationUtils.checkGroupExternalIdNamespace(null));
Assertions.assertThrows(
IllegalNamespaceException.class, () ->
AuthorizationUtils.checkRoleNamespace(null));
Assertions.assertThrows(
@@ -193,6 +213,9 @@ class TestAuthorizationUtils {
Assertions.assertThrows(
IllegalNamespaceException.class,
() -> AuthorizationUtils.checkGroupNamespace(Namespace.of("a")));
+ Assertions.assertThrows(
+ IllegalNamespaceException.class,
+ () ->
AuthorizationUtils.checkGroupExternalIdNamespace(Namespace.of("a", "b")));
Assertions.assertThrows(
IllegalNamespaceException.class,
() -> AuthorizationUtils.checkRoleNamespace(Namespace.of("a", "b",
"c", "d")));
diff --git
a/core/src/test/java/org/apache/gravitino/storage/memory/TestMemoryEntityStore.java
b/core/src/test/java/org/apache/gravitino/storage/memory/TestMemoryEntityStore.java
index 8d337c32ce..a3d679acae 100644
---
a/core/src/test/java/org/apache/gravitino/storage/memory/TestMemoryEntityStore.java
+++
b/core/src/test/java/org/apache/gravitino/storage/memory/TestMemoryEntityStore.java
@@ -157,25 +157,35 @@ public class TestMemoryEntityStore {
public <E extends Entity & HasIdentifier> E getByExternalId(
NameIdentifier ident, EntityType entityType, Class<E> type)
throws NoSuchEntityException, IOException {
- if (entityType != EntityType.USER) {
+ if (entityType != EntityType.USER && entityType != EntityType.GROUP) {
throw new UnsupportedOperationException(
"Get by external id is not supported for entity type: " +
entityType);
}
- AuthorizationUtils.checkUserExternalId(ident);
- String externalId = ident.name();
- Namespace userNamespace =
AuthorizationUtils.ofUserNamespace(ident.namespace().level(0));
+ String externalId;
+ Namespace entityNamespace;
+ if (entityType == EntityType.USER) {
+ AuthorizationUtils.checkUserExternalId(ident);
+ externalId = ident.name();
+ entityNamespace =
AuthorizationUtils.ofUserNamespace(ident.namespace().level(0));
+ } else {
+ AuthorizationUtils.checkGroupExternalId(ident);
+ externalId = ident.name();
+ entityNamespace =
AuthorizationUtils.ofGroupNamespace(ident.namespace().level(0));
+ }
for (Map.Entry<NameIdentifier, Entity> entry : entityMap.entrySet()) {
Entity entity = entry.getValue();
if (!entity.type().equals(entityType)
- || !entry.getKey().namespace().equals(userNamespace)) {
+ || !entry.getKey().namespace().equals(entityNamespace)) {
continue;
}
String entityExternalId = null;
if (entity instanceof UserEntity userEntity) {
entityExternalId = userEntity.externalId();
+ } else if (entity instanceof GroupEntity groupEntity) {
+ entityExternalId = groupEntity.externalId();
}
if (externalId.equals(entityExternalId)) {
@@ -194,17 +204,24 @@ public class TestMemoryEntityStore {
public <E extends Entity & HasIdentifier> E updateByExternalId(
NameIdentifier ident, EntityType entityType, Class<E> type,
Function<E, E> updater)
throws NoSuchEntityException, IOException {
- E user = getByExternalId(ident, entityType, type);
- E updated = updater.apply(user);
- return update(user.nameIdentifier(), type, entityType, entity ->
updated);
+ E entity = getByExternalId(ident, entityType, type);
+ E updated = updater.apply(entity);
+ return update(entity.nameIdentifier(), type, entityType, e -> updated);
}
@Override
public boolean deleteByExternalId(NameIdentifier ident, EntityType
entityType)
throws IOException {
try {
- UserEntity user = getByExternalId(ident, entityType, UserEntity.class);
- return delete(user.nameIdentifier(), entityType);
+ if (entityType == EntityType.USER) {
+ UserEntity user = getByExternalId(ident, entityType,
UserEntity.class);
+ return delete(user.nameIdentifier(), entityType);
+ } else if (entityType == EntityType.GROUP) {
+ GroupEntity group = getByExternalId(ident, entityType,
GroupEntity.class);
+ return delete(group.nameIdentifier(), entityType);
+ }
+ throw new UnsupportedOperationException(
+ "Delete by external id is not supported for entity type: " +
entityType);
} catch (NoSuchEntityException e) {
return false;
}
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/h2/TestGroupMetaH2Provider.java
b/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/h2/TestGroupMetaH2Provider.java
index 3d68c7f329..dba49e9372 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/h2/TestGroupMetaH2Provider.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/h2/TestGroupMetaH2Provider.java
@@ -57,6 +57,7 @@ class TestGroupMetaH2Provider {
+ "group_id BIGINT, "
+ "group_name VARCHAR(255), "
+ "metalake_id BIGINT, "
+ + "external_id VARCHAR(256), "
+ "audit_info VARCHAR(255), "
+ "current_version BIGINT, "
+ "last_version BIGINT, "
@@ -75,7 +76,7 @@ class TestGroupMetaH2Provider {
@Test
void testListExtendedGroupPOsByMetalakeIdWithoutRoles() throws SQLException {
- statement.execute("INSERT INTO group_meta VALUES (1, 'g1', 1, 'audit', 0,
0, 0)");
+ statement.execute("INSERT INTO group_meta VALUES (1, 'g1', 1, null,
'audit', 0, 0, 0)");
String sql =
groupMetaH2Provider
.listExtendedGroupPOsByMetalakeId(1L)
@@ -89,7 +90,7 @@ class TestGroupMetaH2Provider {
@Test
void testListExtendedGroupPOsByMetalakeIdWithRoles() throws SQLException {
- statement.execute("INSERT INTO group_meta VALUES (2, 'g2', 2, 'audit2', 0,
0, 0)");
+ statement.execute("INSERT INTO group_meta VALUES (2, 'g2', 2, null,
'audit2', 0, 0, 0)");
statement.execute("INSERT INTO role_meta VALUES (1, 'role1', 0)");
statement.execute("INSERT INTO role_meta VALUES (2, 'role2', 0)");
statement.execute("INSERT INTO group_role_rel VALUES (2, 1, 0)");
@@ -108,7 +109,7 @@ class TestGroupMetaH2Provider {
@Test
void testListExtendedGroupPOsByMetalakeIdWithInvalidRoles() throws
SQLException {
- statement.execute("INSERT INTO group_meta VALUES (3, 'g3', 3, 'audit3', 0,
0, 0)");
+ statement.execute("INSERT INTO group_meta VALUES (3, 'g3', 3, null,
'audit3', 0, 0, 0)");
statement.execute("INSERT INTO role_meta VALUES (3, 'role3', 0)");
statement.execute("INSERT INTO role_meta VALUES (4, '', 0)");
statement.execute("INSERT INTO role_meta VALUES (5, null, 0)");
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestGroupMetaService.java
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestGroupMetaService.java
index 69b0344e5e..801e8dadb5 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestGroupMetaService.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestGroupMetaService.java
@@ -38,6 +38,7 @@ import java.util.Optional;
import java.util.function.Function;
import org.apache.gravitino.Entity;
import org.apache.gravitino.EntityAlreadyExistsException;
+import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.authorization.AuthorizationUtils;
import org.apache.gravitino.exceptions.NoSuchEntityException;
@@ -57,6 +58,7 @@ import org.apache.gravitino.utils.NameIdentifierUtil;
import org.apache.ibatis.session.SqlSession;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.function.Executable;
class TestGroupMetaService extends TestJDBCBackend {
@@ -1063,6 +1065,64 @@ class TestGroupMetaService extends TestJDBCBackend {
GroupMetaMapper.class, mapper ->
mapper.getGroupUpdatedAt(metalakeName, groupName));
}
+ @TestTemplate
+ void testGroupExtId() throws IOException {
+ GroupMetaService svc = groupMetaService();
+ svc.insertGroup(groupWithExtId("g1", "ext-1"), false);
+ GroupEntity found = svc.getGroupByExternalId(groupExtIdent("ext-1"));
+ Assertions.assertEquals("g1", found.name());
+ Assertions.assertEquals("ext-1", found.externalId());
+ assertThrowsExt(
+ NoSuchEntityException.class,
+ () -> svc.getGroupByExternalId(groupExtIdent("missing-ext-id")));
+ assertThrowsExt(
+ IllegalArgumentException.class, () ->
svc.getGroupByExternalId(groupExtIdent("")));
+ }
+
+ @TestTemplate
+ void testExtDup() throws IOException {
+ GroupMetaService svc = groupMetaService();
+ svc.insertGroup(groupWithExtId("g1", "ext-1"), false);
+ assertThrowsExt(
+ EntityAlreadyExistsException.class,
+ () -> svc.insertGroup(groupWithExtId("g2", "ext-1"), false));
+ }
+
+ @TestTemplate
+ void testGroupExtDel() throws IOException {
+ GroupMetaService svc = groupMetaService();
+ svc.insertGroup(groupWithExtId("g1", "ext-del-by"), false);
+ GroupEntity group = svc.getGroupByExternalId(groupExtIdent("ext-del-by"));
+ Assertions.assertEquals("g1", group.name());
+ Assertions.assertTrue(svc.deleteGroup(group.nameIdentifier()));
+ assertThrowsExt(
+ NoSuchEntityException.class, () ->
svc.getGroupByExternalId(groupExtIdent("ext-del-by")));
+ assertThrowsExt(NoSuchEntityException.class, () ->
svc.deleteGroup(group.nameIdentifier()));
+ }
+
+ private NameIdentifier groupExtIdent(String externalId) {
+ return AuthorizationUtils.ofGroupExternalId(metalakeName, externalId);
+ }
+
+ private GroupMetaService groupMetaService() throws IOException {
+ createAndInsertMakeLake(metalakeName);
+ return GroupMetaService.getInstance();
+ }
+
+ private void assertThrowsExt(Class<? extends Exception> type, Executable
executable) {
+ Assertions.assertThrows(type, executable);
+ }
+
+ private GroupEntity groupWithExtId(String name, String externalId) {
+ return GroupEntity.builder()
+ .withId(RandomIdGenerator.INSTANCE.nextId())
+ .withName(name)
+ .withNamespace(AuthorizationUtils.ofGroupNamespace(metalakeName))
+ .withExternalId(externalId)
+ .withAuditInfo(AUDIT_INFO)
+ .build();
+ }
+
private GroupEntity createGroupEntity(
Long id, Namespace namespace, String name, AuditInfo auditInfo) {
return GroupEntity.builder()