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() {