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 ab0d52bae5 [#11835] feat: external-id user/group REST create APIs and
AccessControlIT (#11921)
ab0d52bae5 is described below
commit ab0d52bae5b056c6b2e4816006dd58ee0bc2e333
Author: MaSai <[email protected]>
AuthorDate: Tue Jul 7 19:27:38 2026 +0800
[#11835] feat: external-id user/group REST create APIs and AccessControlIT
(#11921)
### What changes were proposed in this pull request?
Extend 8090 user/group create REST APIs and Java client to support
optional external-id create, and add integration coverage for the
REST-exposed dispatcher paths.
**REST (`server`)**
- `POST /metalakes/{metalake}/users`: route to `addUser(metalake, name,
externalId, enabled)` when `externalId` is present; otherwise keep
existing `addUser(metalake, name)` path
- `POST /metalakes/{metalake}/groups`: route to `addGroup(metalake,
name, externalId)` when `externalId` is present; otherwise keep existing
`addGroup(metalake, name)` path
**DTOs (`common`)**
- Extend `UserAddRequest` with optional `externalId` and optional
`enabled` (defaults to `true` when unset)
- Extend `GroupAddRequest` with optional `externalId`
**Java client (`clients/client-java`)**
- Add `GravitinoMetalake.addUser(name, externalId, enabled)` and
`addGroup(name, externalId)`
- Delegate the new overloads from `GravitinoClient`
**Docs**
- Update `docs/security/access-control.md` with external-id create
examples for REST and Java client
- Update OpenAPI schemas in `docs/open-api/users.yaml` and
`docs/open-api/groups.yaml`
**Tests**
- Add REST unit tests in `TestUserOperations` and `TestGroupOperations`
- Add client unit tests in `TestUserGroup`
- Add `AccessControlIT.testManageUsersAndGroupsWithExternalId` for
end-to-end create/read/duplicate-externalId flows
Fix: #11835
Part of epic #11830. Split from #11918.
### Why are the changes needed?
SCIM and external identity providers need to create users and groups
through the 8090 REST layer with stable external identifiers, without
breaking existing name-only create clients. Integration tests ensure the
client → REST → dispatcher create path works against a real Gravitino
server.
### Does this PR introduce _any_ user-facing change?
1. `POST /metalakes/{metalake}/users` accepts optional request fields:
- `externalId` (string)
- `enabled` (boolean, default `true` when `externalId` is provided and
`enabled` is omitted)
2. `POST /metalakes/{metalake}/groups` accepts optional request field:
- `externalId` (string)
3. Create responses include `externalId` (and `enabled` for users) when
provided
4. Java client adds `addUser(name, externalId, enabled)` and
`addGroup(name, externalId)`
Note: lookup/enable/disable/delete-by-external-id dispatcher APIs from
#11832/#11834 are not exposed on 8090 REST yet and are therefore not
covered by `AccessControlIT`.
### How was this patch tested?
```bash
export
JAVA_HOME=/Library/Java/JavaVirtualMachines/microsoft-17.jdk/Contents/Home
./gradlew spotlessApply
./gradlew :server:test \
--tests "org.apache.gravitino.server.web.rest.TestUserOperations" \
--tests "org.apache.gravitino.server.web.rest.TestGroupOperations" \
-PskipITs
./gradlew :clients:client-java:test \
--tests "org.apache.gravitino.client.TestUserGroup" \
-PskipITs
./gradlew :clients:client-java:test \
--tests
"org.apache.gravitino.client.integration.test.authorization.AccessControlIT.testManageUsersAndGroupsWithExternalId"
./gradlew :docs:build
```
---------
Co-authored-by: Cursor <[email protected]>
---
.../apache/gravitino/client/GravitinoClient.java | 32 ++++++++++++
.../apache/gravitino/client/GravitinoMetalake.java | 58 ++++++++++++++++++++++
.../org/apache/gravitino/client/TestUserGroup.java | 46 +++++++++++++++++
.../test/authorization/AccessControlIT.java | 45 +++++++++++++++++
.../gravitino/dto/requests/GroupAddRequest.java | 22 +++++++-
.../gravitino/dto/requests/UserAddRequest.java | 28 ++++++++++-
docs/open-api/groups.yaml | 9 +++-
docs/open-api/users.yaml | 18 ++++++-
docs/security/access-control.md | 42 +++++++++++++---
.../gravitino/server/web/rest/GroupOperations.java | 12 +++--
.../gravitino/server/web/rest/UserOperations.java | 15 ++++--
.../server/web/rest/TestGroupOperations.java | 32 ++++++++++++
.../server/web/rest/TestUserOperations.java | 34 +++++++++++++
13 files changed, 373 insertions(+), 20 deletions(-)
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java
index c7e19ba73c..16b222ba8d 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java
@@ -177,6 +177,22 @@ public class GravitinoClient extends GravitinoClientBase
return getMetalake().addUser(user);
}
+ /**
+ * Adds a new User with an external identifier.
+ *
+ * @param user The name of the User.
+ * @param externalId The external identifier of the User.
+ * @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.
+ */
+ public User addUser(String user, String externalId, boolean enabled)
+ throws UserAlreadyExistsException, NoSuchMetalakeException {
+ return getMetalake().addUser(user, externalId, enabled);
+ }
+
/**
* Removes a User.
*
@@ -236,6 +252,22 @@ public class GravitinoClient extends GravitinoClientBase
return getMetalake().addGroup(group);
}
+ /**
+ * Adds a new Group with an external identifier.
+ *
+ * @param group The name of the Group.
+ * @param externalId The external identifier of the Group.
+ * @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.
+ */
+ public Group addGroup(String group, String externalId)
+ throws GroupAlreadyExistsException, NoSuchMetalakeException {
+ return getMetalake().addGroup(group, externalId);
+ }
+
/**
* Removes a Group.
*
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java
index 2be2b88d92..f88a33a227 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java
@@ -784,6 +784,35 @@ public class GravitinoMetalake extends MetalakeDTO
return resp.getUser();
}
+ /**
+ * Adds a new User with an external identifier.
+ *
+ * @param user The name of the User.
+ * @param externalId The external identifier of the User.
+ * @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.
+ */
+ public User addUser(String user, String externalId, boolean enabled)
+ throws UserAlreadyExistsException, NoSuchMetalakeException {
+ UserAddRequest req = new UserAddRequest(user, externalId, enabled);
+ req.validate();
+
+ UserResponse resp =
+ restClient.post(
+ String.format(
+ API_METALAKES_USERS_PATH, RESTUtils.encodeString(this.name()),
BLANK_PLACEHOLDER),
+ req,
+ UserResponse.class,
+ Collections.emptyMap(),
+ ErrorHandlers.userErrorHandler());
+ resp.validate();
+
+ return resp.getUser();
+ }
+
/**
* Removes a User.
*
@@ -900,6 +929,35 @@ public class GravitinoMetalake extends MetalakeDTO
return resp.getGroup();
}
+ /**
+ * Adds a new Group with an external identifier.
+ *
+ * @param group The name of the Group.
+ * @param externalId The external identifier of the Group.
+ * @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.
+ */
+ public Group addGroup(String group, String externalId)
+ throws GroupAlreadyExistsException, NoSuchMetalakeException {
+ GroupAddRequest req = new GroupAddRequest(group, externalId);
+ req.validate();
+
+ GroupResponse resp =
+ restClient.post(
+ String.format(
+ API_METALAKES_GROUPS_PATH,
RESTUtils.encodeString(this.name()), BLANK_PLACEHOLDER),
+ req,
+ GroupResponse.class,
+ Collections.emptyMap(),
+ ErrorHandlers.groupErrorHandler());
+ resp.validate();
+
+ return resp.getGroup();
+ }
+
/**
* Removes a Group.
*
diff --git
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestUserGroup.java
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestUserGroup.java
index ff98b2ca6c..924acb9cf0 100644
---
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestUserGroup.java
+++
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestUserGroup.java
@@ -125,6 +125,30 @@ public class TestUserGroup extends TestBase {
RuntimeException.class, () -> gravitinoClient.addUser(username),
"internal error");
}
+ @Test
+ public void testAddUserWithExternalId() throws Exception {
+ String username = "user";
+ String externalId = "ext-user-1";
+ String userPath = withSlash(String.format(API_METALAKES_USERS_PATH,
metalakeName, ""));
+ UserAddRequest request = new UserAddRequest(username, externalId, false);
+
+ UserDTO mockUser =
+ UserDTO.builder()
+ .withName(username)
+ .withExternalId(externalId)
+ .withEnabled(false)
+ .withAudit(
+
AuditDTO.builder().withCreator("creator").withCreateTime(Instant.now()).build())
+ .build();
+ UserResponse userResponse = new UserResponse(mockUser);
+ buildMockResource(Method.POST, userPath, request, userResponse, SC_OK);
+
+ User addedUser = gravitinoClient.addUser(username, externalId, false);
+ Assertions.assertNotNull(addedUser);
+ Assertions.assertEquals(externalId, addedUser.externalId());
+ Assertions.assertFalse(addedUser.enabled());
+ }
+
@Test
public void testGetUsers() throws Exception {
String username = "user";
@@ -272,6 +296,28 @@ public class TestUserGroup extends TestBase {
RuntimeException.class, () -> gravitinoClient.addGroup(groupName),
"internal error");
}
+ @Test
+ public void testAddGroupWithExternalId() throws Exception {
+ String groupName = "group";
+ String externalId = "ext-group-1";
+ String groupPath = withSlash(String.format(API_METALAKES_GROUPS_PATH,
metalakeName, ""));
+ GroupAddRequest request = new GroupAddRequest(groupName, externalId);
+
+ GroupDTO mockGroup =
+ GroupDTO.builder()
+ .withName(groupName)
+ .withExternalId(externalId)
+ .withAudit(
+
AuditDTO.builder().withCreator("creator").withCreateTime(Instant.now()).build())
+ .build();
+ GroupResponse groupResponse = new GroupResponse(mockGroup);
+ buildMockResource(Method.POST, groupPath, request, groupResponse, SC_OK);
+
+ Group addedGroup = gravitinoClient.addGroup(groupName, externalId);
+ Assertions.assertNotNull(addedGroup);
+ Assertions.assertEquals(externalId, addedGroup.externalId());
+ }
+
@Test
public void testGetGroups() throws Exception {
String groupName = "group";
diff --git
a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/AccessControlIT.java
b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/AccessControlIT.java
index dfcb0f8516..e5321c59c0 100644
---
a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/AccessControlIT.java
+++
b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/AccessControlIT.java
@@ -202,6 +202,51 @@ public class AccessControlIT extends BaseIT {
metalake.deleteRole("role2");
}
+ @Test
+ void testManageUsersAndGroupsWithExternalId() {
+ String userExtId = "ext-user-it-1";
+ String username = "ext_user_it";
+ User user = metalake.addUser(username, userExtId, false);
+ Assertions.assertEquals(userExtId, user.externalId());
+ Assertions.assertFalse(user.enabled());
+
+ user = metalake.getUser(username);
+ Assertions.assertEquals(userExtId, user.externalId());
+ Assertions.assertFalse(user.enabled());
+
+ User listedUser =
+ Arrays.stream(metalake.listUsers())
+ .filter(u -> username.equals(u.name()))
+ .findFirst()
+ .orElseThrow();
+ Assertions.assertEquals(userExtId, listedUser.externalId());
+ Assertions.assertFalse(listedUser.enabled());
+
+ Assertions.assertThrows(
+ UserAlreadyExistsException.class, () ->
metalake.addUser("dup_ext_user", userExtId, true));
+
+ String groupExtId = "ext-group-it-1";
+ String groupName = "ext_group_it";
+ Group group = metalake.addGroup(groupName, groupExtId);
+ Assertions.assertEquals(groupExtId, group.externalId());
+
+ group = metalake.getGroup(groupName);
+ Assertions.assertEquals(groupExtId, group.externalId());
+
+ Group listedGroup =
+ Arrays.stream(metalake.listGroups())
+ .filter(g -> groupName.equals(g.name()))
+ .findFirst()
+ .orElseThrow();
+ Assertions.assertEquals(groupExtId, listedGroup.externalId());
+
+ Assertions.assertThrows(
+ GroupAlreadyExistsException.class, () ->
metalake.addGroup("dup_ext_group", groupExtId));
+
+ Assertions.assertTrue(metalake.removeUser(username));
+ Assertions.assertTrue(metalake.removeGroup(groupName));
+ }
+
@Test
@SuppressWarnings("deprecation")
void testManageRoles() {
diff --git
a/common/src/main/java/org/apache/gravitino/dto/requests/GroupAddRequest.java
b/common/src/main/java/org/apache/gravitino/dto/requests/GroupAddRequest.java
index e389332888..892cacb6e3 100644
---
a/common/src/main/java/org/apache/gravitino/dto/requests/GroupAddRequest.java
+++
b/common/src/main/java/org/apache/gravitino/dto/requests/GroupAddRequest.java
@@ -20,6 +20,7 @@ package org.apache.gravitino.dto.requests;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Preconditions;
+import javax.annotation.Nullable;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@@ -39,9 +40,13 @@ public class GroupAddRequest implements RESTRequest {
@JsonProperty("name")
private final String name;
+ @Nullable
+ @JsonProperty("externalId")
+ private final String externalId;
+
/** Default constructor for GroupAddRequest. (Used for Jackson
deserialization.) */
public GroupAddRequest() {
- this(null);
+ this(null, null);
}
/**
@@ -50,8 +55,19 @@ public class GroupAddRequest implements RESTRequest {
* @param name The name of the group.
*/
public GroupAddRequest(String name) {
+ this(name, null);
+ }
+
+ /**
+ * Creates a new GroupAddRequest.
+ *
+ * @param name The name of the group.
+ * @param externalId The external identifier of the group.
+ */
+ public GroupAddRequest(String name, String externalId) {
super();
this.name = name;
+ this.externalId = externalId;
}
/**
@@ -63,5 +79,9 @@ public class GroupAddRequest implements RESTRequest {
public void validate() throws IllegalArgumentException {
Preconditions.checkArgument(
StringUtils.isNotBlank(name), "\"name\" field is required and cannot
be empty");
+ if (externalId != null) {
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(externalId), "\"externalId\" field cannot be
blank when provided");
+ }
}
}
diff --git
a/common/src/main/java/org/apache/gravitino/dto/requests/UserAddRequest.java
b/common/src/main/java/org/apache/gravitino/dto/requests/UserAddRequest.java
index 6d5fb03b18..688f2a00cb 100644
--- a/common/src/main/java/org/apache/gravitino/dto/requests/UserAddRequest.java
+++ b/common/src/main/java/org/apache/gravitino/dto/requests/UserAddRequest.java
@@ -20,6 +20,7 @@ package org.apache.gravitino.dto.requests;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Preconditions;
+import javax.annotation.Nullable;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@@ -39,9 +40,17 @@ public class UserAddRequest implements RESTRequest {
@JsonProperty("name")
private final String name;
+ @Nullable
+ @JsonProperty("externalId")
+ private final String externalId;
+
+ @Nullable
+ @JsonProperty("enabled")
+ private final Boolean enabled;
+
/** Default constructor for UserAddRequest. (Used for Jackson
deserialization.) */
public UserAddRequest() {
- this(null);
+ this(null, null, null);
}
/**
@@ -50,8 +59,21 @@ public class UserAddRequest implements RESTRequest {
* @param name The name of the user.
*/
public UserAddRequest(String name) {
+ this(name, null, null);
+ }
+
+ /**
+ * Creates a new UserAddRequest.
+ *
+ * @param name The name of the user.
+ * @param externalId The external identifier of the user.
+ * @param enabled Whether the user is enabled.
+ */
+ public UserAddRequest(String name, String externalId, Boolean enabled) {
super();
this.name = name;
+ this.externalId = externalId;
+ this.enabled = enabled;
}
/**
@@ -63,5 +85,9 @@ public class UserAddRequest implements RESTRequest {
public void validate() throws IllegalArgumentException {
Preconditions.checkArgument(
StringUtils.isNotBlank(name), "\"name\" field is required and cannot
be empty");
+ if (externalId != null) {
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(externalId), "\"externalId\" field cannot be
blank when provided");
+ }
}
}
diff --git a/docs/open-api/groups.yaml b/docs/open-api/groups.yaml
index e2e8108daf..464bd6b3ad 100644
--- a/docs/open-api/groups.yaml
+++ b/docs/open-api/groups.yaml
@@ -163,6 +163,9 @@ components:
name:
type: string
description: The name of the group
+ externalId:
+ type: string
+ description: The external identifier of the group
roles:
type: array
items:
@@ -179,6 +182,9 @@ components:
name:
type: string
description: The name of the group
+ externalId:
+ type: string
+ description: The external identifier of the group
responses:
GroupResponse:
@@ -211,7 +217,8 @@ components:
examples:
GroupAddRequest:
value: {
- "name": "group1",
+ "name": "group",
+ "externalId": "ext-group"
}
NameListResponse:
diff --git a/docs/open-api/users.yaml b/docs/open-api/users.yaml
index 40e3a25cb0..39974c0fd6 100644
--- a/docs/open-api/users.yaml
+++ b/docs/open-api/users.yaml
@@ -163,6 +163,13 @@ components:
name:
type: string
description: The name of the user
+ externalId:
+ type: string
+ description: The external identifier of the user
+ enabled:
+ type: boolean
+ description: Whether the user is enabled
+ default: true
roles:
type: array
items:
@@ -179,6 +186,13 @@ components:
name:
type: string
description: The name of the user
+ externalId:
+ type: string
+ description: The external identifier of the user
+ enabled:
+ type: boolean
+ description: Whether the user is enabled
+ default: true
responses:
UserResponse:
@@ -211,7 +225,9 @@ components:
examples:
UserAddRequest:
value: {
- "name": "user1",
+ "name": "user",
+ "externalId": "ext-user",
+ "enabled": true
}
NameListResponse:
diff --git a/docs/security/access-control.md b/docs/security/access-control.md
index e3bf34ef9c..5671932ab9 100644
--- a/docs/security/access-control.md
+++ b/docs/security/access-control.md
@@ -160,6 +160,8 @@ A user represents an individual identity in Gravitino.
Users can be:
- Granted one or more roles
- Given different operating privileges based on their assigned roles
- Made owners of securable objects
+- Correlated with an external identity provider through an optional
`externalId`
+- Enabled or disabled through the optional `enabled` flag (`true` by default
when creating with `externalId`)
### Group
@@ -167,6 +169,7 @@ A group is a collection of users that simplifies permission
management by allowi
- Grant permissions to multiple users at once
- Manage access control for teams or departments
- Assign roles that all group members will inherit
+- Correlated with an external identity provider through an optional
`externalId`
All users in a group inherit the roles and privileges granted to that group.
@@ -574,7 +577,9 @@ The following sections demonstrate how to perform common
access control operatio
### Add a User
-Add a user to your metalake before using authorization features.
+Add a user to your metalake before using authorization features. The request
body requires `name` and
+optionally accepts `externalId` and `enabled`. When `externalId` is omitted,
Gravitino creates the user
+with the legacy name-only path. When `externalId` is provided, `enabled`
defaults to `true` if omitted.
<Tabs groupId='language' queryString>
<TabItem value="shell" label="Shell">
@@ -582,7 +587,14 @@ Add a user to your metalake before using authorization
features.
```shell
curl -X POST -H "Accept: application/vnd.gravitino.v1+json" \
-H "Content-Type: application/json" -d '{
- "name": "user1"
+ "name": "user"
+}' http://localhost:8090/api/metalakes/test/users
+
+curl -X POST -H "Accept: application/vnd.gravitino.v1+json" \
+-H "Content-Type: application/json" -d '{
+ "name": "user",
+ "externalId": "ext-user",
+ "enabled": true
}' http://localhost:8090/api/metalakes/test/users
```
@@ -592,7 +604,10 @@ curl -X POST -H "Accept:
application/vnd.gravitino.v1+json" \
```java
GravitinoClient client = ...
User user =
- client.addUser("user1");
+ client.addUser("user");
+
+User externalUser =
+ client.addUser("user", "ext-user", true);
```
</TabItem>
@@ -628,7 +643,7 @@ User[] users = client.listUsers();
### Get a User
-Get a user by its name.
+Get a user by its name. The response includes `externalId` and `enabled` when
they are set on the user.
<Tabs groupId='language' queryString>
<TabItem value="shell" label="Shell">
@@ -678,7 +693,9 @@ boolean removed =
### Add a Group
-Add the group to your metalake before you use the authorization.
+Add the group to your metalake before you use the authorization. The request
body requires `name` and
+optionally accepts `externalId`. When `externalId` is omitted, Gravitino
creates the group with the
+legacy name-only path.
<Tabs groupId='language' queryString>
<TabItem value="shell" label="Shell">
@@ -686,7 +703,13 @@ Add the group to your metalake before you use the
authorization.
```shell
curl -X POST -H "Accept: application/vnd.gravitino.v1+json" \
-H "Content-Type: application/json" -d '{
- "name": "group1"
+ "name": "group"
+}' http://localhost:8090/api/metalakes/test/groups
+
+curl -X POST -H "Accept: application/vnd.gravitino.v1+json" \
+-H "Content-Type: application/json" -d '{
+ "name": "group",
+ "externalId": "ext-group"
}' http://localhost:8090/api/metalakes/test/groups
```
@@ -696,7 +719,10 @@ curl -X POST -H "Accept:
application/vnd.gravitino.v1+json" \
```java
GravitinoClient client = ...
Group group =
- client.addGroup("group1");
+ client.addGroup("group");
+
+Group externalGroup =
+ client.addGroup("group", "ext-group");
```
</TabItem>
@@ -733,7 +759,7 @@ User[] users = client.listGroups();
### Get a Group
-Get a group by its name.
+Get a group by its name. The response includes `externalId` when it is set on
the group.
<Tabs groupId='language' queryString>
<TabItem value="shell" label="Shell">
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/rest/GroupOperations.java
b/server/src/main/java/org/apache/gravitino/server/web/rest/GroupOperations.java
index eea20c3ccb..5cc083c7f7 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/rest/GroupOperations.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/rest/GroupOperations.java
@@ -31,12 +31,14 @@ import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
+import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.Entity;
import org.apache.gravitino.GravitinoEnv;
import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.MetadataObjects;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.authorization.AccessControlDispatcher;
+import org.apache.gravitino.authorization.Group;
import org.apache.gravitino.authorization.Owner;
import org.apache.gravitino.authorization.OwnerDispatcher;
import org.apache.gravitino.dto.requests.GroupAddRequest;
@@ -109,10 +111,12 @@ public class GroupOperations {
() -> {
request.validate();
MetalakeManager.checkMetalakeInUse(metalake);
- return Utils.ok(
- new GroupResponse(
- DTOConverters.toDTO(
- accessControlManager.addGroup(metalake,
request.getName()))));
+ Group addedGroup =
+ StringUtils.isNotBlank(request.getExternalId())
+ ? accessControlManager.addGroup(
+ metalake, request.getName(), request.getExternalId())
+ : accessControlManager.addGroup(metalake,
request.getName());
+ return Utils.ok(new
GroupResponse(DTOConverters.toDTO(addedGroup)));
});
} catch (Exception e) {
return ExceptionHandlers.handleGroupException(
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/rest/UserOperations.java
b/server/src/main/java/org/apache/gravitino/server/web/rest/UserOperations.java
index 5bce5e8fc9..57e21c844c 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/rest/UserOperations.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/rest/UserOperations.java
@@ -20,6 +20,7 @@ package org.apache.gravitino.server.web.rest;
import com.codahale.metrics.annotation.ResponseMetered;
import com.codahale.metrics.annotation.Timed;
+import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
@@ -31,6 +32,7 @@ import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
+import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.Entity;
import org.apache.gravitino.GravitinoEnv;
import org.apache.gravitino.MetadataObject;
@@ -159,10 +161,15 @@ public class UserOperations {
() -> {
request.validate();
MetalakeManager.checkMetalakeInUse(metalake);
- return Utils.ok(
- new UserResponse(
- DTOConverters.toDTO(
- accessControlManager.addUser(metalake,
request.getName()))));
+ User addedUser =
+ StringUtils.isNotBlank(request.getExternalId())
+ ? accessControlManager.addUser(
+ metalake,
+ request.getName(),
+ request.getExternalId(),
+ Optional.ofNullable(request.getEnabled()).orElse(true))
+ : accessControlManager.addUser(metalake,
request.getName());
+ return Utils.ok(new UserResponse(DTOConverters.toDTO(addedUser)));
});
} catch (Exception e) {
return ExceptionHandlers.handleUserException(
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestGroupOperations.java
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestGroupOperations.java
index 64e6095dc3..5fd1084b6c 100644
---
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestGroupOperations.java
+++
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestGroupOperations.java
@@ -210,6 +210,38 @@ public class TestGroupOperations extends
BaseOperationsTest {
Assertions.assertEquals(RuntimeException.class.getSimpleName(),
errorResponse2.getType());
}
+ @Test
+ public void testAddGroupWithExternalId() throws IOException {
+ GroupAddRequest req = new GroupAddRequest("group1", "ext-group-1");
+ Group group =
+ GroupEntity.builder()
+ .withId(1L)
+ .withName("group1")
+ .withExternalId("ext-group-1")
+ .withRoleNames(Collections.emptyList())
+ .withAuditInfo(
+
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build())
+ .build();
+
+ when(manager.addGroup(any(), any(), any())).thenReturn(group);
+
+ BaseMetalake metalake = mock(BaseMetalake.class);
+ PropertiesMetadata propertiesMetadata = mock(PropertiesMetadata.class);
+ when(propertiesMetadata.getOrDefault(any(), any())).thenReturn(true);
+ when(metalake.propertiesMetadata()).thenReturn(propertiesMetadata);
+ when(entityStore.get(any(), any(), any())).thenReturn(metalake);
+
+ Response resp =
+ target("/metalakes/metalake1/groups")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .post(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(Status.OK.getStatusCode(), resp.getStatus());
+ GroupResponse groupResponse = resp.readEntity(GroupResponse.class);
+ Assertions.assertEquals("ext-group-1",
groupResponse.getGroup().externalId());
+ }
+
@Test
public void testGetGroup() throws IOException {
Group group = buildGroup("group1");
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestUserOperations.java
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestUserOperations.java
index bb82a5dcdc..eac08e9175 100644
---
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestUserOperations.java
+++
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestUserOperations.java
@@ -210,6 +210,40 @@ public class TestUserOperations extends BaseOperationsTest
{
Assertions.assertEquals(RuntimeException.class.getSimpleName(),
errorResponse2.getType());
}
+ @Test
+ public void testAddUserWithExternalId() throws IOException {
+ UserAddRequest req = new UserAddRequest("user1", "ext-1", false);
+ User user =
+ UserEntity.builder()
+ .withId(1L)
+ .withName("user1")
+ .withExternalId("ext-1")
+ .withEnabled(false)
+ .withRoleNames(Collections.emptyList())
+ .withAuditInfo(
+
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build())
+ .build();
+
+ when(manager.addUser(any(), any(), any(),
any(Boolean.class))).thenReturn(user);
+
+ BaseMetalake metalake = mock(BaseMetalake.class);
+ PropertiesMetadata propertiesMetadata = mock(PropertiesMetadata.class);
+ when(propertiesMetadata.getOrDefault(any(), any())).thenReturn(true);
+ when(metalake.propertiesMetadata()).thenReturn(propertiesMetadata);
+ when(entityStore.get(any(), any(), any())).thenReturn(metalake);
+
+ Response resp =
+ target("/metalakes/metalake1/users")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .post(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp.getStatus());
+ UserResponse userResponse = resp.readEntity(UserResponse.class);
+ Assertions.assertEquals("ext-1", userResponse.getUser().externalId());
+ Assertions.assertFalse(userResponse.getUser().enabled());
+ }
+
@Test
public void testGetUser() throws IOException {