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 2b3ff1743f fix(admin): grant event permissions atomically (#7254)
2b3ff1743f is described below
commit 2b3ff1743f37ae3ac59e1d735daca78ce0fe8f6a
Author: Liming Deng <[email protected]>
AuthorDate: Wed Sep 30 09:59:42 2026 +0800
fix(admin): grant event permissions atomically (#7254)
---
.../shenyu/admin/mapper/DataPermissionMapper.java | 9 ++
.../service/impl/DataPermissionServiceImpl.java | 35 +++---
.../resources/mappers/data-permission-sqlmap.xml | 17 +++
.../mapper/DataPermissionBatchDialectTest.java | 69 ++++++++++++
.../service/PermissionListenerIntegrationTest.java | 120 +++++++++++++++++++++
5 files changed, 234 insertions(+), 16 deletions(-)
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/DataPermissionMapper.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/DataPermissionMapper.java
index 4285ce6546..0c1a016eff 100644
---
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/DataPermissionMapper.java
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/DataPermissionMapper.java
@@ -46,6 +46,15 @@ public interface DataPermissionMapper {
*/
List<DataPermissionDO> listByUserId(String userId);
+ /**
+ * Find users already granted access to a selector or rule.
+ *
+ * @param dataId selector or rule id
+ * @param dataType permission type
+ * @return granted user ids
+ */
+ List<String> selectUserIds(@Param("dataId") String dataId,
@Param("dataType") Integer dataType);
+
/**
* deleteSelector data permission by user id and data id.
* @param dataId data id
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DataPermissionServiceImpl.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DataPermissionServiceImpl.java
index e0781a26d7..3fa9bfa1fc 100644
---
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DataPermissionServiceImpl.java
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DataPermissionServiceImpl.java
@@ -265,6 +265,7 @@ public class DataPermissionServiceImpl implements
DataPermissionService {
* @param event event
*/
@EventListener(SelectorCreatedEvent.class)
+ @Transactional(rollbackFor = Exception.class)
public void onSelectorCreated(final SelectorCreatedEvent event) {
// check selector add
Boolean existed;
@@ -278,20 +279,15 @@ public class DataPermissionServiceImpl implements
DataPermissionService {
dataPermissionDTO.setUserId(JwtUtils.getUserInfo().getUserId());
dataPermissionDTO.setDataId(event.getSelector().getId());
dataPermissionDTO.setDataType(AdminConstants.SELECTOR_DATA_TYPE);
-
dataPermissionMapper.insertSelective(DataPermissionDO.buildPermissionDO(dataPermissionDTO));
+
grantMissingPermissions(Collections.singletonList(dataPermissionDTO.getUserId()),
dataPermissionDTO.getDataId(), dataPermissionDTO.getDataType());
} else {
String namespaceId = event.getSelector().getNamespaceId();
if (StringUtils.isNoneBlank(namespaceId)) {
// support namespace
List<NamespaceUserRelDO> namespaceUserRelDOList =
namespaceUserRelMapper.selectListByNamespaceId(namespaceId);
if (CollectionUtils.isNotEmpty(namespaceUserRelDOList)) {
- namespaceUserRelDOList.forEach(namespaceUserRelDO -> {
- DataPermissionDTO dataPermissionDTO = new
DataPermissionDTO();
-
dataPermissionDTO.setUserId(namespaceUserRelDO.getUserId());
-
dataPermissionDTO.setDataId(event.getSelector().getId());
-
dataPermissionDTO.setDataType(AdminConstants.SELECTOR_DATA_TYPE);
-
dataPermissionMapper.insertSelective(DataPermissionDO.buildPermissionDO(dataPermissionDTO));
- });
+
grantMissingPermissions(namespaceUserRelDOList.stream().map(NamespaceUserRelDO::getUserId).collect(Collectors.toList()),
+ event.getSelector().getId(),
AdminConstants.SELECTOR_DATA_TYPE);
}
}
}
@@ -303,6 +299,7 @@ public class DataPermissionServiceImpl implements
DataPermissionService {
* @param event event
*/
@EventListener(RuleCreatedEvent.class)
+ @Transactional(rollbackFor = Exception.class)
public void onRuleCreated(final RuleCreatedEvent event) {
// check rule add
Boolean existed;
@@ -316,26 +313,32 @@ public class DataPermissionServiceImpl implements
DataPermissionService {
dataPermissionDTO.setUserId(JwtUtils.getUserInfo().getUserId());
dataPermissionDTO.setDataId(event.getRule().getId());
dataPermissionDTO.setDataType(AdminConstants.RULE_DATA_TYPE);
-
dataPermissionMapper.insertSelective(DataPermissionDO.buildPermissionDO(dataPermissionDTO));
+
grantMissingPermissions(Collections.singletonList(dataPermissionDTO.getUserId()),
dataPermissionDTO.getDataId(), dataPermissionDTO.getDataType());
} else {
String namespaceId = event.getRule().getNamespaceId();
if (StringUtils.isNoneBlank(namespaceId)) {
// support namespace
List<NamespaceUserRelDO> namespaceUserRelDOList =
namespaceUserRelMapper.selectListByNamespaceId(namespaceId);
if (CollectionUtils.isNotEmpty(namespaceUserRelDOList)) {
- namespaceUserRelDOList.forEach(namespaceUserRelDO -> {
- DataPermissionDTO dataPermissionDTO = new
DataPermissionDTO();
-
dataPermissionDTO.setUserId(namespaceUserRelDO.getUserId());
- dataPermissionDTO.setDataId(event.getRule().getId());
-
dataPermissionDTO.setDataType(AdminConstants.RULE_DATA_TYPE);
-
dataPermissionMapper.insertSelective(DataPermissionDO.buildPermissionDO(dataPermissionDTO));
- });
+
grantMissingPermissions(namespaceUserRelDOList.stream().map(NamespaceUserRelDO::getUserId).collect(Collectors.toList()),
+ event.getRule().getId(),
AdminConstants.RULE_DATA_TYPE);
}
}
}
}
+ private void grantMissingPermissions(final List<String> userIds, final
String dataId, final Integer dataType) {
+ Set<String> existingUsers = new
HashSet<>(dataPermissionMapper.selectUserIds(dataId, dataType));
+ List<DataPermissionDO> permissions = userIds.stream().distinct()
+ .filter(userId -> !existingUsers.contains(userId))
+ .map(userId ->
DataPermissionDO.buildCreatePermissionDO(dataId, userId, dataType))
+ .collect(Collectors.toList());
+ if (CollectionUtils.isNotEmpty(permissions)) {
+ dataPermissionMapper.insertBatch(permissions);
+ }
+ }
+
/**
* listen {@link BatchSelectorDeletedEvent} delete data permission.
*
diff --git a/shenyu-admin/src/main/resources/mappers/data-permission-sqlmap.xml
b/shenyu-admin/src/main/resources/mappers/data-permission-sqlmap.xml
index e2fd69ffc4..a8ca52367e 100644
--- a/shenyu-admin/src/main/resources/mappers/data-permission-sqlmap.xml
+++ b/shenyu-admin/src/main/resources/mappers/data-permission-sqlmap.xml
@@ -156,6 +156,12 @@
</trim>
</insert>
+ <select id="selectUserIds" resultType="java.lang.String">
+ SELECT user_id FROM data_permission
+ WHERE data_id = #{dataId,jdbcType=VARCHAR}
+ AND data_type = #{dataType,jdbcType=INTEGER}
+ </select>
+
<insert id="insertBatch">
INSERT INTO data_permission (id,
user_id,
@@ -172,4 +178,15 @@
</foreach>
</insert>
+ <insert id="insertBatch" databaseId="oracle">
+ INSERT INTO data_permission (id, user_id, data_id, data_type)
+ <foreach collection="dataPermissionList" item="permission" separator="
UNION ALL ">
+ SELECT CAST(#{permission.id,jdbcType=VARCHAR} AS VARCHAR2(128)),
+ CAST(#{permission.userId,jdbcType=VARCHAR} AS
VARCHAR2(128)),
+ CAST(#{permission.dataId,jdbcType=VARCHAR} AS
VARCHAR2(128)),
+ CAST(#{permission.dataType,jdbcType=INTEGER} AS NUMBER(10))
+ FROM dual
+ </foreach>
+ </insert>
+
</mapper>
diff --git
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/DataPermissionBatchDialectTest.java
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/DataPermissionBatchDialectTest.java
new file mode 100644
index 0000000000..0e598f12d5
--- /dev/null
+++
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/DataPermissionBatchDialectTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.shenyu.admin.mapper;
+
+import org.apache.ibatis.builder.xml.XMLMapperBuilder;
+import org.apache.ibatis.mapping.Environment;
+import org.apache.ibatis.session.Configuration;
+import org.apache.ibatis.session.SqlSession;
+import org.apache.ibatis.session.SqlSessionFactoryBuilder;
+import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
+import org.apache.shenyu.admin.model.entity.DataPermissionDO;
+import org.junit.jupiter.api.Test;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.datasource.DriverManagerDataSource;
+
+import java.io.InputStream;
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Oracle-mode regression for batch permission grants.
+ */
+public class DataPermissionBatchDialectTest {
+
+ @Test
+ public void testOracleBatchInsertAndUserLookup() throws Exception {
+ DriverManagerDataSource dataSource = new
DriverManagerDataSource("jdbc:h2:mem:permission-" + UUID.randomUUID()
+ + ";DB_CLOSE_DELAY=-1;MODE=Oracle", "sa", "");
+ JdbcTemplate jdbc = new JdbcTemplate(dataSource);
+ try {
+ jdbc.execute("CREATE TABLE data_permission (id VARCHAR(128)
PRIMARY KEY, user_id VARCHAR(128) NOT NULL, data_id VARCHAR(128), data_type
INTEGER)");
+ Configuration configuration = new Configuration(new
Environment("test", new JdbcTransactionFactory(), dataSource));
+ configuration.setDatabaseId("oracle");
+ ClassPathResource resource = new
ClassPathResource("mappers/data-permission-sqlmap.xml");
+ try (InputStream input = resource.getInputStream()) {
+ new XMLMapperBuilder(input, configuration,
resource.toString(), configuration.getSqlFragments()).parse();
+ }
+ try (SqlSession session = new
SqlSessionFactoryBuilder().build(configuration).openSession()) {
+ DataPermissionMapper mapper =
session.getMapper(DataPermissionMapper.class);
+ assertEquals(2,
mapper.insertBatch(List.of(DataPermissionDO.buildCreatePermissionDO("data",
"first", 0),
+ DataPermissionDO.buildCreatePermissionDO("data",
"second", 0))));
+ assertEquals(2, mapper.selectUserIds("data", 0).size());
+ assertEquals(0, mapper.selectUserIds("data", 1).size());
+ }
+ } finally {
+ jdbc.execute("SHUTDOWN");
+ }
+ }
+}
+
diff --git
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/PermissionListenerIntegrationTest.java
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/PermissionListenerIntegrationTest.java
new file mode 100644
index 0000000000..2d686305b9
--- /dev/null
+++
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/PermissionListenerIntegrationTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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.shenyu.admin.service;
+
+import jakarta.annotation.Resource;
+import org.apache.shenyu.admin.AbstractSpringIntegrationTest;
+import org.apache.shenyu.admin.mapper.NamespaceUserRelMapper;
+import org.apache.shenyu.admin.model.entity.NamespaceUserRelDO;
+import org.apache.shenyu.admin.model.entity.RuleDO;
+import org.apache.shenyu.admin.model.entity.SelectorDO;
+import org.apache.shenyu.admin.model.event.rule.RuleCreatedEvent;
+import org.apache.shenyu.admin.model.event.selector.SelectorCreatedEvent;
+import org.apache.shenyu.admin.service.impl.DataPermissionServiceImpl;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.support.TransactionTemplate;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.when;
+
+/**
+ * Verify permission listeners grant namespace users atomically.
+ */
+public class PermissionListenerIntegrationTest extends
AbstractSpringIntegrationTest {
+
+ @Resource
+ private DataPermissionServiceImpl permissionService;
+
+ @Resource
+ private JdbcTemplate jdbcTemplate;
+
+ @Resource
+ private PlatformTransactionManager transactionManager;
+
+ @MockBean
+ private NamespaceUserRelMapper namespaceUserRelMapper;
+
+ @AfterEach
+ public void cleanup() {
+ jdbcTemplate.update("DELETE FROM data_permission WHERE data_id =
'listener-data'");
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ public void testRepeatedEventDoesNotDuplicateGrants(final boolean rule) {
+
when(namespaceUserRelMapper.selectListByNamespaceId("listener-namespace")).thenReturn(List.of(user("first"),
user("second"), user("first")));
+ invokeListener(rule);
+ invokeListener(rule);
+ assertEquals(2, countPermissions());
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ public void testInvalidUserRollsBackAllGrants(final boolean rule) {
+
when(namespaceUserRelMapper.selectListByNamespaceId("listener-namespace")).thenReturn(List.of(user("first"),
user(null)));
+ assertThrows(DataIntegrityViolationException.class, () ->
invokeListener(rule));
+ assertEquals(0, countPermissions());
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ public void testOuterRollbackRemovesAllGrants(final boolean rule) {
+
when(namespaceUserRelMapper.selectListByNamespaceId("listener-namespace")).thenReturn(List.of(user("first"),
user("second")));
+ new
TransactionTemplate(transactionManager).executeWithoutResult(status -> {
+ invokeListener(rule);
+ assertEquals(2, countPermissions());
+ status.setRollbackOnly();
+ });
+ assertEquals(0, countPermissions());
+ }
+
+ private void invokeListener(final boolean rule) {
+ if (rule) {
+ RuleDO data = new RuleDO();
+ data.setId("listener-data");
+ data.setNamespaceId("listener-namespace");
+ permissionService.onRuleCreated(new RuleCreatedEvent(data,
"test"));
+ } else {
+ SelectorDO data = new SelectorDO();
+ data.setId("listener-data");
+ data.setNamespaceId("listener-namespace");
+ permissionService.onSelectorCreated(new SelectorCreatedEvent(data,
"test"));
+ }
+ }
+
+ private NamespaceUserRelDO user(final String id) {
+ NamespaceUserRelDO user = new NamespaceUserRelDO();
+ user.setUserId(id);
+ return user;
+ }
+
+ private int countPermissions() {
+ return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM
data_permission WHERE data_id = 'listener-data'", Integer.class);
+ }
+}
+