This is an automated email from the ASF dual-hosted git repository.
mchades 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 aff2c9259c [#10966] feat(idp-basic): Add built-in IdP logical layer
for user group management (#11209)
aff2c9259c is described below
commit aff2c9259c0c386496f4ca22e61b5dd113f4ac73
Author: MaSai <[email protected]>
AuthorDate: Tue May 26 11:34:08 2026 +0800
[#10966] feat(idp-basic): Add built-in IdP logical layer for user group
management (#11209)
### What changes were proposed in this pull request?
This PR adds the built-in IdP logical layer in the `idp-basic` plugin
for global user and group management:
- Introduce `IdpUser` / `IdpGroup` models and corresponding metadata
entities.
- Add `IdpStore` / `IdpEntityStore` / `IdpJDBCBackend` for relational
persistence.
- Add `IdpUserGroupManager` as the business layer for user CRUD, group
CRUD, membership changes, and password updates.
### Why are the changes needed?
This is the logical-layer subtask for built-in IdP support (#10959). It
provides the storage and management foundation required before exposing
REST APIs and DTOs in follow-up work.
Fix: #10966
### Does this PR introduce _any_ user-facing change?
No. This change is internal to the `idp-basic` plugin and does not
expose new public APIs yet.
### How was this patch tested?
- `./gradlew :plugins:idp-basic:test -PskipITs -PskipDockerTests=false`
---------
Co-authored-by: Cursor <[email protected]>
---
.../apache/gravitino/idp/IdpUserGroupManager.java | 191 +++++++++++++++++++
.../idp/exception/AlreadyExistsException.java | 49 +++++
.../org/apache/gravitino/idp/model/IdpGroup.java | 72 +++++++
.../org/apache/gravitino/idp/model/IdpUser.java | 72 +++++++
.../gravitino/idp/storage/IdpStorageBootstrap.java | 57 ------
.../idp/storage/gc/IdpLegacyGarbageCollector.java | 137 --------------
.../storage/relational/IdpGarbageCollector.java | 121 ++++++++++++
.../storage/relational/IdpRelationalStorage.java | 81 ++++++++
.../converters/IdpSQLExceptionConverter.java | 94 ++++++++++
.../IdpSQLExceptionConverterFactory.java | 84 +++++++++
.../relational/utils/IdpExceptionUtils.java | 44 +++++
.../idp/storage/service/IdpGroupMetaService.java | 141 ++++++++++----
.../idp/storage/service/IdpUserMetaService.java | 48 +++--
.../gravitino/idp/TestIdpUserGroupManager.java | 206 +++++++++++++++++++++
.../storage/mapper/AbstractIdpMetaStorageTest.java | 25 +++
.../TestIdpGarbageCollector.java} | 23 +--
.../storage/service/TestIdpGroupMetaService.java | 28 ++-
.../storage/service/TestIdpUserMetaService.java | 6 +-
18 files changed, 1202 insertions(+), 277 deletions(-)
diff --git
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/IdpUserGroupManager.java
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/IdpUserGroupManager.java
new file mode 100644
index 0000000000..38065951a4
--- /dev/null
+++
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/IdpUserGroupManager.java
@@ -0,0 +1,191 @@
+/*
+ * 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.idp;
+
+import com.google.common.base.Preconditions;
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.idp.basic.password.PasswordHasher;
+import org.apache.gravitino.idp.basic.password.PasswordHasherFactory;
+import org.apache.gravitino.idp.model.IdpGroup;
+import org.apache.gravitino.idp.model.IdpUser;
+import org.apache.gravitino.idp.storage.po.IdpGroupPO;
+import org.apache.gravitino.idp.storage.po.IdpUserPO;
+import org.apache.gravitino.idp.storage.relational.IdpGarbageCollector;
+import org.apache.gravitino.idp.storage.relational.IdpRelationalStorage;
+import org.apache.gravitino.idp.storage.service.IdpGroupMetaService;
+import org.apache.gravitino.idp.storage.service.IdpUserMetaService;
+import org.apache.gravitino.storage.IdGenerator;
+import org.apache.gravitino.storage.relational.utils.POConverters;
+
+/**
+ * Manager for built-in IdP users and groups. It mirrors {@link
+ * org.apache.gravitino.authorization.UserGroupManager} but operates on global
IdP metadata.
+ */
+public class IdpUserGroupManager implements Closeable {
+
+ private static final IdpUserMetaService USER_SERVICE =
IdpUserMetaService.getInstance();
+ private static final IdpGroupMetaService GROUP_SERVICE =
IdpGroupMetaService.getInstance();
+
+ private final IdpRelationalStorage relationalStorage;
+ private final IdGenerator idGenerator;
+ private final PasswordHasher passwordHasher;
+ private final IdpGarbageCollector garbageCollector;
+
+ /**
+ * Creates a built-in IdP user and group manager.
+ *
+ * @param config The server configuration.
+ * @param idGenerator The id generator.
+ */
+ public IdpUserGroupManager(Config config, IdGenerator idGenerator) {
+ this.relationalStorage = new IdpRelationalStorage(config);
+ this.idGenerator = idGenerator;
+ this.passwordHasher = PasswordHasherFactory.create();
+ this.garbageCollector = new IdpGarbageCollector(config);
+ garbageCollector.start();
+ }
+
+ /**
+ * Adds a built-in IdP user.
+ *
+ * @param username The username.
+ * @param password The plaintext password.
+ * @return The created built-in IdP user.
+ */
+ public IdpUser addUser(String username, String password) throws IOException {
+ USER_SERVICE.insertIdpUser(newUserPO(username,
passwordHasher.hash(password)));
+ return new IdpUser(username, Collections.emptyList());
+ }
+
+ /**
+ * Removes a built-in IdP user.
+ *
+ * @param username The username.
+ * @return True if the user was removed, false if it did not exist.
+ */
+ public boolean removeUser(String username) {
+ return USER_SERVICE.deleteIdpUser(username);
+ }
+
+ /**
+ * Gets a built-in IdP user.
+ *
+ * @param username The username.
+ * @return The built-in IdP user.
+ */
+ public IdpUser getUser(String username) {
+ IdpUserPO userPO = USER_SERVICE.getIdpUserByUsername(username);
+ return new IdpUser(userPO.getUsername(),
USER_SERVICE.listGroupNamesByUsername(username));
+ }
+
+ /**
+ * Changes the password for a built-in IdP user.
+ *
+ * @param username The username.
+ * @param password The new plaintext password.
+ * @return True if the password was updated, false if the user did not exist.
+ */
+ public boolean changePassword(String username, String password) {
+ return USER_SERVICE.updateIdpUserPassword(username,
passwordHasher.hash(password));
+ }
+
+ /**
+ * Adds a built-in IdP group.
+ *
+ * @param groupName The group name.
+ * @return The created built-in IdP group.
+ */
+ public IdpGroup addGroup(String groupName) throws IOException {
+ GROUP_SERVICE.insertIdpGroup(newGroupPO(groupName));
+ return new IdpGroup(groupName, Collections.emptyList());
+ }
+
+ /**
+ * Removes a built-in IdP group.
+ *
+ * @param groupName The group name.
+ * @param force Whether to force delete a non-empty group.
+ * @return True if the group was removed, false if it did not exist.
+ */
+ public boolean removeGroup(String groupName, boolean force) {
+ return GROUP_SERVICE.deleteIdpGroup(groupName, force);
+ }
+
+ /**
+ * Gets a built-in IdP group.
+ *
+ * @param groupName The group name.
+ * @return The built-in IdP group.
+ */
+ public IdpGroup getGroup(String groupName) {
+ IdpGroupPO groupPO = GROUP_SERVICE.getIdpGroupByName(groupName);
+ return new IdpGroup(groupPO.getGroupName(),
GROUP_SERVICE.listUsernamesByGroupName(groupName));
+ }
+
+ /**
+ * Changes built-in IdP group membership.
+ *
+ * @param groupName The group name.
+ * @param additions The usernames to add, or null if none.
+ * @param removals The usernames to remove, or null if none.
+ * @return The updated built-in IdP group.
+ */
+ public IdpGroup changeGroupMembership(
+ String groupName, @Nullable List<String> additions, @Nullable
List<String> removals) {
+ List<String> additionsList = additions == null ? Collections.emptyList() :
additions;
+ List<String> removalsList = removals == null ? Collections.emptyList() :
removals;
+ Preconditions.checkArgument(
+ !additionsList.isEmpty() || !removalsList.isEmpty(),
+ "additions and removals cannot both be empty");
+ GROUP_SERVICE.changeGroupMembership(groupName, additionsList,
removalsList);
+ return getGroup(groupName);
+ }
+
+ @Override
+ public void close() throws IOException {
+ garbageCollector.close();
+ relationalStorage.close();
+ }
+
+ private IdpUserPO newUserPO(String username, String passwordHash) {
+ return IdpUserPO.builder()
+ .withUserId(idGenerator.nextId())
+ .withUsername(username)
+ .withPasswordHash(passwordHash)
+ .withCurrentVersion(POConverters.INIT_VERSION)
+ .withLastVersion(POConverters.INIT_VERSION)
+ .withDeletedAt(POConverters.DEFAULT_DELETED_AT)
+ .build();
+ }
+
+ private IdpGroupPO newGroupPO(String groupName) {
+ return IdpGroupPO.builder()
+ .withGroupId(idGenerator.nextId())
+ .withGroupName(groupName)
+ .withCurrentVersion(POConverters.INIT_VERSION)
+ .withLastVersion(POConverters.INIT_VERSION)
+ .withDeletedAt(POConverters.DEFAULT_DELETED_AT)
+ .build();
+ }
+}
diff --git
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/exception/AlreadyExistsException.java
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/exception/AlreadyExistsException.java
new file mode 100644
index 0000000000..24f0ae6dde
--- /dev/null
+++
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/exception/AlreadyExistsException.java
@@ -0,0 +1,49 @@
+/*
+ * 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.idp.exception;
+
+import com.google.errorprone.annotations.FormatMethod;
+import com.google.errorprone.annotations.FormatString;
+
+/** Exception thrown when a built-in IdP entity already exists. */
+public class AlreadyExistsException extends RuntimeException {
+
+ /**
+ * Constructs a new exception with the specified detail message.
+ *
+ * @param message The detail message.
+ * @param args The arguments to the message.
+ */
+ @FormatMethod
+ public AlreadyExistsException(@FormatString String message, Object... args) {
+ super(String.format(message, args));
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message and cause.
+ *
+ * @param cause The cause.
+ * @param message The detail message.
+ * @param args The arguments to the message.
+ */
+ @FormatMethod
+ public AlreadyExistsException(Throwable cause, @FormatString String message,
Object... args) {
+ super(String.format(message, args), cause);
+ }
+}
diff --git
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/model/IdpGroup.java
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/model/IdpGroup.java
new file mode 100644
index 0000000000..50fddf00a1
--- /dev/null
+++
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/model/IdpGroup.java
@@ -0,0 +1,72 @@
+/*
+ * 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.idp.model;
+
+import java.util.List;
+import java.util.Objects;
+
+/** Built-in IdP group. */
+public class IdpGroup {
+
+ private final String name;
+ private final List<String> usernames;
+
+ /**
+ * Creates a built-in IdP group.
+ *
+ * @param name The group name.
+ * @param usernames The usernames in the group.
+ */
+ public IdpGroup(String name, List<String> usernames) {
+ this.name = name;
+ this.usernames = usernames;
+ }
+
+ /** Returns the group name. */
+ public String name() {
+ return name;
+ }
+
+ /** Returns the usernames in the group. */
+ public List<String> usernames() {
+ return usernames;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof IdpGroup)) {
+ return false;
+ }
+ IdpGroup that = (IdpGroup) other;
+ return Objects.equals(name, that.name) && Objects.equals(usernames,
that.usernames);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, usernames);
+ }
+
+ @Override
+ public String toString() {
+ return "IdpGroup{name='" + name + "', usernames=" + usernames + '}';
+ }
+}
diff --git
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/model/IdpUser.java
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/model/IdpUser.java
new file mode 100644
index 0000000000..aae49bd266
--- /dev/null
+++
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/model/IdpUser.java
@@ -0,0 +1,72 @@
+/*
+ * 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.idp.model;
+
+import java.util.List;
+import java.util.Objects;
+
+/** Built-in IdP user. */
+public class IdpUser {
+
+ private final String name;
+ private final List<String> groupNames;
+
+ /**
+ * Creates a built-in IdP user.
+ *
+ * @param name The username.
+ * @param groupNames The group names the user belongs to.
+ */
+ public IdpUser(String name, List<String> groupNames) {
+ this.name = name;
+ this.groupNames = groupNames;
+ }
+
+ /** Returns the username. */
+ public String name() {
+ return name;
+ }
+
+ /** Returns the group names the user belongs to. */
+ public List<String> groupNames() {
+ return groupNames;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof IdpUser)) {
+ return false;
+ }
+ IdpUser that = (IdpUser) other;
+ return Objects.equals(name, that.name) && Objects.equals(groupNames,
that.groupNames);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, groupNames);
+ }
+
+ @Override
+ public String toString() {
+ return "IdpUser{name='" + name + "', groupNames=" + groupNames + '}';
+ }
+}
diff --git
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/IdpStorageBootstrap.java
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/IdpStorageBootstrap.java
deleted file mode 100644
index 9d2caedd78..0000000000
---
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/IdpStorageBootstrap.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * 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.idp.storage;
-
-import java.util.concurrent.atomic.AtomicBoolean;
-import org.apache.gravitino.GravitinoEnv;
-import org.apache.gravitino.idp.storage.gc.IdpLegacyGarbageCollector;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * One-time initialization for built-in IdP storage components that are not
wired through core
- * entity-store lifecycle.
- *
- * <p>Not invoked from {@link
- *
org.apache.gravitino.idp.storage.mapper.provider.IdpBasicMapperPackageProvider}
in the current
- * PR; a future change can wire this through server/plugin lifecycle.
- */
-public final class IdpStorageBootstrap {
-
- private static final Logger LOG =
LoggerFactory.getLogger(IdpStorageBootstrap.class);
-
- private static final AtomicBoolean INITIALIZED = new AtomicBoolean(false);
-
- private IdpStorageBootstrap() {}
-
- /** Initializes IdP storage background tasks once per JVM. */
- public static void initializeOnce() {
- if (!INITIALIZED.compareAndSet(false, true)) {
- return;
- }
-
- try {
-
IdpLegacyGarbageCollector.startScheduledCollector(GravitinoEnv.getInstance().config());
- } catch (Exception e) {
- INITIALIZED.set(false);
- LOG.warn("Failed to initialize built-in IdP storage", e);
- }
- }
-}
diff --git
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/gc/IdpLegacyGarbageCollector.java
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/gc/IdpLegacyGarbageCollector.java
deleted file mode 100644
index 0898fe7b30..0000000000
---
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/gc/IdpLegacyGarbageCollector.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * 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.idp.storage.gc;
-
-import static
org.apache.gravitino.Configs.GARBAGE_COLLECTOR_SINGLE_DELETION_LIMIT;
-import static org.apache.gravitino.Configs.STORE_DELETE_AFTER_TIME;
-
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-import org.apache.gravitino.Config;
-import org.apache.gravitino.idp.storage.service.IdpGroupMetaService;
-import org.apache.gravitino.idp.storage.service.IdpUserMetaService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Periodically purges soft-deleted built-in IdP rows after {@link
- * org.apache.gravitino.Configs#STORE_DELETE_AFTER_TIME}.
- *
- * <p>Unlike core {@link
org.apache.gravitino.storage.relational.RelationalGarbageCollector}, which
- * is started from {@code RelationalEntityStore}, this plugin has no
entity-store lifecycle hook.
- * {@link org.apache.gravitino.idp.storage.IdpStorageBootstrap} can start it
once server wiring is
- * added; mapper registration alone does not start the collector.
- */
-public final class IdpLegacyGarbageCollector {
-
- private static final Logger LOG =
LoggerFactory.getLogger(IdpLegacyGarbageCollector.class);
-
- private static volatile IdpLegacyGarbageCollector instance;
-
- private final long storeDeleteAfterTimeMillis;
-
- private final ScheduledExecutorService garbageCollectorPool =
- new ScheduledThreadPoolExecutor(
- 2,
- r -> {
- Thread t = new Thread(r, "IdpBasic-Legacy-Garbage-Collector");
- t.setDaemon(true);
- return t;
- },
- new ThreadPoolExecutor.AbortPolicy());
-
- /**
- * Starts the scheduled legacy garbage collector. Idempotent; only the first
call takes effect.
- *
- * @param config Gravitino server configuration
- */
- public static void startScheduledCollector(Config config) {
- if (instance != null) {
- return;
- }
- synchronized (IdpLegacyGarbageCollector.class) {
- if (instance != null) {
- return;
- }
- IdpLegacyGarbageCollector collector = new
IdpLegacyGarbageCollector(config);
- collector.start();
- instance = collector;
- }
- }
-
- public IdpLegacyGarbageCollector(Config config) {
- storeDeleteAfterTimeMillis = config.get(STORE_DELETE_AFTER_TIME);
- }
-
- public void collectAndClean() {
- long threadId = Thread.currentThread().getId();
- LOG.debug("Thread {} start to collect garbage...", threadId);
-
- try {
- LOG.debug("Start to collect and delete legacy data by thread {}",
threadId);
- long legacyTimeline = System.currentTimeMillis() -
storeDeleteAfterTimeMillis;
- long deletedCount = Long.MAX_VALUE;
- LOG.debug(
- "Try to physically delete {} legacy data that has been marked
deleted before {}",
- "idp_user",
- legacyTimeline);
- try {
- while (deletedCount > 0) {
- deletedCount =
- IdpUserMetaService.getInstance()
- .deleteUserMetasByLegacyTimeline(
- legacyTimeline, GARBAGE_COLLECTOR_SINGLE_DELETION_LIMIT);
- }
- } catch (Exception e) {
- LOG.error("Failed to physically delete type of idp_user's legacy data:
", e);
- }
-
- deletedCount = Long.MAX_VALUE;
- LOG.debug(
- "Try to physically delete {} legacy data that has been marked
deleted before {}",
- "idp_group",
- legacyTimeline);
- try {
- while (deletedCount > 0) {
- deletedCount =
- IdpGroupMetaService.getInstance()
- .deleteGroupMetasByLegacyTimeline(
- legacyTimeline, GARBAGE_COLLECTOR_SINGLE_DELETION_LIMIT);
- }
- } catch (Exception e) {
- LOG.error("Failed to physically delete type of idp_group's legacy
data: ", e);
- }
- } catch (Exception e) {
- LOG.error("Thread {} failed to collect and clean garbage.", threadId, e);
- } finally {
- LOG.debug("Thread {} finish to collect garbage.", threadId);
- }
- }
-
- private void start() {
- long dateTimelineMinute = storeDeleteAfterTimeMillis / 1000 / 60;
-
- // We will collect garbage every 10 minutes at least. If the
dateTimelineMinute is larger than
- // 100 minutes, we would collect garbage every dateTimelineMinute/10
minutes.
- long frequency = Math.max(dateTimelineMinute / 10, 10);
- garbageCollectorPool.scheduleAtFixedRate(this::collectAndClean, 5,
frequency, TimeUnit.MINUTES);
- }
-}
diff --git
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/relational/IdpGarbageCollector.java
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/relational/IdpGarbageCollector.java
new file mode 100644
index 0000000000..861cea6ac0
--- /dev/null
+++
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/relational/IdpGarbageCollector.java
@@ -0,0 +1,121 @@
+/*
+ * 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.idp.storage.relational;
+
+import static
org.apache.gravitino.Configs.GARBAGE_COLLECTOR_SINGLE_DELETION_LIMIT;
+import static org.apache.gravitino.Configs.STORE_DELETE_AFTER_TIME;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.idp.storage.service.IdpGroupMetaService;
+import org.apache.gravitino.idp.storage.service.IdpUserMetaService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Garbage collector for built-in IdP metadata. */
+public final class IdpGarbageCollector implements Closeable {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(IdpGarbageCollector.class);
+
+ private static final IdpUserMetaService USER_SERVICE =
IdpUserMetaService.getInstance();
+ private static final IdpGroupMetaService GROUP_SERVICE =
IdpGroupMetaService.getInstance();
+
+ private final long storeDeleteAfterTimeMillis;
+
+ private final ScheduledExecutorService garbageCollectorPool =
+ new ScheduledThreadPoolExecutor(
+ 2,
+ r -> {
+ Thread t = new Thread(r, "Idp-Garbage-Collector");
+ t.setDaemon(true);
+ return t;
+ },
+ new ThreadPoolExecutor.AbortPolicy());
+
+ /**
+ * Creates a garbage collector for built-in IdP metadata.
+ *
+ * @param config The server configuration.
+ */
+ public IdpGarbageCollector(Config config) {
+ storeDeleteAfterTimeMillis = config.get(STORE_DELETE_AFTER_TIME);
+ }
+
+ /** Starts the scheduled garbage collector. */
+ public void start() {
+ long dateTimelineMinute = storeDeleteAfterTimeMillis / 1000 / 60;
+ long frequency = Math.max(dateTimelineMinute / 10, 10);
+ garbageCollectorPool.scheduleAtFixedRate(this::collectAndClean, 5,
frequency, TimeUnit.MINUTES);
+ }
+
+ void collectAndClean() {
+ long threadId = Thread.currentThread().getId();
+ LOG.debug("Thread {} start to collect built-in IdP garbage...", threadId);
+
+ try {
+ long legacyTimeline = System.currentTimeMillis() -
storeDeleteAfterTimeMillis;
+ purgeLegacyData(
+ () -> USER_SERVICE.deleteUserMetasByLegacyTimeline(legacyTimeline,
deletionLimit()));
+ purgeLegacyData(
+ () -> GROUP_SERVICE.deleteGroupMetasByLegacyTimeline(legacyTimeline,
deletionLimit()));
+ } catch (Exception e) {
+ LOG.error("Thread {} failed to collect and clean built-in IdP garbage.",
threadId, e);
+ } finally {
+ LOG.debug("Thread {} finish to collect built-in IdP garbage.", threadId);
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ garbageCollectorPool.shutdown();
+ try {
+ if (!garbageCollectorPool.awaitTermination(5, TimeUnit.SECONDS)) {
+ garbageCollectorPool.shutdownNow();
+ }
+ } catch (InterruptedException ex) {
+ garbageCollectorPool.shutdownNow();
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ private static int deletionLimit() {
+ return GARBAGE_COLLECTOR_SINGLE_DELETION_LIMIT;
+ }
+
+ private static void purgeLegacyData(LegacyDataDeleter deleter) {
+ long deletedCount = Long.MAX_VALUE;
+ try {
+ while (deletedCount > 0) {
+ deletedCount = deleter.delete();
+ }
+ } catch (RuntimeException e) {
+ LOG.error("Failed to physically delete built-in IdP legacy data", e);
+ }
+ }
+
+ @FunctionalInterface
+ private interface LegacyDataDeleter {
+ int delete();
+ }
+}
diff --git
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/relational/IdpRelationalStorage.java
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/relational/IdpRelationalStorage.java
new file mode 100644
index 0000000000..57d87a6ec7
--- /dev/null
+++
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/relational/IdpRelationalStorage.java
@@ -0,0 +1,81 @@
+/*
+ * 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.idp.storage.relational;
+
+import com.google.common.collect.ImmutableMap;
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Map;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.Configs;
+import
org.apache.gravitino.idp.storage.relational.converters.IdpSQLExceptionConverterFactory;
+import org.apache.gravitino.storage.relational.JDBCBackend.JDBCBackendType;
+import org.apache.gravitino.storage.relational.JDBCDatabase;
+import org.apache.gravitino.storage.relational.database.H2Database;
+import org.apache.gravitino.storage.relational.session.SqlSessionFactoryHelper;
+
+/** JDBC bootstrap for built-in IdP relational storage. */
+public final class IdpRelationalStorage implements Closeable {
+
+ private static final Map<JDBCBackendType, String> EMBEDDED_JDBC_DATABASE_MAP
=
+ ImmutableMap.of(JDBCBackendType.H2, H2Database.class.getCanonicalName());
+
+ private JDBCDatabase jdbcDatabase;
+
+ /**
+ * Initializes the JDBC session factory and optional embedded database.
+ *
+ * @param config The server configuration.
+ */
+ public IdpRelationalStorage(Config config) {
+ jdbcDatabase = startEmbeddedDatabaseIfNecessary(config);
+ SqlSessionFactoryHelper.getInstance().init(config);
+ IdpSQLExceptionConverterFactory.initConverter(config);
+ }
+
+ @Override
+ public void close() throws IOException {
+ SqlSessionFactoryHelper.getInstance().close();
+ IdpSQLExceptionConverterFactory.close();
+ if (jdbcDatabase != null) {
+ jdbcDatabase.close();
+ jdbcDatabase = null;
+ }
+ }
+
+ private JDBCDatabase startEmbeddedDatabaseIfNecessary(Config config) {
+ String jdbcUrl = config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL);
+ JDBCBackendType jdbcBackendType = JDBCBackendType.fromURI(jdbcUrl);
+ if (jdbcBackendType != JDBCBackendType.H2) {
+ return null;
+ }
+
+ try {
+ JDBCDatabase database =
+ (JDBCDatabase)
+ Class.forName(EMBEDDED_JDBC_DATABASE_MAP.get(jdbcBackendType))
+ .getDeclaredConstructor()
+ .newInstance();
+ database.initialize(config);
+ return database;
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to create and initialize IdP JDBC
backend.", e);
+ }
+ }
+}
diff --git
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/relational/converters/IdpSQLExceptionConverter.java
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/relational/converters/IdpSQLExceptionConverter.java
new file mode 100644
index 0000000000..89721f0eab
--- /dev/null
+++
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/relational/converters/IdpSQLExceptionConverter.java
@@ -0,0 +1,94 @@
+/*
+ * 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.idp.storage.relational.converters;
+
+import java.io.IOException;
+import java.sql.SQLException;
+import org.apache.gravitino.idp.exception.AlreadyExistsException;
+
+/** Converts JDBC SQL exceptions to built-in IdP exceptions. */
+public final class IdpSQLExceptionConverter {
+
+ /** MySQL duplicate entry error code. */
+ private static final int MYSQL_DUPLICATE_ENTRY_ERROR_CODE = 1062;
+
+ /** H2 duplicate entry error code. */
+ private static final int H2_DUPLICATE_ENTRY_ERROR_CODE = 23505;
+
+ /** PostgreSQL duplicate entry SQL state. */
+ private static final String POSTGRESQL_DUPLICATE_ENTRY_SQL_STATE = "23505";
+
+ /** Supported JDBC backend types for IdP relational storage. */
+ public enum JdbcType {
+ MYSQL,
+ H2,
+ POSTGRESQL
+ }
+
+ private final JdbcType jdbcType;
+
+ /**
+ * Creates a converter for the given JDBC backend type.
+ *
+ * @param jdbcType The JDBC backend type.
+ */
+ public IdpSQLExceptionConverter(JdbcType jdbcType) {
+ this.jdbcType = jdbcType;
+ }
+
+ /**
+ * Convert JDBC exception to IdP exception.
+ *
+ * @param sqlException The sql exception to map
+ * @param resourceType The resource type, for example {@code user} or {@code
group}
+ * @param name The name of the resource
+ * @throws IOException if an I/O error occurs during exception conversion
+ */
+ @SuppressWarnings("FormatStringAnnotation")
+ public void toIdpException(SQLException sqlException, String resourceType,
String name)
+ throws IOException {
+ if (isDuplicateEntry(sqlException)) {
+ throw new AlreadyExistsException(
+ sqlException, "IdP %s %s already exists", resourceType, name);
+ }
+ throw toIOException(sqlException);
+ }
+
+ private boolean isDuplicateEntry(SQLException sqlException) {
+ switch (jdbcType) {
+ case MYSQL:
+ return sqlException.getErrorCode() == MYSQL_DUPLICATE_ENTRY_ERROR_CODE;
+ case H2:
+ // Same as core H2ExceptionConverter: H2 in MySQL mode may report 1062
or 23505.
+ return sqlException.getErrorCode() == H2_DUPLICATE_ENTRY_ERROR_CODE
+ || sqlException.getErrorCode() == MYSQL_DUPLICATE_ENTRY_ERROR_CODE;
+ case POSTGRESQL:
+ return
POSTGRESQL_DUPLICATE_ENTRY_SQL_STATE.equals(sqlException.getSQLState());
+ default:
+ throw new IllegalStateException("Unsupported JDBC type: " + jdbcType);
+ }
+ }
+
+ private IOException toIOException(SQLException sqlException) {
+ if (jdbcType == JdbcType.H2) {
+ return new IOException("error code: " + sqlException.getErrorCode(),
sqlException);
+ }
+ return new IOException(sqlException);
+ }
+}
diff --git
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/relational/converters/IdpSQLExceptionConverterFactory.java
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/relational/converters/IdpSQLExceptionConverterFactory.java
new file mode 100644
index 0000000000..2f6a6dba42
--- /dev/null
+++
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/relational/converters/IdpSQLExceptionConverterFactory.java
@@ -0,0 +1,84 @@
+/*
+ * 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.idp.storage.relational.converters;
+
+import com.google.common.base.Preconditions;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.Configs;
+
+/** Factory for built-in IdP JDBC SQL exception converters. */
+public class IdpSQLExceptionConverterFactory {
+ private static final Pattern TYPE_PATTERN = Pattern.compile("jdbc:(\\w+):");
+ private static volatile IdpSQLExceptionConverter converter;
+
+ private IdpSQLExceptionConverterFactory() {}
+
+ /**
+ * Initializes the SQL exception converter from the JDBC backend URL in
config.
+ *
+ * @param config The server configuration.
+ */
+ public static synchronized void initConverter(Config config) {
+ if (converter == null) {
+ String jdbcUrl = config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL);
+ converter = new IdpSQLExceptionConverter(parseJdbcType(jdbcUrl));
+ }
+ }
+
+ /**
+ * Returns the initialized SQL exception converter.
+ *
+ * @return The SQL exception converter.
+ */
+ public static IdpSQLExceptionConverter getConverter() {
+ Preconditions.checkState(converter != null, "Exception converter is not
initialized.");
+ return converter;
+ }
+
+ /** Closes and resets the SQL exception converter. */
+ public static void close() {
+ if (converter != null) {
+ synchronized (IdpSQLExceptionConverterFactory.class) {
+ if (converter != null) {
+ converter = null;
+ }
+ }
+ }
+ }
+
+ private static IdpSQLExceptionConverter.JdbcType parseJdbcType(String
jdbcUrl) {
+ Matcher typeMatcher = TYPE_PATTERN.matcher(jdbcUrl);
+ if (!typeMatcher.find()) {
+ throw new IllegalArgumentException(
+ String.format("Cannot find jdbc type in jdbc url: %s", jdbcUrl));
+ }
+
+ String jdbcType = typeMatcher.group(1);
+ if (jdbcType.equalsIgnoreCase("mysql")) {
+ return IdpSQLExceptionConverter.JdbcType.MYSQL;
+ } else if (jdbcType.equalsIgnoreCase("h2")) {
+ return IdpSQLExceptionConverter.JdbcType.H2;
+ } else if (jdbcType.equalsIgnoreCase("postgresql")) {
+ return IdpSQLExceptionConverter.JdbcType.POSTGRESQL;
+ }
+ throw new IllegalArgumentException(String.format("Unsupported jdbc type:
%s", jdbcType));
+ }
+}
diff --git
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/relational/utils/IdpExceptionUtils.java
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/relational/utils/IdpExceptionUtils.java
new file mode 100644
index 0000000000..b831786e39
--- /dev/null
+++
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/relational/utils/IdpExceptionUtils.java
@@ -0,0 +1,44 @@
+/*
+ * 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.idp.storage.relational.utils;
+
+import java.io.IOException;
+import java.sql.SQLException;
+import
org.apache.gravitino.idp.storage.relational.converters.IdpSQLExceptionConverterFactory;
+
+/** Utilities for translating JDBC exceptions in built-in IdP storage. */
+public class IdpExceptionUtils {
+ private IdpExceptionUtils() {}
+
+ /**
+ * Converts JDBC SQL exceptions into built-in IdP exceptions when possible.
+ *
+ * @param re The runtime exception thrown by JDBC/MyBatis.
+ * @param resourceType The resource type, for example {@code user} or {@code
group}
+ * @param name The resource name.
+ * @throws IOException if the SQL exception cannot be mapped to an IdP
exception.
+ */
+ public static void checkSQLException(RuntimeException re, String
resourceType, String name)
+ throws IOException {
+ if (re.getCause() instanceof SQLException) {
+ IdpSQLExceptionConverterFactory.getConverter()
+ .toIdpException((SQLException) re.getCause(), resourceType, name);
+ }
+ }
+}
diff --git
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/service/IdpGroupMetaService.java
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/service/IdpGroupMetaService.java
index ce51423775..09313581f9 100644
---
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/service/IdpGroupMetaService.java
+++
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/service/IdpGroupMetaService.java
@@ -20,14 +20,19 @@ package org.apache.gravitino.idp.storage.service;
import static
org.apache.gravitino.metrics.source.MetricsSource.GRAVITINO_RELATIONAL_STORE_METRIC_NAME;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import org.apache.gravitino.idp.exception.NotFoundException;
import org.apache.gravitino.idp.storage.mapper.IdpGroupMetaMapper;
import org.apache.gravitino.idp.storage.mapper.IdpUserGroupRelMapper;
import org.apache.gravitino.idp.storage.po.IdpGroupPO;
import org.apache.gravitino.idp.storage.po.IdpUserGroupRelPO;
+import org.apache.gravitino.idp.storage.relational.utils.IdpExceptionUtils;
import org.apache.gravitino.metrics.Monitored;
import org.apache.gravitino.storage.RandomIdGenerator;
import org.apache.gravitino.storage.relational.utils.SessionUtils;
@@ -49,7 +54,13 @@ public class IdpGroupMetaService {
metricsSource = GRAVITINO_RELATIONAL_STORE_METRIC_NAME,
baseMetricName = "getIdpGroupByName")
public IdpGroupPO getIdpGroupByName(String groupName) {
- return getIdpGroupPOByName(groupName);
+ IdpGroupPO groupPO =
+ SessionUtils.getWithoutCommit(
+ IdpGroupMetaMapper.class, mapper ->
mapper.selectIdpGroup(groupName));
+ if (groupPO == null) {
+ throw new NotFoundException("IdP group not found: %s", groupName);
+ }
+ return groupPO;
}
@Monitored(
@@ -63,47 +74,109 @@ public class IdpGroupMetaService {
@Monitored(
metricsSource = GRAVITINO_RELATIONAL_STORE_METRIC_NAME,
baseMetricName = "insertIdpGroup")
- public void insertIdpGroup(IdpGroupPO groupPO) {
- SessionUtils.doWithCommit(IdpGroupMetaMapper.class, mapper ->
mapper.insertIdpGroup(groupPO));
+ public void insertIdpGroup(IdpGroupPO groupPO) throws IOException {
+ try {
+ SessionUtils.doWithCommit(IdpGroupMetaMapper.class, mapper ->
mapper.insertIdpGroup(groupPO));
+ } catch (RuntimeException re) {
+ IdpExceptionUtils.checkSQLException(re, "group", groupPO.getGroupName());
+ throw re;
+ }
}
+ /**
+ * Deletes a built-in IdP group.
+ *
+ * @param groupName the group name
+ * @param force when false, rejects deletion if the group still has members;
when true, removes
+ * memberships and deletes the group
+ * @return true if the group was deleted
+ */
@Monitored(
metricsSource = GRAVITINO_RELATIONAL_STORE_METRIC_NAME,
baseMetricName = "deleteIdpGroup")
- public boolean deleteIdpGroup(String groupName) {
+ public boolean deleteIdpGroup(String groupName, boolean force) {
+ if (!force && !listUsernamesByGroupName(groupName).isEmpty()) {
+ throw new IllegalStateException(
+ String.format("IdP group %s is not empty, use force=true to delete
it", groupName));
+ }
+
+ int[] deletedCount = new int[] {0};
SessionUtils.doMultipleWithCommit(
- () ->
+ () -> {
+ if (force) {
SessionUtils.doWithoutCommit(
IdpUserGroupRelMapper.class,
- mapper -> mapper.softDeleteRelationsByGroupName(groupName)),
- () ->
- SessionUtils.doWithoutCommit(
- IdpGroupMetaMapper.class, mapper ->
mapper.softDeleteIdpGroup(groupName)));
- return true;
+ mapper -> mapper.softDeleteRelationsByGroupName(groupName));
+ }
+ },
+ () -> {
+ Integer deleted =
+ SessionUtils.getWithoutCommit(
+ IdpGroupMetaMapper.class, mapper ->
mapper.softDeleteIdpGroup(groupName));
+ deletedCount[0] = deleted == null ? 0 : deleted;
+ });
+ return deletedCount[0] > 0;
}
+ /**
+ * Changes built-in IdP group membership in a single transaction.
+ *
+ * @param groupName The group name.
+ * @param additions The usernames to add.
+ * @param removals The usernames to remove.
+ */
@Monitored(
metricsSource = GRAVITINO_RELATIONAL_STORE_METRIC_NAME,
- baseMetricName = "addUsersToGroup")
- public void addUsersToGroup(String groupName, List<String> usernames) {
- IdpGroupPO group = getIdpGroupPOByName(groupName);
- Map<String, Long> userIds =
- IdpUserMetaService.getInstance().resolveUserIdsByUsernames(usernames);
- List<IdpUserGroupRelPO> relations = new ArrayList<>(usernames.size());
- for (String username : usernames) {
- relations.add(newUserGroupRelation(group.getGroupId(),
userIds.get(username)));
- }
-
- SessionUtils.doWithCommit(
- IdpUserGroupRelMapper.class, mapper ->
mapper.batchInsertRelations(relations));
- }
+ baseMetricName = "changeGroupMembership")
+ public void changeGroupMembership(
+ String groupName, List<String> additions, List<String> removals) {
+ SessionUtils.doMultipleWithCommit(
+ () -> {
+ IdpGroupPO group =
+ SessionUtils.getWithoutCommit(
+ IdpGroupMetaMapper.class,
+ mapper -> {
+ IdpGroupPO groupPO = mapper.selectIdpGroup(groupName);
+ if (groupPO == null) {
+ throw new NotFoundException("IdP group not found: %s",
groupName);
+ }
+ return groupPO;
+ });
+
+ List<String> currentUsernames =
+ SessionUtils.getWithoutCommit(
+ IdpUserGroupRelMapper.class,
+ mapper -> mapper.selectUsernamesByGroupName(groupName));
+ Set<String> oldUsernames = Sets.newHashSet(currentUsernames);
+ Set<String> newUsernames = Sets.newHashSet(oldUsernames);
+ newUsernames.addAll(additions);
+ newUsernames.removeAll(removals);
+
+ Set<String> insertUsernames = Sets.difference(newUsernames,
oldUsernames);
+ Set<String> deleteUsernames = Sets.difference(oldUsernames,
newUsernames);
+ if (insertUsernames.isEmpty() && deleteUsernames.isEmpty()) {
+ return;
+ }
+
+ List<String> insertList = Lists.newArrayList(insertUsernames);
+ if (!insertList.isEmpty()) {
+ Map<String, Long> userIds =
+
IdpUserMetaService.getInstance().resolveUserIdsByUsernames(insertList);
+ List<IdpUserGroupRelPO> relations = new
ArrayList<>(insertList.size());
+ for (String username : insertList) {
+ relations.add(newUserGroupRelation(group.getGroupId(),
userIds.get(username)));
+ }
+ SessionUtils.doWithoutCommit(
+ IdpUserGroupRelMapper.class, mapper ->
mapper.batchInsertRelations(relations));
+ }
- @Monitored(
- metricsSource = GRAVITINO_RELATIONAL_STORE_METRIC_NAME,
- baseMetricName = "removeUsersFromGroup")
- public int removeUsersFromGroup(String groupName, List<String> usernames) {
- return SessionUtils.doWithCommitAndFetchResult(
- IdpUserGroupRelMapper.class, mapper ->
mapper.softDeleteRelations(groupName, usernames));
+ List<String> deleteList = Lists.newArrayList(deleteUsernames);
+ if (!deleteList.isEmpty()) {
+ SessionUtils.doWithoutCommit(
+ IdpUserGroupRelMapper.class,
+ mapper -> mapper.softDeleteRelations(groupName, deleteList));
+ }
+ });
}
@Monitored(
@@ -139,14 +212,4 @@ public class IdpGroupMetaService {
.withDeletedAt(0L)
.build();
}
-
- private IdpGroupPO getIdpGroupPOByName(String groupName) {
- IdpGroupPO groupPO =
- SessionUtils.getWithoutCommit(
- IdpGroupMetaMapper.class, mapper ->
mapper.selectIdpGroup(groupName));
- if (groupPO == null) {
- throw new NotFoundException("IdP group not found: %s", groupName);
- }
- return groupPO;
- }
}
diff --git
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/service/IdpUserMetaService.java
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/service/IdpUserMetaService.java
index b4f62c74e3..70834283ef 100644
---
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/service/IdpUserMetaService.java
+++
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/service/IdpUserMetaService.java
@@ -20,6 +20,7 @@ package org.apache.gravitino.idp.storage.service;
import static
org.apache.gravitino.metrics.source.MetricsSource.GRAVITINO_RELATIONAL_STORE_METRIC_NAME;
+import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -27,6 +28,7 @@ import org.apache.gravitino.idp.exception.NotFoundException;
import org.apache.gravitino.idp.storage.mapper.IdpUserGroupRelMapper;
import org.apache.gravitino.idp.storage.mapper.IdpUserMetaMapper;
import org.apache.gravitino.idp.storage.po.IdpUserPO;
+import org.apache.gravitino.idp.storage.relational.utils.IdpExceptionUtils;
import org.apache.gravitino.metrics.Monitored;
import org.apache.gravitino.storage.relational.utils.SessionUtils;
@@ -47,7 +49,13 @@ public class IdpUserMetaService {
metricsSource = GRAVITINO_RELATIONAL_STORE_METRIC_NAME,
baseMetricName = "getIdpUserByUsername")
public IdpUserPO getIdpUserByUsername(String username) {
- return getIdpUserPOByUsername(username);
+ IdpUserPO userPO =
+ SessionUtils.getWithoutCommit(
+ IdpUserMetaMapper.class, mapper -> mapper.selectIdpUser(username));
+ if (userPO == null) {
+ throw new NotFoundException("IdP user not found: %s", username);
+ }
+ return userPO;
}
@Monitored(
@@ -61,23 +69,35 @@ public class IdpUserMetaService {
@Monitored(
metricsSource = GRAVITINO_RELATIONAL_STORE_METRIC_NAME,
baseMetricName = "insertIdpUser")
- public void insertIdpUser(IdpUserPO userPO) {
- SessionUtils.doWithCommit(IdpUserMetaMapper.class, mapper ->
mapper.insertIdpUser(userPO));
+ public void insertIdpUser(IdpUserPO userPO) throws IOException {
+ try {
+ SessionUtils.doWithCommit(IdpUserMetaMapper.class, mapper ->
mapper.insertIdpUser(userPO));
+ } catch (RuntimeException re) {
+ IdpExceptionUtils.checkSQLException(re, "user", userPO.getUsername());
+ throw re;
+ }
}
@Monitored(
metricsSource = GRAVITINO_RELATIONAL_STORE_METRIC_NAME,
baseMetricName = "deleteIdpUser")
public boolean deleteIdpUser(String username) {
+ int[] deletedCount = new int[] {0};
SessionUtils.doMultipleWithCommit(
- () ->
+ () -> {
+ Integer deleted =
+ SessionUtils.getWithoutCommit(
+ IdpUserMetaMapper.class, mapper ->
mapper.softDeleteIdpUser(username));
+ deletedCount[0] = deleted == null ? 0 : deleted;
+ },
+ () -> {
+ if (deletedCount[0] > 0) {
SessionUtils.doWithoutCommit(
IdpUserGroupRelMapper.class,
- mapper -> mapper.softDeleteRelationsByUsername(username)),
- () ->
- SessionUtils.doWithoutCommit(
- IdpUserMetaMapper.class, mapper ->
mapper.softDeleteIdpUser(username)));
- return true;
+ mapper -> mapper.softDeleteRelationsByUsername(username));
+ }
+ });
+ return deletedCount[0] > 0;
}
/**
@@ -144,14 +164,4 @@ public class IdpUserMetaService {
}
return userIds;
}
-
- private IdpUserPO getIdpUserPOByUsername(String username) {
- IdpUserPO userPO =
- SessionUtils.getWithoutCommit(
- IdpUserMetaMapper.class, mapper -> mapper.selectIdpUser(username));
- if (userPO == null) {
- throw new NotFoundException("IdP user not found: %s", username);
- }
- return userPO;
- }
}
diff --git
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/TestIdpUserGroupManager.java
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/TestIdpUserGroupManager.java
new file mode 100644
index 0000000000..5112e8d555
--- /dev/null
+++
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/TestIdpUserGroupManager.java
@@ -0,0 +1,206 @@
+/*
+ * 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.idp;
+
+import static org.apache.gravitino.Configs.CACHE_ENABLED;
+import static
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER;
+import static
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS;
+import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL;
+import static
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS;
+import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_STORE;
+import static org.apache.gravitino.Configs.ENTITY_STORE;
+import static org.apache.gravitino.Configs.RELATIONAL_ENTITY_STORE;
+import static org.apache.gravitino.Configs.STORE_DELETE_AFTER_TIME;
+
+import com.google.common.collect.Lists;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Comparator;
+import java.util.stream.Stream;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.idp.exception.AlreadyExistsException;
+import org.apache.gravitino.idp.exception.NotFoundException;
+import org.apache.gravitino.idp.model.IdpGroup;
+import org.apache.gravitino.idp.model.IdpUser;
+import org.apache.gravitino.storage.RandomIdGenerator;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+/** Integration tests for {@link IdpUserGroupManager} backed by an embedded H2
store. */
+public class TestIdpUserGroupManager {
+
+ private static IdpUserGroupManager manager;
+ private static Path h2Path;
+
+ @BeforeAll
+ public static void setUp() throws Exception {
+ h2Path = Files.createTempDirectory("gravitino_idp_manager_h2_");
+
+ Config config = new Config(false) {};
+ config.set(ENTITY_STORE, RELATIONAL_ENTITY_STORE);
+ config.set(ENTITY_RELATIONAL_STORE, "h2");
+ config.set(
+ ENTITY_RELATIONAL_JDBC_BACKEND_URL,
+ String.format("jdbc:h2:file:%s;DB_CLOSE_DELAY=-1;MODE=MYSQL", h2Path));
+ config.set(ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER, "org.h2.Driver");
+ config.set(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS, 100);
+ config.set(ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS, 1000L);
+ config.set(STORE_DELETE_AFTER_TIME, 20 * 60 * 1000L);
+ config.set(CACHE_ENABLED, false);
+
+ manager = new IdpUserGroupManager(config, RandomIdGenerator.INSTANCE);
+ }
+
+ @AfterAll
+ public static void tearDown() throws IOException {
+ if (manager != null) {
+ manager.close();
+ manager = null;
+ }
+
+ if (h2Path != null && Files.exists(h2Path)) {
+ try (Stream<Path> paths = Files.walk(h2Path)) {
+
paths.sorted(Comparator.reverseOrder()).forEach(TestIdpUserGroupManager::deletePath);
+ }
+ }
+ }
+
+ @Test
+ public void testAddUser() throws IOException {
+ IdpUser user = manager.addUser("testAdd", "password123");
+ Assertions.assertEquals("testAdd", user.name());
+ Assertions.assertTrue(user.groupNames().isEmpty());
+
+ Assertions.assertThrows(
+ AlreadyExistsException.class, () -> manager.addUser("testAdd",
"password456"));
+ }
+
+ @Test
+ public void testGetUser() throws IOException {
+ manager.addUser("testGet", "password123");
+
+ IdpUser user = manager.getUser("testGet");
+ Assertions.assertEquals("testGet", user.name());
+
+ Throwable exception =
+ Assertions.assertThrows(NotFoundException.class, () ->
manager.getUser("not-exist"));
+ Assertions.assertTrue(exception.getMessage().contains("IdP user not found:
not-exist"));
+ }
+
+ @Test
+ public void testRemoveUser() throws IOException {
+ manager.addUser("testRemove", "password123");
+
+ Assertions.assertTrue(manager.removeUser("testRemove"));
+ Assertions.assertFalse(manager.removeUser("no-exist"));
+ }
+
+ @Test
+ public void testChangePassword() throws IOException {
+ manager.addUser("testChangePassword", "password123");
+
+ Assertions.assertTrue(manager.changePassword("testChangePassword",
"new-password"));
+ Assertions.assertEquals("testChangePassword",
manager.getUser("testChangePassword").name());
+
+ Assertions.assertFalse(manager.changePassword("not-exist", "password123"));
+ }
+
+ @Test
+ public void testAddGroup() throws IOException {
+ IdpGroup group = manager.addGroup("testAddGroup");
+ Assertions.assertEquals("testAddGroup", group.name());
+ Assertions.assertTrue(group.usernames().isEmpty());
+
+ Assertions.assertThrows(AlreadyExistsException.class, () ->
manager.addGroup("testAddGroup"));
+ }
+
+ @Test
+ public void testGetGroup() throws IOException {
+ manager.addGroup("testGetGroup");
+
+ IdpGroup group = manager.getGroup("testGetGroup");
+ Assertions.assertEquals("testGetGroup", group.name());
+
+ Throwable exception =
+ Assertions.assertThrows(NotFoundException.class, () ->
manager.getGroup("not-exist"));
+ Assertions.assertTrue(exception.getMessage().contains("IdP group not
found: not-exist"));
+ }
+
+ @Test
+ public void testChangeGroupMembership() throws IOException {
+ manager.addUser("groupUser1", "password123");
+ manager.addUser("groupUser2", "password123");
+ manager.addUser("groupUser3", "password123");
+ manager.addGroup("testMembershipGroup");
+
+ IdpGroup group =
+ manager.changeGroupMembership(
+ "testMembershipGroup", Lists.newArrayList("groupUser1",
"groupUser2"), null);
+ Assertions.assertTrue(group.usernames().contains("groupUser1"));
+ Assertions.assertTrue(group.usernames().contains("groupUser2"));
+
+ group =
+ manager.changeGroupMembership(
+ "testMembershipGroup",
+ Lists.newArrayList("groupUser3"),
+ Lists.newArrayList("groupUser1"));
+ Assertions.assertFalse(group.usernames().contains("groupUser1"));
+ Assertions.assertTrue(group.usernames().contains("groupUser2"));
+ Assertions.assertTrue(group.usernames().contains("groupUser3"));
+
+ group =
+ manager.changeGroupMembership(
+ "testMembershipGroup", null, Lists.newArrayList("groupUser2",
"groupUser3"));
+ Assertions.assertTrue(group.usernames().isEmpty());
+
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> manager.changeGroupMembership("testMembershipGroup", null,
null));
+ }
+
+ @Test
+ public void testRemoveGroup() throws IOException {
+ manager.addUser("groupMember", "password123");
+ manager.addGroup("testRemoveGroup");
+ manager.changeGroupMembership("testRemoveGroup",
Lists.newArrayList("groupMember"), null);
+
+ Assertions.assertThrows(
+ IllegalStateException.class, () ->
manager.removeGroup("testRemoveGroup", false));
+
+ manager.changeGroupMembership("testRemoveGroup", null,
Lists.newArrayList("groupMember"));
+ Assertions.assertTrue(manager.removeGroup("testRemoveGroup", false));
+ Assertions.assertFalse(manager.removeGroup("no-exist", false));
+
+ manager.addUser("forceMember", "password123");
+ manager.addGroup("testForceRemoveGroup");
+ manager.changeGroupMembership("testForceRemoveGroup",
Lists.newArrayList("forceMember"), null);
+ Assertions.assertTrue(manager.removeGroup("testForceRemoveGroup", true));
+ }
+
+ private static void deletePath(Path path) {
+ try {
+ Files.deleteIfExists(path);
+ } catch (IOException e) {
+ throw new RuntimeException("Delete path failed: " + path, e);
+ }
+ }
+}
diff --git
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/AbstractIdpMetaStorageTest.java
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/AbstractIdpMetaStorageTest.java
index 2a2806aaec..d556fc26b5 100644
---
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/AbstractIdpMetaStorageTest.java
+++
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/AbstractIdpMetaStorageTest.java
@@ -34,6 +34,7 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.Config;
import org.apache.gravitino.Configs;
import org.apache.gravitino.config.ConfigConstants;
+import
org.apache.gravitino.idp.storage.relational.converters.IdpSQLExceptionConverterFactory;
import org.apache.gravitino.integration.test.container.ContainerSuite;
import org.apache.gravitino.integration.test.container.MySQLContainer;
import org.apache.gravitino.integration.test.container.PostgreSQLContainer;
@@ -53,6 +54,10 @@ public abstract class AbstractIdpMetaStorageTest {
protected JDBCBackend backend;
public SqlSession sharedSession;
+ protected Config getConfig() {
+ return config;
+ }
+
private Config config;
private Path h2Path;
@@ -69,6 +74,7 @@ public abstract class AbstractIdpMetaStorageTest {
}
SqlSessionFactoryHelper.getInstance().close();
+ IdpSQLExceptionConverterFactory.close();
ContainerSuite.getInstance().close();
if (h2Path != null && Files.exists(h2Path)) {
@@ -81,6 +87,7 @@ public abstract class AbstractIdpMetaStorageTest {
config = createBackendConfig(type);
backend = new JDBCBackend();
backend.initialize(config);
+ IdpSQLExceptionConverterFactory.initConverter(config);
sharedSession =
SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true);
initializeMappers();
}
@@ -92,6 +99,24 @@ public abstract class AbstractIdpMetaStorageTest {
}
}
+ /**
+ * Re-initializes the JDBC backend after another component closed the shared
SqlSession factory.
+ */
+ protected void reinitializeBackend() throws IOException {
+ if (backend != null) {
+ backend.close();
+ }
+ backend = new JDBCBackend();
+ backend.initialize(config);
+ IdpSQLExceptionConverterFactory.initConverter(config);
+ }
+
+ protected void reopenSession() {
+ closeSession();
+ sharedSession =
SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true);
+ initializeMappers();
+ }
+
protected void initializeMappers() {}
private Config createBackendConfig(String type) throws IOException {
diff --git
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/gc/TestIdpLegacyGarbageCollector.java
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/relational/TestIdpGarbageCollector.java
similarity index 91%
rename from
plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/gc/TestIdpLegacyGarbageCollector.java
rename to
plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/relational/TestIdpGarbageCollector.java
index 599954ed7a..7120c38d53 100644
---
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/gc/TestIdpLegacyGarbageCollector.java
+++
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/relational/TestIdpGarbageCollector.java
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.gravitino.idp.storage.gc;
+package org.apache.gravitino.idp.storage.relational;
import static org.apache.gravitino.Configs.STORE_DELETE_AFTER_TIME;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -27,7 +27,6 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
-import org.apache.gravitino.Config;
import org.apache.gravitino.idp.storage.mapper.AbstractIdpMetaStorageTest;
import org.apache.gravitino.idp.storage.mapper.IdpGroupMetaMapper;
import org.apache.gravitino.idp.storage.mapper.IdpUserGroupRelMapper;
@@ -42,7 +41,7 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
@Tag("gravitino-docker-test")
-class TestIdpLegacyGarbageCollector extends AbstractIdpMetaStorageTest {
+class TestIdpGarbageCollector extends AbstractIdpMetaStorageTest {
private IdpUserMetaMapper idpUserMetaMapper;
private IdpGroupMetaMapper idpGroupMetaMapper;
private IdpUserGroupRelMapper idpUserGroupRelMapper;
@@ -70,12 +69,16 @@ class TestIdpLegacyGarbageCollector extends
AbstractIdpMetaStorageTest {
assertEquals(2, countGroups());
assertEquals(8, countUserGroupRels());
- Config config = new Config(false) {};
- config.set(STORE_DELETE_AFTER_TIME, 600000L);
+ getConfig().set(STORE_DELETE_AFTER_TIME, 600000L);
closeSession();
- IdpLegacyGarbageCollector garbageCollector = new
IdpLegacyGarbageCollector(config);
- garbageCollector.collectAndClean();
+ IdpGarbageCollector garbageCollector = new
IdpGarbageCollector(getConfig());
+ try {
+ garbageCollector.collectAndClean();
+ } finally {
+ garbageCollector.close();
+ }
+ reinitializeBackend();
reopenSession();
assertEquals(0, countUsers());
@@ -83,12 +86,6 @@ class TestIdpLegacyGarbageCollector extends
AbstractIdpMetaStorageTest {
assertEquals(0, countUserGroupRels());
}
- private void reopenSession() {
- closeSession();
- sharedSession =
SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true);
- initializeMappers();
- }
-
private void insertGroups() {
for (long index = 1L; index <= 2L; index++) {
idpGroupMetaMapper.insertIdpGroup(
diff --git
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/service/TestIdpGroupMetaService.java
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/service/TestIdpGroupMetaService.java
index 4b7f8e1f5b..42d5f1fc84 100644
---
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/service/TestIdpGroupMetaService.java
+++
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/service/TestIdpGroupMetaService.java
@@ -26,6 +26,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.util.List;
+import org.apache.gravitino.idp.exception.AlreadyExistsException;
import org.apache.gravitino.idp.exception.NotFoundException;
import org.apache.gravitino.idp.storage.po.IdpGroupPO;
import org.junit.jupiter.api.Tag;
@@ -64,7 +65,8 @@ class TestIdpGroupMetaService extends
AbstractIdpMetaServiceTest {
assertThrows(NotFoundException.class, () ->
groupMetaService.getIdpGroupByName("group1"));
runServiceCall(() -> groupMetaService.insertIdpGroup(group1));
- runServiceCall(() -> groupMetaService.addUsersToGroup("group1",
List.of("user1")));
+ runServiceCall(
+ () -> groupMetaService.changeGroupMembership("group1",
List.of("user1"), List.of()));
assertEquals("group1",
groupMetaService.getIdpGroupByName("group1").getGroupName());
assertIterableEquals(List.of("user1"),
groupMetaService.listUsernamesByGroupName("group1"));
@@ -76,19 +78,23 @@ class TestIdpGroupMetaService extends
AbstractIdpMetaServiceTest {
.withLastVersion(0L)
.withDeletedAt(0L)
.build();
- assertThrowsRuntimeException(() ->
groupMetaService.insertIdpGroup(duplicateGroup));
+ assertThrows(
+ AlreadyExistsException.class, () ->
groupMetaService.insertIdpGroup(duplicateGroup));
}
@ParameterizedTest
@MethodSource("storageProvider")
- void testDeleteIdpGroupCascadesMemberships(String type) throws IOException {
+ void testDeleteIdpGroupForceRemovesMemberships(String type) throws
IOException {
init(type);
insertUsers(4);
insertGroups(4);
insertGroupUserGroupRelations();
IdpGroupMetaService groupMetaService = IdpGroupMetaService.getInstance();
- runServiceCall(() ->
assertTrue(groupMetaService.deleteIdpGroup("group1")));
+ assertThrows(
+ IllegalStateException.class, () ->
groupMetaService.deleteIdpGroup("group1", false));
+
+ runServiceCall(() -> assertTrue(groupMetaService.deleteIdpGroup("group1",
true)));
assertNull(idpGroupMetaMapper.selectIdpGroup("group1"));
assertEquals(4, countGroups());
assertEquals(8, countUserGroupRels());
@@ -114,21 +120,21 @@ class TestIdpGroupMetaService extends
AbstractIdpMetaServiceTest {
runServiceCall(() -> groupMetaService.insertIdpGroup(group1));
runServiceCall(
- () -> groupMetaService.addUsersToGroup("engineering", List.of("user1",
"user2")));
+ () ->
+ groupMetaService.changeGroupMembership(
+ "engineering", List.of("user1", "user2"), List.of()));
assertIterableEquals(
List.of("user1", "user2"),
groupMetaService.listUsernamesByGroupName("engineering"));
runServiceCall(
- () ->
- assertEquals(
- 1, groupMetaService.removeUsersFromGroup("engineering",
List.of("user1"))));
+ () -> groupMetaService.changeGroupMembership("engineering", List.of(),
List.of("user1")));
assertIterableEquals(
List.of("user2"),
groupMetaService.listUsernamesByGroupName("engineering"));
}
@ParameterizedTest
@MethodSource("storageProvider")
- void testAddUsersToGroupThrowsWhenUserMissing(String type) throws
IOException {
+ void testChangeGroupMembershipThrowsWhenUserMissing(String type) throws
IOException {
init(type);
insertUsers(1);
IdpGroupMetaService groupMetaService = IdpGroupMetaService.getInstance();
@@ -147,7 +153,9 @@ class TestIdpGroupMetaService extends
AbstractIdpMetaServiceTest {
NotFoundException.class,
() ->
runServiceCall(
- () -> groupMetaService.addUsersToGroup("engineering",
List.of("missing-user"))));
+ () ->
+ groupMetaService.changeGroupMembership(
+ "engineering", List.of("missing-user"), List.of())));
}
@ParameterizedTest
diff --git
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/service/TestIdpUserMetaService.java
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/service/TestIdpUserMetaService.java
index dbc71999d5..7af936a0a5 100644
---
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/service/TestIdpUserMetaService.java
+++
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/service/TestIdpUserMetaService.java
@@ -27,6 +27,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.util.List;
+import org.apache.gravitino.idp.exception.AlreadyExistsException;
import org.apache.gravitino.idp.exception.NotFoundException;
import org.apache.gravitino.idp.storage.po.IdpUserPO;
import org.junit.jupiter.api.Tag;
@@ -67,7 +68,8 @@ class TestIdpUserMetaService extends
AbstractIdpMetaServiceTest {
assertThrows(NotFoundException.class, () ->
userMetaService.getIdpUserByUsername("user1"));
runServiceCall(() -> userMetaService.insertIdpUser(user1));
- runServiceCall(() -> groupMetaService.addUsersToGroup("group1",
List.of("user1")));
+ runServiceCall(
+ () -> groupMetaService.changeGroupMembership("group1",
List.of("user1"), List.of()));
assertEquals("user1",
userMetaService.getIdpUserByUsername("user1").getUsername());
assertIterableEquals(List.of("group1"),
userMetaService.listGroupNamesByUsername("user1"));
@@ -80,7 +82,7 @@ class TestIdpUserMetaService extends
AbstractIdpMetaServiceTest {
.withLastVersion(0L)
.withDeletedAt(0L)
.build();
- assertThrowsRuntimeException(() ->
userMetaService.insertIdpUser(duplicateUser));
+ assertThrows(AlreadyExistsException.class, () ->
userMetaService.insertIdpUser(duplicateUser));
}
@ParameterizedTest