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 90c43acca3 [#11832] feat(core): Add user external ID, enable/disable,
and delete-by-external-id APIs (#11848)
90c43acca3 is described below
commit 90c43acca36f0dc1d6cde214622515facfde2bb3
Author: MaSai <[email protected]>
AuthorDate: Tue Jul 7 10:38:09 2026 +0800
[#11832] feat(core): Add user external ID, enable/disable, and
delete-by-external-id APIs (#11848)
### What changes were proposed in this pull request?
User-scoped external ID correlation, enable/disable, and
delete-by-external-id. Group external-id work is split to #11834;
pagination remains #11833.
**Persistence**
- Persist `external_id` / `enabled` in user PO, entity, SQL, and
converters
**New dispatcher APIs**
- `getUserByExternalId`
- `addUser(metalake, user, externalId, enabled)`
- `enableUser` / `disableUser` (by `externalId`; updates
`user_meta.enabled` only, keeps `user_role_rel`)
- `removeUserByExternalId`
**Store path**
- External-id lookup/update/delete via `EntityStore` →
`RelationalBackend` → `UserMetaService`
- `SupportsExternalIdOperations` with `getByExternalId`,
`updateByExternalId`, and `deleteByExternalId` (USER)
- `RelationalEntityStore` invalidates cache after enable/disable/delete
**Locking**
- Get/enable/disable/delete-by-externalId use
`AuthorizationUtils.ofUserExternalId` for per-externalId TreeLock paths
**Validation & errors**
- `checkExternalId` in `MetaService` and lock-path helpers; duplicate
externalId rejected by DB unique constraint
- Dedicated error messages for missing user by external id
**Wiring**
- `AccessControlManager`, `UserGroupManager`, hook/event dispatchers,
and `UserDTO`
Fix: #11832
### Why are the changes needed?
Downstream integrations need stable external ID lookup on users, the
ability to disable a user without removing role bindings, and
delete-by-external-id for identity synchronization workflows.
### Does this PR introduce _any_ user-facing change?
1. New `User.externalId()` and `User.enabled()` API fields
2. New user dispatcher methods listed above (no new 8090 REST endpoints)
### How was this patch tested?
**UT** (`./gradlew :core:test -PskipITs`):
- `TestAccessControlManager` — user external ID CRUD, enable/disable,
duplicate/missing externalId, cache invalidation
- `TestUserMetaService` — external ID lookup, enable/disable,
delete-by-externalId, duplicate externalId
- `TestAuthorizationUtils` — `ofUserExternalId` lock paths
- `TestMemoryEntityStore`, `TestOwnerEvent`
**IT** (`./gradlew :core:test -PskipTests -PskipDockerTests=false`):
- `TestUserMetaService` (H2 + MySQL + PostgreSQL via Testcontainers)
---------
Co-authored-by: Cursor <[email protected]>
---
.../org/apache/gravitino/authorization/User.java | 25 ++++
.../apache/gravitino/cli/commands/ListUsers.java | 10 ++
.../gravitino/dto/authorization/UserDTO.java | 55 ++++++-
.../apache/gravitino/dto/util/DTOConverters.java | 2 +
.../src/main/java/org/apache/gravitino/Entity.java | 11 ++
.../java/org/apache/gravitino/EntityStore.java | 10 ++
.../gravitino/SupportsExternalIdOperations.java | 71 +++++++++
.../authorization/AccessControlDispatcher.java | 70 +++++++++
.../authorization/AccessControlManager.java | 48 +++++++
.../authorization/AuthorizationUtils.java | 40 ++++++
.../gravitino/authorization/PermissionManager.java | 4 +
.../authorization/UserGroupExternalManager.java | 160 +++++++++++++++++++++
.../gravitino/authorization/UserGroupManager.java | 6 +-
.../hook/AccessControlHookDispatcher.java | 30 ++++
.../listener/AccessControlEventDispatcher.java | 35 +++++
.../java/org/apache/gravitino/meta/UserEntity.java | 46 +++++-
.../gravitino/storage/relational/JDBCBackend.java | 26 ++++
.../storage/relational/RelationalBackend.java | 29 ++++
.../storage/relational/RelationalEntityStore.java | 46 +++++-
.../storage/relational/mapper/UserMetaMapper.java | 10 ++
.../mapper/UserMetaSQLProviderFactory.java | 10 ++
.../provider/base/OwnerMetaBaseSQLProvider.java | 4 +
.../provider/base/UserMetaBaseSQLProvider.java | 64 +++++++--
.../mapper/provider/h2/UserMetaH2Provider.java | 1 +
.../postgresql/UserMetaPostgreSQLProvider.java | 9 +-
.../gravitino/storage/relational/po/UserPO.java | 27 ++++
.../relational/service/UserMetaService.java | 67 +++++++++
.../storage/relational/utils/POConverters.java | 8 ++
.../authorization/TestAccessControlManager.java | 111 ++++++++++++++
.../authorization/TestAuthorizationUtils.java | 28 ++++
.../listener/api/event/TestOwnerEvent.java | 3 +-
.../storage/memory/TestMemoryEntityStore.java | 65 ++++++++-
.../mapper/provider/base/TestAuthMappers.java | 1 +
.../provider/base/TestUserRoleRelMapper.java | 1 +
.../relational/service/TestUserMetaService.java | 147 +++++++++++++++++++
35 files changed, 1259 insertions(+), 21 deletions(-)
diff --git a/api/src/main/java/org/apache/gravitino/authorization/User.java
b/api/src/main/java/org/apache/gravitino/authorization/User.java
index ee1dbfa1db..a5ac53e51e 100644
--- a/api/src/main/java/org/apache/gravitino/authorization/User.java
+++ b/api/src/main/java/org/apache/gravitino/authorization/User.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;
@@ -34,6 +35,30 @@ public interface User 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 User#name() user names} may differ from upstream ids
or be unknown at sync
+ * time. External id lets integrators look up, enable/disable, and delete
users without relying on
+ * the Gravitino user name.
+ *
+ * @return The upstream external identifier, or null if not set.
+ */
+ @Nullable
+ default String externalId() {
+ return null;
+ }
+
+ /**
+ * Whether the user is enabled.
+ *
+ * @return True if the user is enabled, false otherwise.
+ */
+ default boolean enabled() {
+ return true;
+ }
+
/**
* The roles of the user. A user can have multiple roles. Every role binds
several privileges.
*
diff --git
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListUsers.java
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListUsers.java
index 39c24d500f..19cf9718be 100644
--- a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListUsers.java
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListUsers.java
@@ -73,6 +73,16 @@ public class ListUsers extends Command {
return user;
}
+ @Override
+ public String externalId() {
+ return null;
+ }
+
+ @Override
+ public boolean enabled() {
+ return true;
+ }
+
@Override
public List<String> roles() {
return null;
diff --git
a/common/src/main/java/org/apache/gravitino/dto/authorization/UserDTO.java
b/common/src/main/java/org/apache/gravitino/dto/authorization/UserDTO.java
index 35530291bc..68297c9e84 100644
--- a/common/src/main/java/org/apache/gravitino/dto/authorization/UserDTO.java
+++ b/common/src/main/java/org/apache/gravitino/dto/authorization/UserDTO.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.User;
@@ -33,6 +34,13 @@ public class UserDTO implements User {
@JsonProperty("name")
private String name;
+ @Nullable
+ @JsonProperty("externalId")
+ private String externalId;
+
+ @JsonProperty("enabled")
+ private boolean enabled = true;
+
@JsonProperty("audit")
private AuditDTO audit;
@@ -46,11 +54,16 @@ public class UserDTO implements User {
* Creates a new instance of UserDTO.
*
* @param name The name of the User DTO.
+ * @param externalId The external id of the User DTO.
* @param roles The roles of the User DTO.
* @param audit The audit information of the User DTO.
+ * @param enabled Whether the User DTO is enabled.
*/
- protected UserDTO(String name, List<String> roles, AuditDTO audit) {
+ protected UserDTO(
+ String name, String externalId, List<String> roles, AuditDTO audit,
boolean enabled) {
this.name = name;
+ this.externalId = externalId;
+ this.enabled = enabled;
this.audit = audit;
this.roles = roles;
}
@@ -63,6 +76,16 @@ public class UserDTO implements User {
return name;
}
+ @Override
+ public String externalId() {
+ return externalId;
+ }
+
+ @Override
+ public boolean enabled() {
+ return enabled;
+ }
+
/**
* The roles of the user. A user can have multiple roles. Every role binds
several privileges.
*
@@ -100,6 +123,12 @@ public class UserDTO implements User {
/** The name of the user. */
protected String name;
+ /** The external id of the user. */
+ protected String externalId;
+
+ /** Whether the user is enabled. */
+ protected boolean enabled = true;
+
/** The roles of the user. */
protected List<String> roles = Collections.emptyList();
@@ -117,6 +146,28 @@ public class UserDTO implements User {
return (S) this;
}
+ /**
+ * Sets the external id of the user.
+ *
+ * @param externalId The external id of the user.
+ * @return The builder instance.
+ */
+ public S withExternalId(String externalId) {
+ this.externalId = externalId;
+ return (S) this;
+ }
+
+ /**
+ * Sets whether the user is enabled.
+ *
+ * @param enabled Whether the user is enabled.
+ * @return The builder instance.
+ */
+ public S withEnabled(boolean enabled) {
+ this.enabled = enabled;
+ return (S) this;
+ }
+
/**
* Sets the roles of the user.
*
@@ -151,7 +202,7 @@ public class UserDTO implements User {
public UserDTO build() {
Preconditions.checkArgument(StringUtils.isNotBlank(name), "name cannot
be null or empty");
Preconditions.checkArgument(audit != null, "audit cannot be null");
- return new UserDTO(name, roles, audit);
+ return new UserDTO(name, externalId, roles, audit, enabled);
}
}
}
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 e9ee2226dd..e5843aa689 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
@@ -494,6 +494,8 @@ public class DTOConverters {
return UserDTO.builder()
.withName(user.name())
+ .withExternalId(user.externalId())
+ .withEnabled(user.enabled())
.withRoles(user.roles())
.withAudit(toDTO(user.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 3447b225b3..555bd2b3a3 100644
--- a/core/src/main/java/org/apache/gravitino/Entity.java
+++ b/core/src/main/java/org/apache/gravitino/Entity.java
@@ -41,6 +41,17 @@ public interface Entity extends Serializable {
/** The user schema name in the system catalog. */
String USER_SCHEMA_NAME = "user";
+ /**
+ * A virtual schema name used only for {@link
org.apache.gravitino.lock.TreeLockUtils} lock paths
+ * when operating on users by external id (for example,
get/enable/disable/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, user-external-id,
+ * <externalId>}} so that concurrent operations on the same external id are
serialized without
+ * requiring the Gravitino user name.
+ */
+ String USER_EXTERNAL_ID_SCHEMA_NAME = "user-external-id";
+
/** The group schema name in the system catalog. */
String GROUP_SCHEMA_NAME = "group";
diff --git a/core/src/main/java/org/apache/gravitino/EntityStore.java
b/core/src/main/java/org/apache/gravitino/EntityStore.java
index d2c19c65c6..cadf3b2615 100644
--- a/core/src/main/java/org/apache/gravitino/EntityStore.java
+++ b/core/src/main/java/org/apache/gravitino/EntityStore.java
@@ -268,4 +268,14 @@ public interface EntityStore extends Closeable {
default SupportsRelationOperations relationOperations() {
throw new UnsupportedOperationException("relation operations are not
supported");
}
+
+ /**
+ * Get the extra external id operations that are supported by the entity
store.
+ *
+ * @return the external id operations that are supported by the entity store
+ * @throws UnsupportedOperationException if the extra operations are not
supported
+ */
+ default SupportsExternalIdOperations externalIdOperations() {
+ throw new UnsupportedOperationException("external id operations are not
supported");
+ }
}
diff --git
a/core/src/main/java/org/apache/gravitino/SupportsExternalIdOperations.java
b/core/src/main/java/org/apache/gravitino/SupportsExternalIdOperations.java
new file mode 100644
index 0000000000..520da24143
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/SupportsExternalIdOperations.java
@@ -0,0 +1,71 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.util.function.Function;
+import org.apache.gravitino.exceptions.NoSuchEntityException;
+
+/**
+ * Optional extension for entity stores that support lookup and mutation by
external id within a
+ * namespace.
+ */
+public interface SupportsExternalIdOperations {
+
+ /**
+ * Get the entity from the underlying storage by external id.
+ *
+ * @param ident the external id name identifier
+ * @param entityType the general type of the entity
+ * @param type the detailed type of the entity
+ * @param <E> the class of entity
+ * @return the entity retrieved from the underlying storage
+ * @throws NoSuchEntityException if the entity does not exist
+ * @throws IOException if the retrieve operation fails
+ */
+ <E extends Entity & HasIdentifier> E getByExternalId(
+ NameIdentifier ident, Entity.EntityType entityType, Class<E> type)
+ throws NoSuchEntityException, IOException;
+
+ /**
+ * Update an entity by external id.
+ *
+ * @param ident the external id name identifier
+ * @param entityType the general type of the entity
+ * @param type the detailed type of the entity
+ * @param updater the updater function to update the entity
+ * @param <E> the class of entity
+ * @return the updated entity
+ * @throws NoSuchEntityException if the entity does not exist
+ * @throws IOException if the update operation fails
+ */
+ <E extends Entity & HasIdentifier> E updateByExternalId(
+ NameIdentifier ident, Entity.EntityType entityType, Class<E> type,
Function<E, E> updater)
+ throws NoSuchEntityException, IOException;
+
+ /**
+ * Delete an entity by external id.
+ *
+ * @param ident the external id name identifier
+ * @param entityType the general type of the entity
+ * @return true if the entity exists and is deleted successfully, false
otherwise
+ * @throws IOException if the delete operation fails
+ */
+ boolean deleteByExternalId(NameIdentifier ident, Entity.EntityType
entityType) throws IOException;
+}
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 49771a1f13..258f20a6af 100644
---
a/core/src/main/java/org/apache/gravitino/authorization/AccessControlDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/authorization/AccessControlDispatcher.java
@@ -51,6 +51,21 @@ public interface AccessControlDispatcher {
User addUser(String metalake, String user)
throws UserAlreadyExistsException, NoSuchMetalakeException;
+ /**
+ * Adds a new User.
+ *
+ * @param metalake The Metalake of the User.
+ * @param user The name of the User.
+ * @param externalId The external identifier, or null if unset.
+ * @param enabled Whether the User is enabled.
+ * @return The added User instance.
+ * @throws UserAlreadyExistsException If a User 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 User encounters storage issues.
+ */
+ User addUser(String metalake, String user, String externalId, boolean
enabled)
+ throws UserAlreadyExistsException, NoSuchMetalakeException;
+
/**
* Removes a User.
*
@@ -63,6 +78,19 @@ public interface AccessControlDispatcher {
*/
boolean removeUser(String metalake, String user) throws
NoSuchMetalakeException;
+ /**
+ * Removes a User by external identifier.
+ *
+ * @param metalake The Metalake of the User.
+ * @param externalId The external identifier of the User.
+ * @return True if the User was successfully removed, false only when
there's no such user,
+ * 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 User encounters storage issues.
+ */
+ boolean removeUserByExternalId(String metalake, String externalId) throws
NoSuchMetalakeException;
+
/**
* Gets a User.
*
@@ -75,6 +103,48 @@ public interface AccessControlDispatcher {
*/
User getUser(String metalake, String user) throws NoSuchUserException,
NoSuchMetalakeException;
+ /**
+ * Gets a User by external identifier.
+ *
+ * @param metalake The Metalake of the User.
+ * @param externalId The external identifier of the User.
+ * @return The getting User instance.
+ * @throws IllegalArgumentException If externalId is null or blank.
+ * @throws NoSuchUserException If the User 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 User encounters storage issues.
+ */
+ User getUserByExternalId(String metalake, String externalId)
+ throws NoSuchUserException, NoSuchMetalakeException;
+
+ /**
+ * Enables a User without removing role bindings.
+ *
+ * @param metalake The Metalake of the User.
+ * @param externalId The external identifier of the User.
+ * @return The updated User instance.
+ * @throws IllegalArgumentException If externalId is null or blank.
+ * @throws NoSuchUserException If the User with the given external id does
not exist.
+ * @throws NoSuchMetalakeException If the Metalake with the given name does
not exist.
+ * @throws RuntimeException If updating the User encounters storage issues.
+ */
+ User enableUser(String metalake, String externalId)
+ throws NoSuchUserException, NoSuchMetalakeException;
+
+ /**
+ * Disables a User without removing role bindings.
+ *
+ * @param metalake The Metalake of the User.
+ * @param externalId The external identifier of the User.
+ * @return The updated User instance.
+ * @throws IllegalArgumentException If externalId is null or blank.
+ * @throws NoSuchUserException If the User with the given external id does
not exist.
+ * @throws NoSuchMetalakeException If the Metalake with the given name does
not exist.
+ * @throws RuntimeException If updating the User encounters storage issues.
+ */
+ User disableUser(String metalake, String externalId)
+ throws NoSuchUserException, NoSuchMetalakeException;
+
/**
* Lists the users.
*
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 762044178a..25e40e18f9 100644
---
a/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java
+++
b/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java
@@ -47,6 +47,7 @@ import org.apache.gravitino.utils.MetadataObjectUtil;
public class AccessControlManager implements AccessControlDispatcher {
private final UserGroupManager userGroupManager;
+ private final UserGroupExternalManager userGroupExternalManager;
private final RoleManager roleManager;
private final PermissionManager permissionManager;
private final List<String> serviceAdmins;
@@ -54,6 +55,7 @@ public class AccessControlManager implements
AccessControlDispatcher {
public AccessControlManager(EntityStore store, IdGenerator idGenerator,
Config config) {
this.roleManager = new RoleManager(store, idGenerator);
this.userGroupManager = new UserGroupManager(store, idGenerator);
+ this.userGroupExternalManager = new UserGroupExternalManager(store,
idGenerator);
this.permissionManager = new PermissionManager(store, roleManager);
this.serviceAdmins = config.get(Configs.SERVICE_ADMINS);
}
@@ -67,6 +69,15 @@ public class AccessControlManager implements
AccessControlDispatcher {
() -> userGroupManager.addUser(metalake, user));
}
+ @Override
+ public User addUser(String metalake, String user, String externalId, boolean
enabled)
+ throws UserAlreadyExistsException, NoSuchMetalakeException {
+ return TreeLockUtils.doWithTreeLock(
+
NameIdentifier.of(AuthorizationUtils.ofUserNamespace(metalake).levels()),
+ LockType.WRITE,
+ () -> userGroupExternalManager.addUser(metalake, user, externalId,
enabled));
+ }
+
@Override
public boolean removeUser(String metalake, String user) throws
NoSuchMetalakeException {
return TreeLockUtils.doWithTreeLock(
@@ -75,6 +86,15 @@ public class AccessControlManager implements
AccessControlDispatcher {
() -> userGroupManager.removeUser(metalake, user));
}
+ @Override
+ public boolean removeUserByExternalId(String metalake, String externalId)
+ throws NoSuchMetalakeException {
+ return TreeLockUtils.doWithTreeLock(
+ AuthorizationUtils.ofUserExternalId(metalake, externalId),
+ LockType.WRITE,
+ () -> userGroupExternalManager.removeUserByExternalId(metalake,
externalId));
+ }
+
@Override
public User getUser(String metalake, String user)
throws NoSuchUserException, NoSuchMetalakeException {
@@ -84,6 +104,33 @@ public class AccessControlManager implements
AccessControlDispatcher {
() -> userGroupManager.getUser(metalake, user));
}
+ @Override
+ public User getUserByExternalId(String metalake, String externalId)
+ throws NoSuchUserException, NoSuchMetalakeException {
+ return TreeLockUtils.doWithTreeLock(
+ AuthorizationUtils.ofUserExternalId(metalake, externalId),
+ LockType.READ,
+ () -> userGroupExternalManager.getUserByExternalId(metalake,
externalId));
+ }
+
+ @Override
+ public User enableUser(String metalake, String externalId)
+ throws NoSuchUserException, NoSuchMetalakeException {
+ return TreeLockUtils.doWithTreeLock(
+ AuthorizationUtils.ofUserExternalId(metalake, externalId),
+ LockType.WRITE,
+ () -> userGroupExternalManager.enableUser(metalake, externalId));
+ }
+
+ @Override
+ public User disableUser(String metalake, String externalId)
+ throws NoSuchUserException, NoSuchMetalakeException {
+ return TreeLockUtils.doWithTreeLock(
+ AuthorizationUtils.ofUserExternalId(metalake, externalId),
+ LockType.WRITE,
+ () -> userGroupExternalManager.disableUser(metalake, externalId));
+ }
+
@Override
public String[] listUserNames(String metalake) throws
NoSuchMetalakeException {
return TreeLockUtils.doWithTreeLock(
@@ -100,6 +147,7 @@ public class AccessControlManager implements
AccessControlDispatcher {
() -> userGroupManager.listUsers(metalake));
}
+ @Override
public Group addGroup(String metalake, String group)
throws GroupAlreadyExistsException, 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 7ac6008c1d..d768df1743 100644
---
a/core/src/main/java/org/apache/gravitino/authorization/AuthorizationUtils.java
+++
b/core/src/main/java/org/apache/gravitino/authorization/AuthorizationUtils.java
@@ -64,6 +64,8 @@ public class AuthorizationUtils {
private static final String FILESET_SCHEMA_LOCATION = "location";
private static final String HIVE_LOCATION = "location";
static final String USER_DOES_NOT_EXIST_MSG = "User %s does not exist in the
metalake %s";
+ 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 ROLE_DOES_NOT_EXIST_MSG = "Role %s does not exist in the
metalake %s";
@@ -145,6 +147,22 @@ public class AuthorizationUtils {
metalake, Entity.SYSTEM_CATALOG_RESERVED_NAME,
Entity.USER_SCHEMA_NAME, user);
}
+ /**
+ * Creates a synthetic {@link NameIdentifier} used only as a {@link
+ * org.apache.gravitino.lock.TreeLockUtils} lock path for user operations
keyed by external id.
+ *
+ * @param metalake the metalake name
+ * @param externalId the external id of the user
+ * @return a synthetic name identifier for tree locking only
+ */
+ public static NameIdentifier ofUserExternalId(String metalake, String
externalId) {
+ return NameIdentifier.of(
+ metalake,
+ Entity.SYSTEM_CATALOG_RESERVED_NAME,
+ Entity.USER_EXTERNAL_ID_SCHEMA_NAME,
+ externalId);
+ }
+
public static Namespace ofRoleNamespace(String metalake) {
return Namespace.of(metalake, Entity.SYSTEM_CATALOG_RESERVED_NAME,
Entity.ROLE_SCHEMA_NAME);
}
@@ -157,11 +175,26 @@ public class AuthorizationUtils {
return Namespace.of(metalake, Entity.SYSTEM_CATALOG_RESERVED_NAME,
Entity.USER_SCHEMA_NAME);
}
+ public static Namespace ofUserExternalIdNamespace(String metalake) {
+ return Namespace.of(
+ metalake, Entity.SYSTEM_CATALOG_RESERVED_NAME,
Entity.USER_EXTERNAL_ID_SCHEMA_NAME);
+ }
+
public static void checkUser(NameIdentifier ident) {
NameIdentifier.check(ident != null, "User identifier must not be null");
checkUserNamespace(ident.namespace());
}
+ /**
+ * Validates that the name identifier refers to a user external id in a
metalake.
+ *
+ * @param ident the external id name identifier to validate
+ */
+ public static void checkUserExternalId(NameIdentifier ident) {
+ NameIdentifier.check(ident != null, "External id identifier must not be
null");
+ checkUserExternalIdNamespace(ident.namespace());
+ }
+
public static void checkGroup(NameIdentifier ident) {
NameIdentifier.check(ident != null, "Group identifier must not be null");
checkGroupNamespace(ident.namespace());
@@ -179,6 +212,13 @@ public class AuthorizationUtils {
namespace);
}
+ public static void checkUserExternalIdNamespace(Namespace namespace) {
+ Namespace.check(
+ namespace != null && namespace.length() == 3,
+ "User 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 fadb3463bc..c864d69051 100644
---
a/core/src/main/java/org/apache/gravitino/authorization/PermissionManager.java
+++
b/core/src/main/java/org/apache/gravitino/authorization/PermissionManager.java
@@ -118,6 +118,8 @@ class PermissionManager {
.withNamespace(userEntity.namespace())
.withId(userEntity.id())
.withName(userEntity.name())
+ .withExternalId(userEntity.externalId())
+ .withEnabled(userEntity.enabled())
.withRoleNames(roleNames)
.withRoleIds(roleIds)
.withAuditInfo(auditInfo)
@@ -384,6 +386,8 @@ class PermissionManager {
.withId(userEntity.id())
.withNamespace(userEntity.namespace())
.withName(userEntity.name())
+ .withExternalId(userEntity.externalId())
+ .withEnabled(userEntity.enabled())
.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
new file mode 100644
index 0000000000..fe850a66f8
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/authorization/UserGroupExternalManager.java
@@ -0,0 +1,160 @@
+/*
+ * 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.authorization;
+
+import com.google.common.collect.Lists;
+import java.io.IOException;
+import java.time.Instant;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.EntityAlreadyExistsException;
+import org.apache.gravitino.EntityStore;
+import org.apache.gravitino.exceptions.NoSuchEntityException;
+import org.apache.gravitino.exceptions.NoSuchUserException;
+import org.apache.gravitino.exceptions.UserAlreadyExistsException;
+import org.apache.gravitino.meta.AuditInfo;
+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. */
+class UserGroupExternalManager extends UserGroupManager {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(UserGroupExternalManager.class);
+
+ /**
+ * Creates a {@link UserGroupExternalManager} instance.
+ *
+ * @param store the entity store
+ * @param idGenerator the id generator
+ */
+ UserGroupExternalManager(EntityStore store, IdGenerator idGenerator) {
+ super(store, idGenerator);
+ }
+
+ User addUser(String metalake, String name, String externalId, boolean
enabled)
+ throws UserAlreadyExistsException {
+ try {
+ UserEntity userEntity =
+ UserEntity.builder()
+ .withId(idGenerator.nextId())
+ .withName(name)
+ .withNamespace(AuthorizationUtils.ofUserNamespace(metalake))
+ .withRoleNames(Lists.newArrayList())
+ .withEnabled(enabled)
+ .withExternalId(externalId)
+ .withAuditInfo(
+ AuditInfo.builder()
+
.withCreator(PrincipalUtils.getCurrentPrincipal().getName())
+ .withCreateTime(Instant.now())
+ .build())
+ .build();
+ store.put(userEntity, false /* overwritten */);
+ return userEntity;
+ } catch (EntityAlreadyExistsException e) {
+ LOG.warn("User {} in the metalake {} already exists", name, metalake, e);
+ throw new UserAlreadyExistsException(
+ "User %s in the metalake %s already exists", name, metalake);
+ } catch (IOException ioe) {
+ LOG.error(
+ "Adding user {} failed in the metalake {} due to storage issues",
name, metalake, ioe);
+ throw new RuntimeException(ioe);
+ }
+ }
+
+ boolean removeUserByExternalId(String metalake, String externalId) {
+ try {
+ return store
+ .externalIdOperations()
+ .deleteByExternalId(
+ AuthorizationUtils.ofUserExternalId(metalake, externalId),
Entity.EntityType.USER);
+ } catch (IOException ioe) {
+ LOG.error(
+ "Removing user with external id {} in the metalake {} failed due to
storage issues",
+ externalId,
+ metalake,
+ ioe);
+ throw new RuntimeException(ioe);
+ }
+ }
+
+ User getUserByExternalId(String metalake, String externalId) throws
NoSuchUserException {
+ try {
+ return store
+ .externalIdOperations()
+ .getByExternalId(
+ AuthorizationUtils.ofUserExternalId(metalake, externalId),
+ Entity.EntityType.USER,
+ UserEntity.class);
+ } catch (NoSuchEntityException e) {
+ LOG.warn(
+ "User with external id {} does not exist in the metalake {}",
externalId, metalake, e);
+ throw new NoSuchUserException(
+ AuthorizationUtils.USER_WITH_EXTERNAL_ID_DOES_NOT_EXIST_MSG,
externalId, metalake);
+ } catch (IOException ioe) {
+ LOG.error("Getting user with external id {} failed due to storage
issues", externalId, ioe);
+ throw new RuntimeException(ioe);
+ }
+ }
+
+ User enableUser(String metalake, String externalId) throws
NoSuchUserException {
+ return updateEnabledByExternalId(metalake, externalId, true);
+ }
+
+ User disableUser(String metalake, String externalId) throws
NoSuchUserException {
+ return updateEnabledByExternalId(metalake, externalId, false);
+ }
+
+ private User updateEnabledByExternalId(String metalake, String externalId,
boolean enabled)
+ throws NoSuchUserException {
+ try {
+ return store
+ .externalIdOperations()
+ .updateByExternalId(
+ AuthorizationUtils.ofUserExternalId(metalake, externalId),
+ Entity.EntityType.USER,
+ UserEntity.class,
+ user ->
+ UserEntity.builder()
+ .withId(user.id())
+ .withName(user.name())
+ .withNamespace(user.namespace())
+ .withExternalId(user.externalId())
+ .withEnabled(enabled)
+ .withRoleNames(user.roleNames())
+ .withRoleIds(user.roleIds())
+ .withAuditInfo(user.auditInfo())
+ .build());
+ } catch (NoSuchEntityException e) {
+ LOG.warn(
+ "User with external id {} does not exist in the metalake {}",
externalId, metalake, e);
+ throw new NoSuchUserException(
+ AuthorizationUtils.USER_WITH_EXTERNAL_ID_DOES_NOT_EXIST_MSG,
externalId, metalake);
+ } catch (IOException ioe) {
+ LOG.error(
+ "Updating enabled state for user with external id {} in the metalake
{} failed due to"
+ + " storage issues",
+ externalId,
+ metalake,
+ ioe);
+ throw new RuntimeException(ioe);
+ }
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/authorization/UserGroupManager.java
b/core/src/main/java/org/apache/gravitino/authorization/UserGroupManager.java
index 9a244212e1..d6dc2b0636 100644
---
a/core/src/main/java/org/apache/gravitino/authorization/UserGroupManager.java
+++
b/core/src/main/java/org/apache/gravitino/authorization/UserGroupManager.java
@@ -52,10 +52,10 @@ class UserGroupManager {
private static final Logger LOG =
LoggerFactory.getLogger(UserGroupManager.class);
private static final String METALAKE_DOES_NOT_EXIST_MSG = "Metalake %s does
not exist";
- private final EntityStore store;
- private final IdGenerator idGenerator;
+ protected final EntityStore store;
+ protected final IdGenerator idGenerator;
- UserGroupManager(EntityStore store, IdGenerator idGenerator) {
+ protected UserGroupManager(EntityStore store, IdGenerator idGenerator) {
this.store = store;
this.idGenerator = idGenerator;
}
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 a4b2e28007..9c7bb5aba1 100644
---
a/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java
@@ -69,17 +69,47 @@ public class AccessControlHookDispatcher implements
AccessControlDispatcher {
return dispatcher.addUser(metalake, user);
}
+ @Override
+ public User addUser(String metalake, String user, String externalId, boolean
enabled)
+ throws UserAlreadyExistsException, NoSuchMetalakeException {
+ return dispatcher.addUser(metalake, user, externalId, enabled);
+ }
+
@Override
public boolean removeUser(String metalake, String user) throws
NoSuchMetalakeException {
return dispatcher.removeUser(metalake, user);
}
+ @Override
+ public boolean removeUserByExternalId(String metalake, String externalId)
+ throws NoSuchMetalakeException {
+ return dispatcher.removeUserByExternalId(metalake, externalId);
+ }
+
@Override
public User getUser(String metalake, String user)
throws NoSuchUserException, NoSuchMetalakeException {
return dispatcher.getUser(metalake, user);
}
+ @Override
+ public User getUserByExternalId(String metalake, String externalId)
+ throws NoSuchUserException, NoSuchMetalakeException {
+ return dispatcher.getUserByExternalId(metalake, externalId);
+ }
+
+ @Override
+ public User enableUser(String metalake, String externalId)
+ throws NoSuchUserException, NoSuchMetalakeException {
+ return dispatcher.enableUser(metalake, externalId);
+ }
+
+ @Override
+ public User disableUser(String metalake, String externalId)
+ throws NoSuchUserException, NoSuchMetalakeException {
+ return dispatcher.disableUser(metalake, externalId);
+ }
+
@Override
public User[] listUsers(String metalake) throws NoSuchMetalakeException {
return dispatcher.listUsers(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 343c7128bb..db12c51d8b 100644
---
a/core/src/main/java/org/apache/gravitino/listener/AccessControlEventDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/listener/AccessControlEventDispatcher.java
@@ -145,6 +145,13 @@ public class AccessControlEventDispatcher implements
AccessControlDispatcher {
}
}
+ /** {@inheritDoc} */
+ @Override
+ public User addUser(String metalake, String user, String externalId, boolean
enabled)
+ throws UserAlreadyExistsException, NoSuchMetalakeException {
+ return dispatcher.addUser(metalake, user, externalId, enabled);
+ }
+
/** {@inheritDoc} */
@Override
public boolean removeUser(String metalake, String user) throws
NoSuchMetalakeException {
@@ -162,6 +169,13 @@ public class AccessControlEventDispatcher implements
AccessControlDispatcher {
}
}
+ /** {@inheritDoc} */
+ @Override
+ public boolean removeUserByExternalId(String metalake, String externalId)
+ throws NoSuchMetalakeException {
+ return dispatcher.removeUserByExternalId(metalake, externalId);
+ }
+
/** {@inheritDoc} */
@Override
public User getUser(String metalake, String user)
@@ -180,6 +194,27 @@ public class AccessControlEventDispatcher implements
AccessControlDispatcher {
}
}
+ /** {@inheritDoc} */
+ @Override
+ public User getUserByExternalId(String metalake, String externalId)
+ throws NoSuchUserException, NoSuchMetalakeException {
+ return dispatcher.getUserByExternalId(metalake, externalId);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public User enableUser(String metalake, String externalId)
+ throws NoSuchUserException, NoSuchMetalakeException {
+ return dispatcher.enableUser(metalake, externalId);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public User disableUser(String metalake, String externalId)
+ throws NoSuchUserException, NoSuchMetalakeException {
+ return dispatcher.disableUser(metalake, externalId);
+ }
+
/** {@inheritDoc} */
@Override
public User[] listUsers(String metalake) throws NoSuchMetalakeException {
diff --git a/core/src/main/java/org/apache/gravitino/meta/UserEntity.java
b/core/src/main/java/org/apache/gravitino/meta/UserEntity.java
index c9d7089f85..579cbd2ee7 100644
--- a/core/src/main/java/org/apache/gravitino/meta/UserEntity.java
+++ b/core/src/main/java/org/apache/gravitino/meta/UserEntity.java
@@ -42,6 +42,12 @@ public class UserEntity implements User, Entity, Auditable,
HasIdentifier {
public static final Field NAME =
Field.required("name", String.class, "The name of the user entity.");
+ public static final Field EXTERNAL_ID =
+ Field.optional("external_id", String.class, "The external id of the user
entity.");
+
+ public static final Field ENABLED =
+ Field.required("enabled", Boolean.class, "Whether the user entity is
enabled.");
+
public static final Field AUDIT_INFO =
Field.required("audit_info", AuditInfo.class, "The audit details of the
user entity.");
@@ -53,6 +59,8 @@ public class UserEntity implements User, Entity, Auditable,
HasIdentifier {
private Long id;
private String name;
+ private String externalId;
+ private boolean enabled = true;
private AuditInfo auditInfo;
private List<String> roleNames;
private List<Long> roleIds;
@@ -70,6 +78,8 @@ public class UserEntity implements User, Entity, Auditable,
HasIdentifier {
Map<Field, Object> fields = Maps.newHashMap();
fields.put(ID, id);
fields.put(NAME, name);
+ fields.put(EXTERNAL_ID, externalId);
+ fields.put(ENABLED, enabled);
fields.put(AUDIT_INFO, auditInfo);
fields.put(ROLE_NAMES, roleNames);
fields.put(ROLE_IDS, roleIds);
@@ -87,6 +97,16 @@ public class UserEntity implements User, Entity, Auditable,
HasIdentifier {
return name;
}
+ @Override
+ public String externalId() {
+ return externalId;
+ }
+
+ @Override
+ public boolean enabled() {
+ return enabled;
+ }
+
/**
* Returns the unique id of the user.
*
@@ -165,13 +185,15 @@ public class UserEntity implements User, Entity,
Auditable, HasIdentifier {
&& Objects.equals(name, that.name)
&& Objects.equals(namespace, that.namespace)
&& Objects.equals(auditInfo, that.auditInfo)
+ && Objects.equals(externalId, that.externalId)
+ && enabled == that.enabled
&& 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, enabled, roleNames,
roleIds);
}
public static Builder builder() {
@@ -207,6 +229,28 @@ public class UserEntity implements User, Entity,
Auditable, HasIdentifier {
return this;
}
+ /**
+ * Sets the external id of the user entity.
+ *
+ * @param externalId The external id of the user entity.
+ * @return The builder instance.
+ */
+ public Builder withExternalId(String externalId) {
+ userEntity.externalId = externalId;
+ return this;
+ }
+
+ /**
+ * Sets whether the user entity is enabled.
+ *
+ * @param enabled Whether the user entity is enabled.
+ * @return The builder instance.
+ */
+ public Builder withEnabled(boolean enabled) {
+ userEntity.enabled = enabled;
+ return this;
+ }
+
/**
* Sets the audit details of the user 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 f401cd50be..e564146ecd 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
@@ -312,6 +312,32 @@ public class JDBCBackend implements RelationalBackend {
}
}
+ @Override
+ public <E extends Entity & HasIdentifier> E getByExternalId(
+ NameIdentifier ident, Entity.EntityType entityType)
+ throws NoSuchEntityException, IOException {
+ switch (entityType) {
+ case USER:
+ return (E) UserMetaService.getInstance().getUserByExternalId(ident);
+ default:
+ throw new UnsupportedEntityTypeException(
+ "Unsupported entity type: %s for get by external id operation",
entityType);
+ }
+ }
+
+ @Override
+ public <E extends Entity & HasIdentifier> E updateByExternalId(
+ NameIdentifier ident, Entity.EntityType entityType, Function<E, E>
updater)
+ throws NoSuchEntityException, IOException {
+ switch (entityType) {
+ case USER:
+ return (E) UserMetaService.getInstance().updateUserByExternalId(ident,
updater);
+ default:
+ throw new UnsupportedEntityTypeException(
+ "Unsupported entity type: %s for update by external id operation",
entityType);
+ }
+ }
+
@Override
public <E extends Entity & HasIdentifier> List<E> batchGet(
List<NameIdentifier> identifiers, Entity.EntityType entityType) {
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/RelationalBackend.java
b/core/src/main/java/org/apache/gravitino/storage/relational/RelationalBackend.java
index 61eb9c435b..cffdd5e278 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/RelationalBackend.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/RelationalBackend.java
@@ -113,6 +113,35 @@ public interface RelationalBackend extends Closeable,
SupportsRelationOperations
<E extends Entity & HasIdentifier> E get(NameIdentifier ident,
Entity.EntityType entityType)
throws IOException;
+ /**
+ * Retrieves the entity associated with the external id name identifier.
+ *
+ * @param <E> The type of the entity returned.
+ * @param ident The external id name identifier.
+ * @param entityType The type of the entity.
+ * @return The entity associated with the external id name identifier.
+ * @throws NoSuchEntityException If the entity does not exist.
+ * @throws IOException If an I/O exception occurs during retrieval.
+ */
+ <E extends Entity & HasIdentifier> E getByExternalId(
+ NameIdentifier ident, Entity.EntityType entityType) throws
NoSuchEntityException, IOException;
+
+ /**
+ * Updates an entity by external id.
+ *
+ * @param <E> the type of the entity returned
+ * @param ident the external id name identifier
+ * @param entityType the type of the entity
+ * @param updater a {@link Function} that takes the current entity instance
and returns the
+ * updated instance
+ * @return the updated entity
+ * @throws NoSuchEntityException if the entity does not exist
+ * @throws IOException if the update operation fails
+ */
+ <E extends Entity & HasIdentifier> E updateByExternalId(
+ NameIdentifier ident, Entity.EntityType entityType, Function<E, E>
updater)
+ throws NoSuchEntityException, IOException;
+
/**
* Batch retrieves the entities associated with the identifiers and the
entity type.
*
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java
b/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java
index 6f21491173..a45d4b7d96 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java
@@ -40,6 +40,7 @@ import org.apache.gravitino.HasIdentifier;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.RelationalEntity;
+import org.apache.gravitino.SupportsExternalIdOperations;
import org.apache.gravitino.SupportsRelationOperations;
import org.apache.gravitino.authorization.SecurableObject;
import org.apache.gravitino.cache.CacheFactory;
@@ -65,7 +66,10 @@ import org.slf4j.LoggerFactory;
* RelationalBackend} interface. The default JDBC backend is {@link
JDBCBackend}.
*/
public class RelationalEntityStore
- implements EntityStore, SupportsRelationOperations,
SupportsEntityChangeLog {
+ implements EntityStore,
+ SupportsRelationOperations,
+ SupportsExternalIdOperations,
+ SupportsEntityChangeLog {
private static final Logger LOGGER =
LoggerFactory.getLogger(RelationalEntityStore.class);
public static final ImmutableMap<String, String> RELATIONAL_BACKENDS =
ImmutableMap.of(
@@ -181,6 +185,46 @@ public class RelationalEntityStore
});
}
+ @Override
+ public SupportsExternalIdOperations externalIdOperations() {
+ return this;
+ }
+
+ @Override
+ public <E extends Entity & HasIdentifier> E getByExternalId(
+ NameIdentifier ident, Entity.EntityType entityType, Class<E> type)
+ throws NoSuchEntityException, IOException {
+ return backend.getByExternalId(ident, entityType);
+ }
+
+ @Override
+ public <E extends Entity & HasIdentifier> E updateByExternalId(
+ NameIdentifier ident, Entity.EntityType entityType, Class<E> type,
Function<E, E> updater)
+ throws NoSuchEntityException, IOException {
+ E updatedEntity = backend.updateByExternalId(ident, entityType, updater);
+ cache.invalidate(updatedEntity.nameIdentifier(), entityType);
+ return updatedEntity;
+ }
+
+ @Override
+ public boolean deleteByExternalId(NameIdentifier ident, Entity.EntityType
entityType)
+ throws IOException {
+ NameIdentifier nameIdent = null;
+ try {
+ HasIdentifier entity = backend.getByExternalId(ident, entityType);
+ nameIdent = entity.nameIdentifier();
+ return backend.delete(nameIdent, entityType, false);
+ } catch (NoSuchEntityException e) {
+ LOGGER.warn(
+ "The entity to be deleted by external id does not exist in the
store: {}", ident, e);
+ return false;
+ } finally {
+ if (nameIdent != null) {
+ cache.invalidate(nameIdent, entityType);
+ }
+ }
+ }
+
@Override
public <E extends Entity & HasIdentifier> List<E> batchGet(
List<NameIdentifier> idents, Entity.EntityType entityType, Class<E>
clazz) {
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaMapper.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaMapper.java
index 4f16086b84..87f8a26b17 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaMapper.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaMapper.java
@@ -98,6 +98,16 @@ public interface UserMetaMapper {
UserUpdatedAt getUserUpdatedAt(
@Param("metalakeName") String metalakeName, @Param("userName") String
userName);
+ @SelectProvider(
+ type = UserMetaSQLProviderFactory.class,
+ method = "selectUserMetaByMetalakeNameAndExternalId")
+ UserPO selectUserMetaByMetalakeNameAndExternalId(
+ @Param("metalakeName") String metalakeName, @Param("externalId") String
externalId);
+
+ @UpdateProvider(type = UserMetaSQLProviderFactory.class, method =
"updateUserMetaByExternalId")
+ Integer updateUserMetaByExternalId(
+ @Param("newUserMeta") UserPO newUserPO, @Param("oldUserMeta") UserPO
oldUserPO);
+
/**
* Single-round-trip auth prefetch for the JCasbin authorize hot path.
Returns every version
* sentinel the request needs:
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaSQLProviderFactory.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaSQLProviderFactory.java
index 27953c35de..cc7de6fb28 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaSQLProviderFactory.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaSQLProviderFactory.java
@@ -114,4 +114,14 @@ public class UserMetaSQLProviderFactory {
@Param("groupNames") List<String> groupNames) {
return getProvider().batchGetAuthSubjectsForUser(metalakeName, userName,
groupNames);
}
+
+ public static String selectUserMetaByMetalakeNameAndExternalId(
+ @Param("metalakeName") String metalakeName, @Param("externalId") String
externalId) {
+ return
getProvider().selectUserMetaByMetalakeNameAndExternalId(metalakeName,
externalId);
+ }
+
+ public static String updateUserMetaByExternalId(
+ @Param("newUserMeta") UserPO newUserPO, @Param("oldUserMeta") UserPO
oldUserPO) {
+ return getProvider().updateUserMetaByExternalId(newUserPO, oldUserPO);
+ }
}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/OwnerMetaBaseSQLProvider.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/OwnerMetaBaseSQLProvider.java
index a89c3a1cc3..bee7d6014f 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/OwnerMetaBaseSQLProvider.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/OwnerMetaBaseSQLProvider.java
@@ -47,6 +47,8 @@ public class OwnerMetaBaseSQLProvider {
return "SELECT ut.user_id as userId,"
+ " ut.user_name as userName,"
+ " ut.metalake_id as metalakeId,"
+ + " ut.external_id as externalId,"
+ + " ut.enabled as enabled,"
+ " ut.audit_info as auditInfo,"
+ " ut.current_version as currentVersion,"
+ " ut.last_version as lastVersion,"
@@ -70,6 +72,8 @@ public class OwnerMetaBaseSQLProvider {
+ "ut.user_id as userId, "
+ "ut.user_name as userName, "
+ "ut.metalake_id as metalakeId, "
+ + "ut.external_id as externalId, "
+ + "ut.enabled as enabled, "
+ "ut.audit_info as auditInfo, "
+ "ut.current_version as currentVersion, "
+ "ut.last_version as lastVersion, "
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/UserMetaBaseSQLProvider.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/UserMetaBaseSQLProvider.java
index 4bb92e103c..6c0dc22334 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/UserMetaBaseSQLProvider.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/UserMetaBaseSQLProvider.java
@@ -44,25 +44,63 @@ public class UserMetaBaseSQLProvider {
@Param("metalakeId") Long metalakeId, @Param("userName") String name) {
return "SELECT user_id as userId, user_name as userName,"
+ " metalake_id as metalakeId,"
- + " audit_info as auditInfo,"
- + " current_version as currentVersion, last_version as lastVersion,"
- + " deleted_at as deletedAt"
+ + " external_id as externalId, enabled as enabled,"
+ + " audit_info as auditInfo, current_version as currentVersion,"
+ + " last_version as lastVersion, deleted_at as deletedAt"
+ " FROM "
+ USER_TABLE_NAME
+ " WHERE metalake_id = #{metalakeId} AND user_name = #{userName}"
+ " AND deleted_at = 0";
}
+ public String selectUserMetaByMetalakeNameAndExternalId(
+ @Param("metalakeName") String metalakeName, @Param("externalId") String
externalId) {
+ return "SELECT ut.user_id as userId, ut.user_name as userName,"
+ + " ut.metalake_id as metalakeId,"
+ + " ut.external_id as externalId, ut.enabled as enabled,"
+ + " ut.audit_info as auditInfo, ut.current_version as currentVersion,"
+ + " ut.last_version as lastVersion, ut.deleted_at as deletedAt"
+ + " FROM "
+ + USER_TABLE_NAME
+ + " ut JOIN "
+ + MetalakeMetaMapper.TABLE_NAME
+ + " mt ON ut.metalake_id = mt.metalake_id"
+ + " WHERE mt.metalake_name = #{metalakeName}"
+ + " AND ut.external_id = #{externalId}"
+ + " AND ut.deleted_at = 0 AND mt.deleted_at = 0";
+ }
+
+ public String updateUserMetaByExternalId(
+ @Param("newUserMeta") UserPO newUserPO, @Param("oldUserMeta") UserPO
oldUserPO) {
+ return "UPDATE "
+ + USER_TABLE_NAME
+ + " SET user_name = #{newUserMeta.userName},"
+ + " metalake_id = #{newUserMeta.metalakeId},"
+ + " audit_info = #{newUserMeta.auditInfo},"
+ + " external_id = #{newUserMeta.externalId},"
+ + " enabled = #{newUserMeta.enabled},"
+ + " current_version = #{newUserMeta.currentVersion},"
+ + " last_version = #{newUserMeta.lastVersion},"
+ + " deleted_at = #{newUserMeta.deletedAt}"
+ + " WHERE external_id = #{oldUserMeta.externalId}"
+ + " AND metalake_id = #{oldUserMeta.metalakeId}"
+ + " AND audit_info = #{oldUserMeta.auditInfo}"
+ + " AND current_version = #{oldUserMeta.currentVersion}"
+ + " AND last_version = #{oldUserMeta.lastVersion}"
+ + " AND deleted_at = 0";
+ }
+
public String insertUserMeta(@Param("userMeta") UserPO userPO) {
return "INSERT INTO "
+ USER_TABLE_NAME
- + " (user_id, user_name,"
- + " metalake_id, audit_info,"
- + " current_version, last_version, deleted_at)"
+ + " (user_id, user_name, metalake_id, external_id, enabled,"
+ + " audit_info, current_version, last_version, deleted_at)"
+ " VALUES ("
+ " #{userMeta.userId},"
+ " #{userMeta.userName},"
+ " #{userMeta.metalakeId},"
+ + " #{userMeta.externalId},"
+ + " #{userMeta.enabled},"
+ " #{userMeta.auditInfo},"
+ " #{userMeta.currentVersion},"
+ " #{userMeta.lastVersion},"
@@ -73,13 +111,14 @@ public class UserMetaBaseSQLProvider {
public String insertUserMetaOnDuplicateKeyUpdate(@Param("userMeta") UserPO
userPO) {
return "INSERT INTO "
+ USER_TABLE_NAME
- + " (user_id, user_name,"
- + " metalake_id, audit_info,"
- + " current_version, last_version, deleted_at)"
+ + " (user_id, user_name, metalake_id, external_id, enabled,"
+ + " audit_info, current_version, last_version, deleted_at)"
+ " VALUES ("
+ " #{userMeta.userId},"
+ " #{userMeta.userName},"
+ " #{userMeta.metalakeId},"
+ + " #{userMeta.externalId},"
+ + " #{userMeta.enabled},"
+ " #{userMeta.auditInfo},"
+ " #{userMeta.currentVersion},"
+ " #{userMeta.lastVersion},"
@@ -89,6 +128,8 @@ public class UserMetaBaseSQLProvider {
+ " user_name = #{userMeta.userName},"
+ " metalake_id = #{userMeta.metalakeId},"
+ " audit_info = #{userMeta.auditInfo},"
+ + " external_id = #{userMeta.externalId},"
+ + " enabled = #{userMeta.enabled},"
+ " current_version = #{userMeta.currentVersion},"
+ " last_version = #{userMeta.lastVersion},"
+ " deleted_at = #{userMeta.deletedAt}";
@@ -117,6 +158,8 @@ public class UserMetaBaseSQLProvider {
+ " SET user_name = #{newUserMeta.userName},"
+ " metalake_id = #{newUserMeta.metalakeId},"
+ " audit_info = #{newUserMeta.auditInfo},"
+ + " external_id = #{newUserMeta.externalId},"
+ + " enabled = #{newUserMeta.enabled},"
+ " current_version = #{newUserMeta.currentVersion},"
+ " last_version = #{newUserMeta.lastVersion},"
+ " deleted_at = #{newUserMeta.deletedAt}"
@@ -132,6 +175,7 @@ public class UserMetaBaseSQLProvider {
public String listUsersByRoleId(@Param("roleId") Long roleId) {
return "SELECT us.user_id as userId, us.user_name as userName,"
+ " us.metalake_id as metalakeId,"
+ + " us.external_id as externalId, us.enabled as enabled,"
+ " us.audit_info as auditInfo, us.current_version as currentVersion,"
+ " us.last_version as lastVersion, us.deleted_at as deletedAt"
+ " FROM "
@@ -146,6 +190,7 @@ public class UserMetaBaseSQLProvider {
public String listUserPOsByMetalake(@Param("metalakeName") String
metalakeName) {
return "SELECT ut.user_id as userId, ut.user_name as userName,"
+ " ut.metalake_id as metalakeId,"
+ + " ut.external_id as externalId, ut.enabled as enabled,"
+ " ut.audit_info as auditInfo,"
+ " ut.current_version as currentVersion, ut.last_version as
lastVersion,"
+ " ut.deleted_at as deletedAt"
@@ -161,6 +206,7 @@ public class UserMetaBaseSQLProvider {
public String listExtendedUserPOsByMetalakeId(@Param("metalakeId") Long
metalakeId) {
return "SELECT ut.user_id as userId, ut.user_name as userName,"
+ " ut.metalake_id as metalakeId,"
+ + " ut.external_id as externalId, ut.enabled as enabled,"
+ " ut.audit_info as auditInfo,"
+ " ut.current_version as currentVersion, ut.last_version as
lastVersion,"
+ " ut.deleted_at as deletedAt,"
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/h2/UserMetaH2Provider.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/h2/UserMetaH2Provider.java
index fb2a3bd827..894babf2db 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/h2/UserMetaH2Provider.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/h2/UserMetaH2Provider.java
@@ -30,6 +30,7 @@ public class UserMetaH2Provider extends
UserMetaBaseSQLProvider {
public String listExtendedUserPOsByMetalakeId(@Param("metalakeId") Long
metalakeId) {
return "SELECT ut.user_id as userId, ut.user_name as userName,"
+ " ut.metalake_id as metalakeId,"
+ + " ut.external_id as externalId, ut.enabled as enabled,"
+ " ut.audit_info as auditInfo,"
+ " ut.current_version as currentVersion, ut.last_version as
lastVersion,"
+ " ut.deleted_at as deletedAt,"
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/UserMetaPostgreSQLProvider.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/UserMetaPostgreSQLProvider.java
index a85a1610ab..2305535b1d 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/UserMetaPostgreSQLProvider.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/UserMetaPostgreSQLProvider.java
@@ -47,13 +47,15 @@ public class UserMetaPostgreSQLProvider extends
UserMetaBaseSQLProvider {
public String insertUserMetaOnDuplicateKeyUpdate(UserPO userPO) {
return "INSERT INTO "
+ USER_TABLE_NAME
- + " (user_id, user_name,"
- + " metalake_id, audit_info,"
+ + " (user_id, user_name, metalake_id, external_id, enabled,"
+ + " audit_info,"
+ " current_version, last_version, deleted_at)"
+ " VALUES ("
+ " #{userMeta.userId},"
+ " #{userMeta.userName},"
+ " #{userMeta.metalakeId},"
+ + " #{userMeta.externalId},"
+ + " #{userMeta.enabled},"
+ " #{userMeta.auditInfo},"
+ " #{userMeta.currentVersion},"
+ " #{userMeta.lastVersion},"
@@ -62,6 +64,8 @@ public class UserMetaPostgreSQLProvider extends
UserMetaBaseSQLProvider {
+ " ON CONFLICT(user_id) DO UPDATE SET"
+ " user_name = #{userMeta.userName},"
+ " metalake_id = #{userMeta.metalakeId},"
+ + " external_id = #{userMeta.externalId},"
+ + " enabled = #{userMeta.enabled},"
+ " audit_info = #{userMeta.auditInfo},"
+ " current_version = #{userMeta.currentVersion},"
+ " last_version = #{userMeta.lastVersion},"
@@ -72,6 +76,7 @@ public class UserMetaPostgreSQLProvider extends
UserMetaBaseSQLProvider {
public String listExtendedUserPOsByMetalakeId(Long metalakeId) {
return "SELECT ut.user_id as userId, ut.user_name as userName,"
+ " ut.metalake_id as metalakeId,"
+ + " ut.external_id as externalId, ut.enabled as enabled,"
+ " ut.audit_info as auditInfo,"
+ " ut.current_version as currentVersion, ut.last_version as
lastVersion,"
+ " ut.deleted_at as deletedAt,"
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/po/UserPO.java
b/core/src/main/java/org/apache/gravitino/storage/relational/po/UserPO.java
index 2a9079bc53..0bf3cd9dfe 100644
--- a/core/src/main/java/org/apache/gravitino/storage/relational/po/UserPO.java
+++ b/core/src/main/java/org/apache/gravitino/storage/relational/po/UserPO.java
@@ -25,6 +25,8 @@ public class UserPO {
private Long userId;
private String userName;
private Long metalakeId;
+ private String externalId;
+ private Boolean enabled;
private String auditInfo;
private Long currentVersion;
private Long lastVersion;
@@ -42,6 +44,14 @@ public class UserPO {
return metalakeId;
}
+ public String getExternalId() {
+ return externalId;
+ }
+
+ public Boolean getEnabled() {
+ return enabled;
+ }
+
public String getAuditInfo() {
return auditInfo;
}
@@ -70,6 +80,8 @@ public class UserPO {
return Objects.equal(getUserId(), tablePO.getUserId())
&& Objects.equal(getUserName(), tablePO.getUserName())
&& Objects.equal(getMetalakeId(), tablePO.getMetalakeId())
+ && Objects.equal(getExternalId(), tablePO.getExternalId())
+ && Objects.equal(getEnabled(), tablePO.getEnabled())
&& Objects.equal(getAuditInfo(), tablePO.getAuditInfo())
&& Objects.equal(getCurrentVersion(), tablePO.getCurrentVersion())
&& Objects.equal(getLastVersion(), tablePO.getLastVersion())
@@ -82,6 +94,8 @@ public class UserPO {
getUserId(),
getUserName(),
getMetalakeId(),
+ getExternalId(),
+ getEnabled(),
getAuditInfo(),
getCurrentVersion(),
getLastVersion(),
@@ -110,6 +124,16 @@ public class UserPO {
return this;
}
+ public Builder withExternalId(String externalId) {
+ userPO.externalId = externalId;
+ return this;
+ }
+
+ public Builder withEnabled(Boolean enabled) {
+ userPO.enabled = enabled;
+ return this;
+ }
+
public Builder withAuditInfo(String auditInfo) {
userPO.auditInfo = auditInfo;
return this;
@@ -141,6 +165,9 @@ public class UserPO {
}
public UserPO build() {
+ if (userPO.enabled == null) {
+ userPO.enabled = true;
+ }
validate();
return userPO;
}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/service/UserMetaService.java
b/core/src/main/java/org/apache/gravitino/storage/relational/service/UserMetaService.java
index bb810b1811..ba8cf5271a 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/service/UserMetaService.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/service/UserMetaService.java
@@ -320,4 +320,71 @@ public class UserMetaService {
return userDeletedCount[0] + userRoleRelDeletedCount[0];
}
+
+ private UserPO getUserPOByMetalakeNameAndExternalId(String metalakeName,
String externalId) {
+ UserPO userPO =
+ SessionUtils.getWithoutCommit(
+ UserMetaMapper.class,
+ mapper ->
mapper.selectUserMetaByMetalakeNameAndExternalId(metalakeName, externalId));
+
+ if (userPO == null) {
+ throw new NoSuchEntityException(
+ NoSuchEntityException.NO_SUCH_ENTITY_MESSAGE,
+ Entity.EntityType.USER.name().toLowerCase(),
+ externalId);
+ }
+ return userPO;
+ }
+
+ @Monitored(
+ metricsSource = GRAVITINO_RELATIONAL_STORE_METRIC_NAME,
+ baseMetricName = "getUserByExternalId")
+ public UserEntity getUserByExternalId(NameIdentifier ident) {
+ AuthorizationUtils.checkUserExternalId(ident);
+ String metalake = ident.namespace().level(0);
+ String externalId = ident.name();
+ Namespace userNamespace = AuthorizationUtils.ofUserNamespace(metalake);
+ UserPO userPO = getUserPOByMetalakeNameAndExternalId(metalake, externalId);
+ List<RolePO> rolePOs =
RoleMetaService.getInstance().listRolesByUserId(userPO.getUserId());
+ return POConverters.fromUserPO(userPO, rolePOs, userNamespace);
+ }
+
+ @Monitored(
+ metricsSource = GRAVITINO_RELATIONAL_STORE_METRIC_NAME,
+ baseMetricName = "updateUserByExternalId")
+ public <E extends Entity & HasIdentifier> UserEntity updateUserByExternalId(
+ NameIdentifier ident, Function<E, E> updater) throws IOException {
+ AuthorizationUtils.checkUserExternalId(ident);
+ String metalake = ident.namespace().level(0);
+ String externalId = ident.name();
+ Namespace userNamespace = AuthorizationUtils.ofUserNamespace(metalake);
+ UserPO oldUserPO = getUserPOByMetalakeNameAndExternalId(metalake,
externalId);
+ List<RolePO> rolePOs =
RoleMetaService.getInstance().listRolesByUserId(oldUserPO.getUserId());
+ UserEntity oldEntity = POConverters.fromUserPO(oldUserPO, rolePOs,
userNamespace);
+ UserEntity newEntity = (UserEntity) updater.apply((E) oldEntity);
+ Preconditions.checkArgument(
+ Objects.equals(oldEntity.id(), newEntity.id()),
+ "The updated user entity id: %s should be same with the user entity id
before: %s",
+ newEntity.id(),
+ oldEntity.id());
+
+ try {
+ SessionUtils.doMultipleWithCommit(
+ () ->
+ SessionUtils.doWithoutCommit(
+ UserMetaMapper.class,
+ mapper ->
+ mapper.updateUserMetaByExternalId(
+ POConverters.updateUserPOWithVersion(oldUserPO,
newEntity), oldUserPO)),
+ () ->
+ SessionUtils.doWithoutCommit(
+ UserMetaMapper.class,
+ mapper -> mapper.touchUserUpdatedAt(oldUserPO.getUserId())));
+ } catch (RuntimeException re) {
+ ExceptionUtils.checkSQLException(
+ re, Entity.EntityType.USER, newEntity.nameIdentifier().toString());
+ throw re;
+ }
+ return newEntity;
+ }
}
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 ae69ce2797..48eb78dfc1 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
@@ -956,6 +956,8 @@ public class POConverters {
return builder
.withUserId(userEntity.id())
.withUserName(userEntity.name())
+ .withExternalId(userEntity.externalId())
+ .withEnabled(userEntity.enabled())
.withAuditInfo(JsonUtils.anyFieldMapper().writeValueAsString(userEntity.auditInfo()))
.withCurrentVersion(INIT_VERSION)
.withLastVersion(INIT_VERSION)
@@ -983,6 +985,8 @@ public class POConverters {
.withUserId(oldUserPO.getUserId())
.withUserName(newUser.name())
.withMetalakeId(oldUserPO.getMetalakeId())
+ .withExternalId(newUser.externalId())
+ .withEnabled(newUser.enabled())
.withAuditInfo(JsonUtils.anyFieldMapper().writeValueAsString(newUser.auditInfo()))
.withCurrentVersion(nextVersion)
.withLastVersion(nextVersion)
@@ -1012,6 +1016,8 @@ public class POConverters {
.withId(userPO.getUserId())
.withName(userPO.getUserName())
.withNamespace(namespace)
+ .withExternalId(userPO.getExternalId())
+ .withEnabled(userPO.getEnabled())
.withAuditInfo(
JsonUtils.anyFieldMapper().readValue(userPO.getAuditInfo(),
AuditInfo.class));
if (!roleNames.isEmpty()) {
@@ -1040,6 +1046,8 @@ public class POConverters {
.withId(userPO.getUserId())
.withName(userPO.getUserName())
.withNamespace(namespace)
+ .withExternalId(userPO.getExternalId())
+ .withEnabled(userPO.getEnabled())
.withAuditInfo(
JsonUtils.anyFieldMapper().readValue(userPO.getAuditInfo(),
AuditInfo.class));
if (StringUtils.isNotBlank(userPO.getRoleNames())) {
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 e3523d3bd1..1763c2b8c6 100644
---
a/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java
+++
b/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java
@@ -49,7 +49,9 @@ import java.io.File;
import java.io.IOException;
import java.time.Instant;
import java.util.Arrays;
+import java.util.Collections;
import java.util.Comparator;
+import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.apache.commons.lang3.reflect.FieldUtils;
@@ -80,6 +82,7 @@ import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
import org.mockito.Mockito;
public class TestAccessControlManager {
@@ -454,6 +457,114 @@ public class TestAccessControlManager {
accessControlManager.deleteRole("metalake_list", "testList2");
}
+ @Test
+ public void testUserExtId() {
+ accessControlManager.addUser(METALAKE, "disabled_user",
"ext-disabled-user", false);
+ accessControlManager.removeUser(METALAKE, "disabled_user");
+
+ String user = "ext_user";
+ String extId = "ext-user-1";
+ User added = accessControlManager.addUser(METALAKE, user, extId, true);
+ Assertions.assertEquals(extId, added.externalId());
+ Assertions.assertTrue(added.enabled());
+
+ createCatalogRole("ext_role");
+ accessControlManager.grantRolesToUser(METALAKE,
Lists.newArrayList("ext_role"), user);
+ User disabled = accessControlManager.disableUser(METALAKE, extId);
+ Assertions.assertFalse(disabled.enabled());
+ assertSortedRoles(disabled, "ext_role");
+
+ createCatalogRole("ext_role2");
+ accessControlManager.grantRolesToUser(METALAKE,
Lists.newArrayList("ext_role2"), user);
+ assertSortedRoles(accessControlManager.getUser(METALAKE, user),
"ext_role", "ext_role2");
+ Assertions.assertFalse(accessControlManager.getUserByExternalId(METALAKE,
extId).enabled());
+
+ User enabled = accessControlManager.enableUser(METALAKE, extId);
+ Assertions.assertTrue(enabled.enabled());
+ assertSortedRoles(enabled, "ext_role", "ext_role2");
+
+ accessControlManager.revokeRolesFromUser(
+ METALAKE, Lists.newArrayList("ext_role", "ext_role2"), user);
+ accessControlManager.deleteRole(METALAKE, "ext_role2");
+ accessControlManager.deleteRole(METALAKE, "ext_role");
+ accessControlManager.removeUser(METALAKE, user);
+ }
+
+ @Test
+ public void testMissingExt() {
+ assertInvalidExt(() -> accessControlManager.getUserByExternalId(METALAKE,
null));
+ assertInvalidExt(() -> accessControlManager.getUserByExternalId(METALAKE,
""));
+ assertMissingExt(
+ NoSuchUserException.class,
+ () -> accessControlManager.getUserByExternalId(METALAKE,
"missing-ext-id"));
+ assertMissingExt(
+ NoSuchUserException.class,
+ () -> accessControlManager.disableUser(METALAKE, "missing-ext-id"));
+ assertMissingExt(
+ NoSuchUserException.class,
+ () -> accessControlManager.enableUser(METALAKE, "missing-ext-id"));
+ }
+
+ @Test
+ public void testExtDup() {
+ accessControlManager.addUser(METALAKE, "u1", "dup-ext", true);
+ assertThrowsExt(
+ UserAlreadyExistsException.class,
+ () -> accessControlManager.addUser(METALAKE, "u2", "dup-ext", true));
+ accessControlManager.removeUser(METALAKE, "u1");
+ }
+
+ @Test
+ public void testUserExtDel() {
+ String extId = "ext-remove-user";
+ accessControlManager.addUser(METALAKE, "remove_user", extId, true);
+
Assertions.assertTrue(accessControlManager.removeUserByExternalId(METALAKE,
extId));
+ assertMissingExt(
+ NoSuchUserException.class, () ->
accessControlManager.getUserByExternalId(METALAKE, extId));
+ Assertions.assertThrows(
+ NoSuchUserException.class, () ->
accessControlManager.getUser(METALAKE, "remove_user"));
+
Assertions.assertFalse(accessControlManager.removeUserByExternalId(METALAKE,
"missing-ext-id"));
+ }
+
+ @Test
+ public void testExtCache() {
+ String extId = "ext-cache-user";
+ accessControlManager.addUser(METALAKE, "cache_user", extId, true);
+ accessControlManager.getUser(METALAKE, "cache_user");
+ accessControlManager.disableUser(METALAKE, extId);
+ Assertions.assertFalse(accessControlManager.getUser(METALAKE,
"cache_user").enabled());
+ accessControlManager.removeUser(METALAKE, "cache_user");
+ }
+
+ private void createCatalogRole(String role) {
+ accessControlManager.createRole(
+ METALAKE,
+ role,
+ ImmutableMap.of("k1", "v1"),
+ Lists.newArrayList(
+ SecurableObjects.ofCatalog(
+ "catalog",
Lists.newArrayList(Privileges.UseCatalog.allow()))));
+ }
+
+ private void assertSortedRoles(User user, String... expectedRoles) {
+ List<String> roles = Lists.newArrayList(user.roles());
+ Collections.sort(roles);
+ Assertions.assertEquals(Lists.newArrayList(expectedRoles), roles);
+ }
+
+ private void assertThrowsExt(Class<? extends Exception> type, Executable
executable) {
+ Assertions.assertThrows(type, executable);
+ }
+
+ private void assertInvalidExt(Executable executable) {
+ Assertions.assertThrows(IllegalArgumentException.class, executable);
+ }
+
+ private void assertMissingExt(Class<? extends Exception> type, Executable
executable) {
+ Exception ex = Assertions.assertThrows(type, executable);
+ Assertions.assertTrue(ex.getMessage().contains("external id"));
+ }
+
private void testProperties(Map<String, String> expectedProps, Map<String,
String> testProps) {
expectedProps.forEach(
(k, v) -> {
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 c0ae444eb0..5017f4211c 100644
---
a/core/src/test/java/org/apache/gravitino/authorization/TestAuthorizationUtils.java
+++
b/core/src/test/java/org/apache/gravitino/authorization/TestAuthorizationUtils.java
@@ -54,6 +54,7 @@ class TestAuthorizationUtils {
NameIdentifier user = AuthorizationUtils.ofUser(metalake, "user");
NameIdentifier group = AuthorizationUtils.ofGroup(metalake, "group");
NameIdentifier role = AuthorizationUtils.ofRole(metalake, "role");
+ NameIdentifier userExt = AuthorizationUtils.ofUserExternalId(metalake,
"ext-1");
Assertions.assertEquals(AuthorizationUtils.ofUserNamespace(metalake),
user.namespace());
Assertions.assertEquals("user", user.name());
@@ -61,6 +62,17 @@ class TestAuthorizationUtils {
Assertions.assertEquals("group", group.name());
Assertions.assertEquals(AuthorizationUtils.ofRoleNamespace(metalake),
role.namespace());
Assertions.assertEquals("role", role.name());
+ Assertions.assertEquals(
+ AuthorizationUtils.ofUserExternalIdNamespace(metalake),
userExt.namespace());
+ Assertions.assertEquals("ext-1", userExt.name());
+ Assertions.assertNotEquals(user, userExt);
+ Assertions.assertNotEquals(AuthorizationUtils.ofUser(metalake, "ext-1"),
userExt);
+ assertInvalidExternalId(() ->
AuthorizationUtils.ofUserExternalId(metalake, null));
+ assertInvalidExternalId(() ->
AuthorizationUtils.ofUserExternalId(metalake, ""));
+ }
+
+ private void
assertInvalidExternalId(org.junit.jupiter.api.function.Executable executable) {
+ Assertions.assertThrows(IllegalNameIdentifierException.class, executable);
}
@Test
@@ -119,15 +131,19 @@ class TestAuthorizationUtils {
@Test
void testCheckNameIdentifier() {
NameIdentifier user = AuthorizationUtils.ofUser(metalake, "user");
+ NameIdentifier userExternalId =
AuthorizationUtils.ofUserExternalId(metalake, "ext-1");
NameIdentifier group = AuthorizationUtils.ofGroup(metalake, "group");
NameIdentifier role = AuthorizationUtils.ofRole(metalake, "role");
Assertions.assertDoesNotThrow(() -> AuthorizationUtils.checkUser(user));
+ Assertions.assertDoesNotThrow(() ->
AuthorizationUtils.checkUserExternalId(userExternalId));
Assertions.assertDoesNotThrow(() -> AuthorizationUtils.checkGroup(group));
Assertions.assertDoesNotThrow(() -> AuthorizationUtils.checkRole(role));
Assertions.assertThrows(
IllegalNameIdentifierException.class, () ->
AuthorizationUtils.checkUser(null));
+ Assertions.assertThrows(
+ IllegalNameIdentifierException.class, () ->
AuthorizationUtils.checkUserExternalId(null));
Assertions.assertThrows(
IllegalNameIdentifierException.class, () ->
AuthorizationUtils.checkGroup(null));
Assertions.assertThrows(
@@ -135,6 +151,9 @@ class TestAuthorizationUtils {
Assertions.assertThrows(
IllegalNameIdentifierException.class,
() -> AuthorizationUtils.checkUser(NameIdentifier.of("")));
+ Assertions.assertThrows(
+ IllegalNameIdentifierException.class,
+ () -> AuthorizationUtils.checkUserExternalId(NameIdentifier.of("")));
Assertions.assertThrows(
IllegalNameIdentifierException.class,
() -> AuthorizationUtils.checkGroup(NameIdentifier.of("")));
@@ -146,15 +165,21 @@ class TestAuthorizationUtils {
@Test
void testCheckNamespace() {
Namespace userNamespace = AuthorizationUtils.ofUserNamespace(metalake);
+ Namespace userExternalIdNamespace =
AuthorizationUtils.ofUserExternalIdNamespace(metalake);
Namespace groupNamespace = AuthorizationUtils.ofGroupNamespace(metalake);
Namespace roleNamespace = AuthorizationUtils.ofRoleNamespace(metalake);
Assertions.assertDoesNotThrow(() ->
AuthorizationUtils.checkUserNamespace(userNamespace));
+ Assertions.assertDoesNotThrow(
+ () ->
AuthorizationUtils.checkUserExternalIdNamespace(userExternalIdNamespace));
Assertions.assertDoesNotThrow(() ->
AuthorizationUtils.checkGroupNamespace(groupNamespace));
Assertions.assertDoesNotThrow(() ->
AuthorizationUtils.checkRoleNamespace(roleNamespace));
Assertions.assertThrows(
IllegalNamespaceException.class, () ->
AuthorizationUtils.checkUserNamespace(null));
+ Assertions.assertThrows(
+ IllegalNamespaceException.class,
+ () -> AuthorizationUtils.checkUserExternalIdNamespace(null));
Assertions.assertThrows(
IllegalNamespaceException.class, () ->
AuthorizationUtils.checkGroupNamespace(null));
Assertions.assertThrows(
@@ -162,6 +187,9 @@ class TestAuthorizationUtils {
Assertions.assertThrows(
IllegalNamespaceException.class,
() -> AuthorizationUtils.checkUserNamespace(Namespace.of("a", "b")));
+ Assertions.assertThrows(
+ IllegalNamespaceException.class,
+ () ->
AuthorizationUtils.checkUserExternalIdNamespace(Namespace.of("a", "b")));
Assertions.assertThrows(
IllegalNamespaceException.class,
() -> AuthorizationUtils.checkGroupNamespace(Namespace.of("a")));
diff --git
a/core/src/test/java/org/apache/gravitino/listener/api/event/TestOwnerEvent.java
b/core/src/test/java/org/apache/gravitino/listener/api/event/TestOwnerEvent.java
index 4f2c597b38..49f177c479 100644
---
a/core/src/test/java/org/apache/gravitino/listener/api/event/TestOwnerEvent.java
+++
b/core/src/test/java/org/apache/gravitino/listener/api/event/TestOwnerEvent.java
@@ -37,7 +37,6 @@ import org.apache.gravitino.authorization.OwnerManager;
import org.apache.gravitino.listener.DummyEventListener;
import org.apache.gravitino.listener.EventBus;
import org.apache.gravitino.listener.api.info.OwnerInfo;
-import org.apache.gravitino.storage.relational.RelationalBackend;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -51,7 +50,7 @@ public class TestOwnerEvent {
private OwnerManager innerOwnerManager;
private DummyEventListener dummyEventListener;
- interface TestEntityStore extends EntityStore, RelationalBackend,
SupportsRelationOperations {
+ interface TestEntityStore extends EntityStore, SupportsRelationOperations {
// This interface is used to mock the EntityStore for testing purposes.
}
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 ead13a39f1..8d337c32ce 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
@@ -41,6 +41,7 @@ import org.apache.gravitino.HasIdentifier;
import org.apache.gravitino.Metalake;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
+import org.apache.gravitino.SupportsExternalIdOperations;
import org.apache.gravitino.TestCatalog;
import org.apache.gravitino.authorization.AuthorizationUtils;
import org.apache.gravitino.authorization.Privileges;
@@ -65,7 +66,7 @@ import org.mockito.Mockito;
public class TestMemoryEntityStore {
- public static class InMemoryEntityStore implements EntityStore {
+ public static class InMemoryEntityStore implements EntityStore,
SupportsExternalIdOperations {
private final Map<NameIdentifier, Entity> entityMap;
private final Lock lock;
@@ -147,6 +148,68 @@ public class TestMemoryEntityStore {
return e;
}
+ @Override
+ public SupportsExternalIdOperations externalIdOperations() {
+ return this;
+ }
+
+ @Override
+ public <E extends Entity & HasIdentifier> E getByExternalId(
+ NameIdentifier ident, EntityType entityType, Class<E> type)
+ throws NoSuchEntityException, IOException {
+ if (entityType != EntityType.USER) {
+ 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));
+
+ for (Map.Entry<NameIdentifier, Entity> entry : entityMap.entrySet()) {
+ Entity entity = entry.getValue();
+ if (!entity.type().equals(entityType)
+ || !entry.getKey().namespace().equals(userNamespace)) {
+ continue;
+ }
+
+ String entityExternalId = null;
+ if (entity instanceof UserEntity userEntity) {
+ entityExternalId = userEntity.externalId();
+ }
+
+ if (externalId.equals(entityExternalId)) {
+ return (E) entity;
+ }
+ }
+
+ throw new NoSuchEntityException(
+ NoSuchEntityException.NO_SUCH_ENTITY_MESSAGE,
+ entityType.name().toLowerCase(),
+ externalId);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ 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);
+ }
+
+ @Override
+ public boolean deleteByExternalId(NameIdentifier ident, EntityType
entityType)
+ throws IOException {
+ try {
+ UserEntity user = getByExternalId(ident, entityType, UserEntity.class);
+ return delete(user.nameIdentifier(), entityType);
+ } catch (NoSuchEntityException e) {
+ return false;
+ }
+ }
+
@Override
public <E extends Entity & HasIdentifier> List<E> batchGet(
List<NameIdentifier> idents, EntityType entityType, Class<E> e) {
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestAuthMappers.java
b/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestAuthMappers.java
index 2175a0a6b5..e26613c790 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestAuthMappers.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestAuthMappers.java
@@ -414,6 +414,7 @@ public class TestAuthMappers {
.withUserId(userId)
.withUserName(userName)
.withMetalakeId(metalakeId)
+ .withEnabled(true)
.withAuditInfo(auditInfo.toString())
.withCurrentVersion(1L)
.withLastVersion(0L)
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestUserRoleRelMapper.java
b/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestUserRoleRelMapper.java
index 38e9b044cc..1c99ea8e89 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestUserRoleRelMapper.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestUserRoleRelMapper.java
@@ -166,6 +166,7 @@ public class TestUserRoleRelMapper {
.withUserId(1L)
.withUserName("user1")
.withMetalakeId(1L)
+ .withEnabled(true)
.withAuditInfo(auditInfo.toString())
.withCurrentVersion(1L)
.withLastVersion(0L)
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestUserMetaService.java
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestUserMetaService.java
index 73345e1340..f6370317a9 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestUserMetaService.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestUserMetaService.java
@@ -40,6 +40,7 @@ import java.util.function.Function;
import java.util.stream.Collectors;
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;
@@ -65,6 +66,7 @@ import org.apache.gravitino.utils.NamespaceUtil;
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 TestUserMetaService extends TestJDBCBackend {
@@ -1260,6 +1262,151 @@ class TestUserMetaService extends TestJDBCBackend {
assertTrue(none.isEmpty());
}
+ @TestTemplate
+ void testUserExtId() throws IOException {
+ UserMetaService svc = userMetaService();
+ svc.insertUser(userWithExtId("u1", "ext-1"), false);
+ UserEntity found = svc.getUserByExternalId(userExtIdent("ext-1"));
+ Assertions.assertEquals("u1", found.name());
+ Assertions.assertEquals("ext-1", found.externalId());
+ assertThrowsExt(
+ NoSuchEntityException.class, () ->
svc.getUserByExternalId(userExtIdent("missing-ext-id")));
+ assertThrowsExt(
+ IllegalArgumentException.class, () ->
svc.getUserByExternalId(userExtIdent("")));
+ assertThrowsExt(
+ NoSuchEntityException.class,
+ () -> svc.updateUserByExternalId(userExtIdent("missing-ext-id"),
enabledUpdater(false)));
+ }
+
+ @TestTemplate
+ void testExtEnable() throws IOException {
+ UserMetaService svc = userMetaService();
+ svc.insertUser(userWithExtId("u1", "ext-1"), false);
+ Assertions.assertFalse(
+ svc.updateUserByExternalId(userExtIdent("ext-1"),
enabledUpdater(false)).enabled());
+ Assertions.assertTrue(
+ svc.updateUserByExternalId(userExtIdent("ext-1"),
enabledUpdater(true)).enabled());
+
+ svc.insertUser(userWithExtId("u2", "ext-db"), false);
+ long updatedAtBefore = queryUpdatedAtByExtId("ext-db");
+ svc.updateUserByExternalId(userExtIdent("ext-db"), enabledUpdater(false));
+ Assertions.assertFalse(queryEnabledByExtId("ext-db"));
+ long updatedAtAfterDisable = queryUpdatedAtByExtId("ext-db");
+ Assertions.assertTrue(updatedAtAfterDisable >= updatedAtBefore);
+
+ svc.updateUserByExternalId(userExtIdent("ext-db"), enabledUpdater(true));
+ Assertions.assertTrue(queryEnabledByExtId("ext-db"));
+ Assertions.assertTrue(queryUpdatedAtByExtId("ext-db") >=
updatedAtAfterDisable);
+ }
+
+ @TestTemplate
+ void testExtEnableDel() throws IOException {
+ UserMetaService svc = userMetaService();
+ UserEntity user = userWithExtId("u1", "ext-del");
+ svc.insertUser(user, false);
+ Assertions.assertTrue(svc.deleteUser(user.nameIdentifier()));
+ assertThrowsExt(
+ NoSuchEntityException.class,
+ () -> svc.updateUserByExternalId(userExtIdent("ext-del"),
enabledUpdater(false)));
+ }
+
+ @TestTemplate
+ void testUserExtDel() throws IOException {
+ UserMetaService svc = userMetaService();
+ svc.insertUser(userWithExtId("u1", "ext-del-by"), false);
+ UserEntity user = svc.getUserByExternalId(userExtIdent("ext-del-by"));
+ Assertions.assertEquals("u1", user.name());
+ Assertions.assertTrue(svc.deleteUser(user.nameIdentifier()));
+ assertThrowsExt(
+ NoSuchEntityException.class, () ->
svc.getUserByExternalId(userExtIdent("ext-del-by")));
+ assertThrowsExt(NoSuchEntityException.class, () ->
svc.deleteUser(user.nameIdentifier()));
+ }
+
+ @TestTemplate
+ void testExtDup() throws IOException {
+ UserMetaService svc = userMetaService();
+ svc.insertUser(userWithExtId("u1", "ext-1"), false);
+ assertThrowsExt(
+ EntityAlreadyExistsException.class,
+ () -> svc.insertUser(userWithExtId("u2", "ext-1"), false));
+ }
+
+ private UserMetaService userMetaService() throws IOException {
+ createAndInsertMakeLake(metalakeName);
+ return UserMetaService.getInstance();
+ }
+
+ private void assertThrowsExt(Class<? extends Exception> type, Executable
executable) {
+ Assertions.assertThrows(type, executable);
+ }
+
+ private UserEntity userWithExtId(String name, String externalId) {
+ return UserEntity.builder()
+ .withId(RandomIdGenerator.INSTANCE.nextId())
+ .withName(name)
+ .withNamespace(AuthorizationUtils.ofUserNamespace(metalakeName))
+ .withExternalId(externalId)
+ .withEnabled(true)
+ .withAuditInfo(AUDIT_INFO)
+ .build();
+ }
+
+ private Namespace userNamespace() {
+ return AuthorizationUtils.ofUserNamespace(metalakeName);
+ }
+
+ private NameIdentifier userExtIdent(String externalId) {
+ return AuthorizationUtils.ofUserExternalId(metalakeName, externalId);
+ }
+
+ private Function<UserEntity, UserEntity> enabledUpdater(boolean enabled) {
+ return user ->
+ UserEntity.builder()
+ .withId(user.id())
+ .withName(user.name())
+ .withNamespace(user.namespace())
+ .withExternalId(user.externalId())
+ .withEnabled(enabled)
+ .withRoleNames(user.roleNames())
+ .withRoleIds(user.roleIds())
+ .withAuditInfo(user.auditInfo())
+ .build();
+ }
+
+ private boolean queryEnabledByExtId(String externalId) {
+ try (SqlSession sqlSession =
+
SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true);
+ Connection connection = sqlSession.getConnection();
+ Statement statement = connection.createStatement();
+ ResultSet rs =
+ statement.executeQuery(
+ String.format(
+ "SELECT enabled FROM user_meta WHERE external_id = '%s'
AND deleted_at = 0",
+ externalId))) {
+ Assertions.assertTrue(rs.next());
+ return rs.getBoolean(1);
+ } catch (SQLException e) {
+ throw new RuntimeException("Query user enabled failed", e);
+ }
+ }
+
+ private long queryUpdatedAtByExtId(String externalId) {
+ try (SqlSession sqlSession =
+
SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true);
+ Connection connection = sqlSession.getConnection();
+ Statement statement = connection.createStatement();
+ ResultSet rs =
+ statement.executeQuery(
+ String.format(
+ "SELECT updated_at FROM user_meta WHERE external_id = '%s'
AND deleted_at = 0",
+ externalId))) {
+ Assertions.assertTrue(rs.next());
+ return rs.getLong(1);
+ } catch (SQLException e) {
+ throw new RuntimeException("Query user updated_at failed", e);
+ }
+ }
+
private Integer countUsers(Long metalakeId) {
int count = 0;
try (SqlSession sqlSession =