This is an automated email from the ASF dual-hosted git repository.

Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git


The following commit(s) were added to refs/heads/master by this push:
     new 36d388815d fix(admin): enforce unique data permission grants (#7610)
36d388815d is described below

commit 36d388815df029d2c5073f11b690ad459d5bb771
Author: lymerin <[email protected]>
AuthorDate: Thu Oct 15 12:58:44 2026 +0800

    fix(admin): enforce unique data permission grants (#7610)
---
 .github/scripts/native-sql-matrix.py               | 52 +++++++++++++++
 .github/scripts/test-native-sql-matrix.py          | 34 ++++++++++
 .github/workflows/native-sql-matrix.yml            |  2 +-
 db/init/mysql/schema.sql                           |  2 +-
 db/init/oracle/schema.sql                          |  1 +
 db/init/pg/create-table.sql                        |  1 +
 db/upgrade/2.7.1-upgrade-2.7.2-mysql.sql           |  9 ++-
 db/upgrade/2.7.1-upgrade-2.7.2-ob.sql              |  9 +++
 db/upgrade/2.7.1-upgrade-2.7.2-og.sql              |  9 +++
 db/upgrade/2.7.1-upgrade-2.7.2-oracle.sql          | 11 ++++
 db/upgrade/2.7.1-upgrade-2.7.2-pg.sql              |  9 +++
 db/upgrade/native-sql-matrix.md                    |  2 +
 db/upgrade/upgrade-guide.md                        | 10 +++
 .../src/main/resources/sql-script/h2/schema.sql    |  2 +-
 .../shenyu/admin/mapper/AdminQueryIndexTest.java   |  4 ++
 .../admin/mapper/DataPermissionMapperTest.java     | 76 ++++++++++++++++++++++
 16 files changed, 229 insertions(+), 4 deletions(-)

diff --git a/.github/scripts/native-sql-matrix.py 
b/.github/scripts/native-sql-matrix.py
index 35aba716cb..5e94e21ca7 100644
--- a/.github/scripts/native-sql-matrix.py
+++ b/.github/scripts/native-sql-matrix.py
@@ -397,6 +397,54 @@ class Engine:
         else:
             raise AssertionError("Duplicate discovery upstream was accepted")
 
+    def insert_data_permissions(self, duplicates=False):
+        grants = [("1", "sql-matrix-user", "sql-matrix-data", 0),
+                  ("4", "sql-matrix-user-other", "sql-matrix-data", 0),
+                  ("5", "sql-matrix-user", "sql-matrix-data-other", 0),
+                  ("6", "sql-matrix-user", "sql-matrix-data", 1)]
+        if duplicates:
+            grants += [("2", "sql-matrix-user", "sql-matrix-data", 0),
+                       ("3", "sql-matrix-user", "sql-matrix-data", 0)]
+        self.sql("\n".join("INSERT INTO data_permission 
(id,user_id,data_id,data_type,date_created,date_updated) "
+                           f"VALUES 
('sql-matrix-grant-{identifier}','{user}','{data}',{kind},"
+                           "TIMESTAMP '2026-01-01 00:00:00',TIMESTAMP 
'2026-01-02 00:00:00');"
+                           for identifier, user, data, kind in grants))
+
+    def check_data_permission_uniqueness(self, indexes):
+        index = "idx_data_permission_user_type"
+        if indexes.get(("data_permission", index)) != ["user_id", "data_type", 
"data_id"]:
+            raise AssertionError("Wrong data permission unique index columns")
+        if self.dialect in ("mysql", "ob"):
+            query = ("SELECT COUNT(*) FROM information_schema.statistics WHERE 
table_schema='shenyu' "
+                     f"AND table_name='data_permission' AND 
index_name='{index}' AND non_unique=0;")
+            expected = "3"
+        elif self.dialect in ("pg", "og"):
+            query = ("SELECT COUNT(*) FROM pg_indexes WHERE 
schemaname='public' AND tablename='data_permission' "
+                     f"AND indexname='{index}' AND indexdef LIKE 'CREATE 
UNIQUE INDEX%';")
+            expected = "1"
+        else:
+            query = ("SELECT COUNT(*) FROM user_indexes WHERE 
table_name='DATA_PERMISSION' "
+                     f"AND index_name='{index.upper()}' AND 
uniqueness='UNIQUE';")
+            expected = "1"
+        if scalar(self.sql(query)) != expected:
+            raise AssertionError("Data permission index is not unique")
+        identifiers = set(self.sql("SELECT id FROM data_permission "
+                                   "WHERE user_id IN 
('sql-matrix-user','sql-matrix-user-other');").split())
+        if identifiers != {f"sql-matrix-grant-{number}" for number in (1, 4, 
5, 6)}:
+            raise AssertionError("Data permission migration lost distinct 
grants or retained duplicate rows")
+        if scalar(self.sql("SELECT COUNT(*) FROM data_permission WHERE 
id='sql-matrix-grant-1' "
+                           "AND date_created=TIMESTAMP '2026-01-01 00:00:00' "
+                           "AND date_updated=TIMESTAMP '2026-01-02 
00:00:00';")) != "1":
+            raise AssertionError("Retained data permission timestamps changed")
+        try:
+            self.sql("INSERT INTO data_permission 
(id,user_id,data_id,data_type) "
+                     "VALUES 
('sql-matrix-grant-conflict','sql-matrix-user','sql-matrix-data',0);")
+        except RuntimeError as error:
+            if index not in str(error).lower():
+                raise
+        else:
+            raise AssertionError("Duplicate data permission was accepted")
+
 
 def execute_matrix(dialect, root, output):
     output.mkdir(parents=True, exist_ok=True)
@@ -417,8 +465,11 @@ def execute_matrix(dialect, root, output):
             original_listing = engine.sql("SELECT " + ",".join(listing) + f" 
FROM plugin WHERE id='{SENTINEL}';")
             before = {table: set(engine.sql("SELECT id FROM " + 
engine.table(table) + ";").split()) for table in TABLES}
             if flow == "upgrade":
+                engine.insert_data_permissions(duplicates=True)
                 migration = root / 
f"db/upgrade/2.7.1-upgrade-2.7.2-{dialect}.sql"
                 engine.sql(migration.read_text(encoding="utf-8"))
+            else:
+                engine.insert_data_permissions()
             engine.check_data(listing, detail)
             current_listing = engine.sql("SELECT " + ",".join(listing) + f" 
FROM plugin WHERE id='{SENTINEL}';")
             if current_listing != original_listing:
@@ -428,6 +479,7 @@ def execute_matrix(dialect, root, output):
                 if not identifiers.issubset(after):
                     raise AssertionError(f"Migration removed rows from 
{table}: {sorted(identifiers - after)}")
             indexes = engine.indexes()
+            engine.check_data_permission_uniqueness(indexes)
             if flow == "fresh" and dialect in ("mysql", "ob"):
                 engine.check_fresh_mysql_schema(schema, indexes)
             if flow == "fresh" and dialect in ("pg", "og"):
diff --git a/.github/scripts/test-native-sql-matrix.py 
b/.github/scripts/test-native-sql-matrix.py
index f9553b9a5c..a07291f2f9 100644
--- a/.github/scripts/test-native-sql-matrix.py
+++ b/.github/scripts/test-native-sql-matrix.py
@@ -223,6 +223,40 @@ class NativeSqlMatrixTest(unittest.TestCase):
             with patch.object(engine, "sql", 
side_effect=RuntimeError("connection refused")), 
self.assertRaisesRegex(RuntimeError, "connection"):
                 engine.check_discovery_upstream_uniqueness()
 
+    def test_data_permission_uniqueness_requires_the_intended_constraint(self):
+        indexes = {("data_permission", "idx_data_permission_user_type"): 
["user_id", "data_type", "data_id"]}
+        for dialect in matrix.DIALECTS:
+            for conflict, error in ((RuntimeError("duplicate key: 
IDX_DATA_PERMISSION_USER_TYPE"), None),
+                                    (RuntimeError("different constraint"), 
RuntimeError), (None, AssertionError)):
+                def sql(query):
+                    if query.startswith("INSERT"):
+                        if conflict:
+                            raise conflict
+                        return ""
+                    if query.startswith("SELECT id"):
+                        return "\n".join(f"sql-matrix-grant-{number}" for 
number in (1, 4, 5, 6))
+                    return "3" if "information_schema.statistics" in query 
else "1"
+
+                with self.subTest(dialect=dialect, conflict=conflict), 
tempfile.TemporaryDirectory() as directory:
+                    engine = matrix.Engine(dialect, "upgrade", Path(directory))
+                    with patch.object(engine, "sql", side_effect=sql):
+                        if error:
+                            with self.assertRaises(error):
+                                
engine.check_data_permission_uniqueness(indexes)
+                        else:
+                            engine.check_data_permission_uniqueness(indexes)
+
+    def 
test_data_permission_check_rejects_nonunique_index_and_incorrect_cleanup(self):
+        indexes = {("data_permission", "idx_data_permission_user_type"): 
["user_id", "data_type", "data_id"]}
+        with tempfile.TemporaryDirectory() as directory:
+            engine = matrix.Engine("mysql", "upgrade", Path(directory))
+            with patch.object(engine, "sql", return_value="0"), 
self.assertRaisesRegex(AssertionError, "not unique"):
+                engine.check_data_permission_uniqueness(indexes)
+            for numbers in ((1, 2, 4, 5, 6), (1, 5, 6)):
+                with patch.object(engine, "sql", side_effect=["3", 
"\n".join(f"sql-matrix-grant-{number}" for number in numbers)]), \
+                        self.assertRaisesRegex(AssertionError, "lost distinct 
grants or retained duplicate"):
+                    engine.check_data_permission_uniqueness(indexes)
+
     def test_actual_mapper_projects_metadata_and_preserves_detail_jar(self):
         listing, detail = matrix.fragment_columns(ROOT / 
"shenyu-admin/src/main/resources/mappers/plugin-sqlmap.xml")
         self.assertNotIn("plugin_jar", listing)
diff --git a/.github/workflows/native-sql-matrix.yml 
b/.github/workflows/native-sql-matrix.yml
index 66c5a41897..fe429b1151 100644
--- a/.github/workflows/native-sql-matrix.yml
+++ b/.github/workflows/native-sql-matrix.yml
@@ -77,7 +77,7 @@ jobs:
       - name: Verify list/detail/export and index contracts
         run: >-
           ./mvnw -B -ntp -pl shenyu-admin -am test
-          
-Dtest=PluginMapperTest,PluginServiceTest,PluginControllerTest,AdminQueryIndexTest,PluginIndexUpgradeContractTest
+          
-Dtest=PluginMapperTest,PluginServiceTest,PluginControllerTest,AdminQueryIndexTest,PluginIndexUpgradeContractTest,DataPermissionMapperTest,PermissionListenerIntegrationTest
           -Dsurefire.failIfNoSpecifiedTests=false -Dmaven.javadoc.skip=true
 
   native-sql:
diff --git a/db/init/mysql/schema.sql b/db/init/mysql/schema.sql
index 26828e7f63..f6699a41fe 100644
--- a/db/init/mysql/schema.sql
+++ b/db/init/mysql/schema.sql
@@ -2459,7 +2459,7 @@ CREATE INDEX idx_app_auth_ns_key ON `app_auth` 
(namespace_id, app_key);
 CREATE INDEX idx_auth_path_auth ON `auth_path` (auth_id);
 CREATE INDEX idx_auth_param_auth ON `auth_param` (auth_id);
 CREATE INDEX idx_permission_object_resource ON `permission` (object_id, 
resource_id);
-CREATE INDEX idx_data_permission_user_type ON `data_permission` (user_id, 
data_type, data_id);
+CREATE UNIQUE INDEX idx_data_permission_user_type ON `data_permission` 
(user_id, data_type, data_id);
 CREATE INDEX idx_tag_relation_api_tag ON `tag_relation` (api_id, tag_id);
 CREATE INDEX idx_tag_relation_tag_api ON `tag_relation` (tag_id, api_id);
 CREATE INDEX idx_api_path_method_rpc ON `api` (api_path, http_method, 
rpc_type);
diff --git a/db/init/oracle/schema.sql b/db/init/oracle/schema.sql
index 9c4b162e7c..3b0a1f08ea 100644
--- a/db/init/oracle/schema.sql
+++ b/db/init/oracle/schema.sql
@@ -818,6 +818,7 @@ create table data_permission
     date_updated timestamp(3) default SYSDATE not null,
     PRIMARY KEY (id)
 );
+CREATE UNIQUE INDEX idx_data_permission_user_type ON data_permission (user_id, 
data_type, data_id);
 -- Add comments to the table
 comment on table DATA_PERMISSION
   is 'data permission table';
diff --git a/db/init/pg/create-table.sql b/db/init/pg/create-table.sql
index 50a7fc461d..cb0c92bdcb 100644
--- a/db/init/pg/create-table.sql
+++ b/db/init/pg/create-table.sql
@@ -2047,6 +2047,7 @@ ALTER TABLE "public"."dashboard_user" ADD CONSTRAINT 
"dashboard_user_pkey" PRIMA
 -- Primary Key structure for table data_permission
 -- ----------------------------
 ALTER TABLE "public"."data_permission" ADD CONSTRAINT "data_permission_pkey" 
PRIMARY KEY ("id");
+CREATE UNIQUE INDEX "idx_data_permission_user_type" ON 
"public"."data_permission" USING btree ("user_id", "data_type", "data_id");
 
 -- ----------------------------
 -- Primary Key structure for table meta_data
diff --git a/db/upgrade/2.7.1-upgrade-2.7.2-mysql.sql 
b/db/upgrade/2.7.1-upgrade-2.7.2-mysql.sql
index 04104c6207..6c2771ce99 100644
--- a/db/upgrade/2.7.1-upgrade-2.7.2-mysql.sql
+++ b/db/upgrade/2.7.1-upgrade-2.7.2-mysql.sql
@@ -96,7 +96,14 @@ CREATE INDEX idx_app_auth_ns_key ON `app_auth` 
(namespace_id, app_key);
 CREATE INDEX idx_auth_path_auth ON `auth_path` (auth_id);
 CREATE INDEX idx_auth_param_auth ON `auth_param` (auth_id);
 CREATE INDEX idx_permission_object_resource ON `permission` (object_id, 
resource_id);
-CREATE INDEX idx_data_permission_user_type ON `data_permission` (user_id, 
data_type, data_id);
+-- Retain the smallest existing id for each grant before enforcing uniqueness.
+DELETE duplicate_permission FROM data_permission duplicate_permission
+JOIN data_permission retained_permission
+  ON duplicate_permission.user_id = retained_permission.user_id
+ AND duplicate_permission.data_type = retained_permission.data_type
+ AND duplicate_permission.data_id = retained_permission.data_id
+ AND duplicate_permission.id > retained_permission.id;
+CREATE UNIQUE INDEX idx_data_permission_user_type ON `data_permission` 
(user_id, data_type, data_id);
 CREATE INDEX idx_tag_relation_api_tag ON `tag_relation` (api_id, tag_id);
 CREATE INDEX idx_tag_relation_tag_api ON `tag_relation` (tag_id, api_id);
 CREATE INDEX idx_api_path_method_rpc ON `api` (api_path, http_method, 
rpc_type);
diff --git a/db/upgrade/2.7.1-upgrade-2.7.2-ob.sql 
b/db/upgrade/2.7.1-upgrade-2.7.2-ob.sql
index 162849ebdc..84a631d0b4 100644
--- a/db/upgrade/2.7.1-upgrade-2.7.2-ob.sql
+++ b/db/upgrade/2.7.1-upgrade-2.7.2-ob.sql
@@ -88,3 +88,12 @@ INSERT INTO `permission` VALUES ('1960000000000001029', 
'1346358560427216896', '
 INSERT INTO `namespace_plugin_rel` (`id`,`namespace_id`,`plugin_id`, `config`, 
`sort`, `enabled`, `date_created`, `date_updated`) VALUES 
('1960000000000001030','649330b6-c2d7-4edc-be8e-8a54df9eb385','68', NULL, 198, 
0, '2026-09-19 00:00:00.000', '2026-09-19 00:00:00.000');
 -- add index to speed up the meta data path uniqueness check
 ALTER TABLE `meta_data` ADD INDEX `idx_meta_data_namespace_path` 
(`namespace_id`, `path`) USING BTREE;
+
+-- Retain the smallest existing id for each grant before enforcing uniqueness.
+DELETE duplicate_permission FROM data_permission duplicate_permission
+JOIN data_permission retained_permission
+  ON duplicate_permission.user_id = retained_permission.user_id
+ AND duplicate_permission.data_type = retained_permission.data_type
+ AND duplicate_permission.data_id = retained_permission.data_id
+ AND duplicate_permission.id > retained_permission.id;
+CREATE UNIQUE INDEX idx_data_permission_user_type ON data_permission (user_id, 
data_type, data_id);
diff --git a/db/upgrade/2.7.1-upgrade-2.7.2-og.sql 
b/db/upgrade/2.7.1-upgrade-2.7.2-og.sql
index b6524e0fc9..b4be51b9a8 100644
--- a/db/upgrade/2.7.1-upgrade-2.7.2-og.sql
+++ b/db/upgrade/2.7.1-upgrade-2.7.2-og.sql
@@ -88,3 +88,12 @@ INSERT INTO "public"."permission" VALUES 
('1960000000000001029', '13463585604272
 INSERT INTO "public"."namespace_plugin_rel" VALUES 
('1960000000000001030','649330b6-c2d7-4edc-be8e-8a54df9eb385','68', NULL, 198, 
0, '2026-09-19 00:00:00.000', '2026-09-19 00:00:00.000');
 -- add index to speed up the meta data path uniqueness check
 CREATE INDEX "idx_meta_data_namespace_path" ON "public"."meta_data" USING 
btree ("namespace_id","path");
+
+-- Retain the smallest existing id for each grant before enforcing uniqueness.
+DELETE FROM data_permission duplicate_permission
+USING data_permission retained_permission
+WHERE duplicate_permission.user_id = retained_permission.user_id
+  AND duplicate_permission.data_type = retained_permission.data_type
+  AND duplicate_permission.data_id = retained_permission.data_id
+  AND duplicate_permission.id > retained_permission.id;
+CREATE UNIQUE INDEX "idx_data_permission_user_type" ON 
"public"."data_permission" USING btree ("user_id", "data_type", "data_id");
diff --git a/db/upgrade/2.7.1-upgrade-2.7.2-oracle.sql 
b/db/upgrade/2.7.1-upgrade-2.7.2-oracle.sql
index 3705cc8af3..ed839909cd 100644
--- a/db/upgrade/2.7.1-upgrade-2.7.2-oracle.sql
+++ b/db/upgrade/2.7.1-upgrade-2.7.2-oracle.sql
@@ -102,3 +102,14 @@ INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX 
(namespace_plugin_rel(id)) */ INTO namespa
 VALUES ('1960000000000001030','649330b6-c2d7-4edc-be8e-8a54df9eb385','68', 
NULL, 198, 0);
 -- add index to speed up the meta data path uniqueness check
 create index idx_meta_data_namespace_path on meta_data (namespace_id, path);
+
+-- Retain the smallest existing id for each grant before enforcing uniqueness.
+DELETE FROM data_permission duplicate_permission
+WHERE EXISTS (
+    SELECT 1 FROM data_permission retained_permission
+    WHERE duplicate_permission.user_id = retained_permission.user_id
+      AND duplicate_permission.data_type = retained_permission.data_type
+      AND duplicate_permission.data_id = retained_permission.data_id
+      AND duplicate_permission.id > retained_permission.id
+);
+CREATE UNIQUE INDEX idx_data_permission_user_type ON data_permission (user_id, 
data_type, data_id);
diff --git a/db/upgrade/2.7.1-upgrade-2.7.2-pg.sql 
b/db/upgrade/2.7.1-upgrade-2.7.2-pg.sql
index 87bd1e1dee..ec034e6d50 100644
--- a/db/upgrade/2.7.1-upgrade-2.7.2-pg.sql
+++ b/db/upgrade/2.7.1-upgrade-2.7.2-pg.sql
@@ -88,3 +88,12 @@ INSERT INTO "public"."permission" VALUES 
('1960000000000001029', '13463585604272
 INSERT INTO "public"."namespace_plugin_rel" VALUES 
('1960000000000001030','649330b6-c2d7-4edc-be8e-8a54df9eb385','68', NULL, 198, 
0, '2026-09-19 00:00:00.000', '2026-09-19 00:00:00.000');
 -- add index to speed up the meta data path uniqueness check
 CREATE INDEX "idx_meta_data_namespace_path" ON "public"."meta_data" USING 
btree ("namespace_id","path");
+
+-- Retain the smallest existing id for each grant before enforcing uniqueness.
+DELETE FROM data_permission duplicate_permission
+USING data_permission retained_permission
+WHERE duplicate_permission.user_id = retained_permission.user_id
+  AND duplicate_permission.data_type = retained_permission.data_type
+  AND duplicate_permission.data_id = retained_permission.data_id
+  AND duplicate_permission.id > retained_permission.id;
+CREATE UNIQUE INDEX "idx_data_permission_user_type" ON 
"public"."data_permission" USING btree ("user_id", "data_type", "data_id");
diff --git a/db/upgrade/native-sql-matrix.md b/db/upgrade/native-sql-matrix.md
index 9e620fd392..a702559ec2 100644
--- a/db/upgrade/native-sql-matrix.md
+++ b/db/upgrade/native-sql-matrix.md
@@ -63,6 +63,8 @@ and JAR bytes, preserve pre-upgrade row IDs in 
plugin/selector/rule/resource/
 permission/user-role/namespace-plugin relation tables, and inspect native index
 catalogs. Composite indexes only count when the requested column leads the 
index.
 The exact new selector/plugin and permission/resource indexes must also exist.
+Both flows verify data-permission uniqueness; the upgrade flow also verifies
+duplicate-grant cleanup preserves distinct grants.
 
 The shared MySQL/OceanBase fresh flows additionally require all declared 
tables,
 secondary indexes and unique constraints, reject orphan seed relations and
diff --git a/db/upgrade/upgrade-guide.md b/db/upgrade/upgrade-guide.md
index 0d51073f93..c966b31633 100644
--- a/db/upgrade/upgrade-guide.md
+++ b/db/upgrade/upgrade-guide.md
@@ -4,6 +4,16 @@
 
 ## To Shenyu Users
 
+### Data permission uniqueness in 2.7.2
+
+Stop Admin instances before running the `2.7.1-upgrade-2.7.2-<dialect>.sql`
+script, then restart them after it completes. Run the version upgrade script
+once. It removes duplicate `data_permission` rows with the same
+`(user_id, data_type, data_id)`, retaining the complete row with the smallest
+existing `id`, and creates the unique `idx_data_permission_user_type` index.
+Each distinct grant is preserved. Fresh installations create the same unique
+index directly.
+
 - 2.7.1-upgrade-2.7.2-mysql.sql
 
 - 2.7.1-upgrade-2.7.2-ob.sql
diff --git a/shenyu-admin/src/main/resources/sql-script/h2/schema.sql 
b/shenyu-admin/src/main/resources/sql-script/h2/schema.sql
index 6a58548e1e..77a6941a53 100644
--- a/shenyu-admin/src/main/resources/sql-script/h2/schema.sql
+++ b/shenyu-admin/src/main/resources/sql-script/h2/schema.sql
@@ -1567,7 +1567,7 @@ CREATE INDEX idx_app_auth_ns_key ON `app_auth` 
(namespace_id, app_key);
 CREATE INDEX idx_auth_path_auth ON `auth_path` (auth_id);
 CREATE INDEX idx_auth_param_auth ON `auth_param` (auth_id);
 CREATE INDEX idx_permission_object_resource ON `permission` (object_id, 
resource_id);
-CREATE INDEX idx_data_permission_user_type ON `data_permission` (user_id, 
data_type, data_id);
+CREATE UNIQUE INDEX idx_data_permission_user_type ON `data_permission` 
(user_id, data_type, data_id);
 CREATE INDEX idx_tag_relation_api_tag ON `tag_relation` (api_id, tag_id);
 CREATE INDEX idx_tag_relation_tag_api ON `tag_relation` (tag_id, api_id);
 CREATE INDEX idx_api_path_method_rpc ON `api` (api_path, http_method, 
rpc_type);
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/AdminQueryIndexTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/AdminQueryIndexTest.java
index c785319ddb..e0499a33c8 100644
--- 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/AdminQueryIndexTest.java
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/AdminQueryIndexTest.java
@@ -32,6 +32,7 @@ import java.util.Locale;
 import java.util.stream.Stream;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 
 class AdminQueryIndexTest extends AbstractSpringIntegrationTest {
 
@@ -48,6 +49,9 @@ class AdminQueryIndexTest extends 
AbstractSpringIntegrationTest {
             while (indexes.next()) {
                 if (index.equalsIgnoreCase(indexes.getString("INDEX_NAME"))) {
                     
actual.add(indexes.getString("COLUMN_NAME").toLowerCase(Locale.ROOT));
+                    if ("data_permission".equals(table)) {
+                        assertFalse(indexes.getBoolean("NON_UNIQUE"));
+                    }
                 }
             }
         }
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/DataPermissionMapperTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/DataPermissionMapperTest.java
index 70eae28cce..b3b7051e1d 100644
--- 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/DataPermissionMapperTest.java
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/DataPermissionMapperTest.java
@@ -22,11 +22,21 @@ import 
org.apache.shenyu.admin.model.entity.DataPermissionDO;
 import org.apache.shenyu.common.utils.UUIDUtils;
 import org.assertj.core.util.Lists;
 import org.junit.jupiter.api.Test;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.support.TransactionTemplate;
 
 import jakarta.annotation.Resource;
 import java.util.List;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
 
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 /**
  * Test cases for DataPermissionMapper.
@@ -37,6 +47,72 @@ public class DataPermissionMapperTest extends 
AbstractSpringIntegrationTest {
     @Resource
     private DataPermissionMapper dataPermissionMapper;
 
+    @Resource
+    private PlatformTransactionManager transactionManager;
+
+    @Test
+    public void testDuplicateGrantIsRejected() {
+        DataPermissionDO permission = buildDataPermissionDO();
+        dataPermissionMapper.insertSelective(permission);
+        DataPermissionDO duplicate = 
DataPermissionDO.buildCreatePermissionDO(permission.getDataId(), 
permission.getUserId(), permission.getDataType());
+
+        assertThrows(DuplicateKeyException.class, () -> 
dataPermissionMapper.insertSelective(duplicate));
+        assertEquals(permission.getId(), 
dataPermissionMapper.findOneByUniqueKey(permission.getDataId(), 
permission.getUserId(), permission.getDataType()).getId());
+    }
+
+    @Test
+    public void testDuplicateBatchDoesNotInsertOtherGrants() {
+        DataPermissionDO permission = buildDataPermissionDO();
+        dataPermissionMapper.insertSelective(permission);
+        DataPermissionDO duplicate = 
DataPermissionDO.buildCreatePermissionDO(permission.getDataId(), 
permission.getUserId(), permission.getDataType());
+        DataPermissionDO other = 
DataPermissionDO.buildCreatePermissionDO(UUIDUtils.getInstance().generateShortUuid(),
 permission.getUserId(), permission.getDataType());
+
+        assertThrows(DuplicateKeyException.class, () -> 
dataPermissionMapper.insertBatch(List.of(other, duplicate)));
+        assertEquals(1, 
dataPermissionMapper.listByUserId(permission.getUserId()).size());
+    }
+
+    @Test
+    public void testDistinctGrantKeysArePreserved() {
+        DataPermissionDO permission = buildDataPermissionDO();
+        List<DataPermissionDO> permissions = List.of(permission,
+                
DataPermissionDO.buildCreatePermissionDO(permission.getDataId(), 
UUIDUtils.getInstance().generateShortUuid(), permission.getDataType()),
+                
DataPermissionDO.buildCreatePermissionDO(UUIDUtils.getInstance().generateShortUuid(),
 permission.getUserId(), permission.getDataType()),
+                
DataPermissionDO.buildCreatePermissionDO(permission.getDataId(), 
permission.getUserId(), permission.getDataType() + 1));
+
+        assertEquals(4, dataPermissionMapper.insertBatch(permissions));
+        assertEquals(3, 
dataPermissionMapper.listByUserId(permission.getUserId()).size());
+    }
+
+    @Test
+    public void testConcurrentGrantCommitsOnlyOnce() throws Exception {
+        DataPermissionDO permission = buildDataPermissionDO();
+        DataPermissionDO duplicate = 
DataPermissionDO.buildCreatePermissionDO(permission.getDataId(), 
permission.getUserId(), permission.getDataType());
+        CyclicBarrier barrier = new CyclicBarrier(2);
+        ExecutorService executor = Executors.newFixedThreadPool(2);
+        try {
+            Future<Boolean> first = executor.submit(() -> 
insertConcurrentGrant(permission, barrier));
+            Future<Boolean> second = executor.submit(() -> 
insertConcurrentGrant(duplicate, barrier));
+            int committed = (first.get(20, TimeUnit.SECONDS) ? 1 : 0) + 
(second.get(20, TimeUnit.SECONDS) ? 1 : 0);
+            assertEquals(1, committed);
+            assertEquals(1, 
dataPermissionMapper.listByUserId(permission.getUserId()).size());
+        } finally {
+            executor.shutdownNow();
+        }
+    }
+
+    private boolean insertConcurrentGrant(final DataPermissionDO permission, 
final CyclicBarrier barrier) {
+        try {
+            new 
TransactionTemplate(transactionManager).executeWithoutResult(status -> {
+                assertEquals(List.of(), 
dataPermissionMapper.selectUserIds(permission.getDataId(), 
permission.getDataType()));
+                assertDoesNotThrow(() -> barrier.await(10, TimeUnit.SECONDS));
+                dataPermissionMapper.insertSelective(permission);
+            });
+            return true;
+        } catch (DuplicateKeyException e) {
+            return false;
+        }
+    }
+
     @Test
     public void insertBatch() {
 

Reply via email to