chore: 并入 iotlcm-worker-svr 历史到 worker-svr/(原提交 96f0f4e)
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
package rolesync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/library/rolepolicy"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/entity"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func SyncInheritedRole(ctx context.Context, message *model.AgentRoleSyncMessage) error {
|
||||
if message == nil || message.SourceRoleId <= 0 || message.SourceDeptId <= 0 || message.TenantId == "" {
|
||||
return gerror.New("invalid agent role sync message")
|
||||
}
|
||||
template, templateErr := rolepolicy.Template(ctx)
|
||||
if templateErr != nil {
|
||||
return templateErr
|
||||
}
|
||||
if message.TemplateRole && (template == nil || message.SourceRoleId != template.RoleId) {
|
||||
message.Superseded = true
|
||||
return nil
|
||||
}
|
||||
if template != nil && message.SourceRoleId == template.RoleId && message.SourceDeptId == template.DeptId && (message.TenantId == template.TenantId || message.TenantId == "") {
|
||||
return syncTemplateRoles(ctx, template, message)
|
||||
}
|
||||
if template != nil {
|
||||
message.Superseded = true
|
||||
return nil
|
||||
}
|
||||
var source *entity.SysRole
|
||||
sourceModel := dao.SysRole.Ctx(ctx).Master().Where(dao.SysRole.Columns().RoleId, message.SourceRoleId)
|
||||
if message.TenantId != "" {
|
||||
sourceModel = sourceModel.Where(dao.SysRole.Columns().TenantId, message.TenantId)
|
||||
}
|
||||
if err := sourceModel.Scan(&source); err != nil {
|
||||
return err
|
||||
}
|
||||
if source == nil || source.IsPass != 1 || source.DeletedAt != nil {
|
||||
message.Superseded = true
|
||||
return nil
|
||||
}
|
||||
if message.SourceDeptId > 0 && message.SourceDeptId != source.DeptId {
|
||||
return gerror.New("role sync source department changed")
|
||||
}
|
||||
var children []*entity.SysDept
|
||||
if err := dao.SysDept.Ctx(ctx).Master().
|
||||
Where(dao.SysDept.Columns().TenantId, source.TenantId).
|
||||
Where(dao.SysDept.Columns().ParentId, source.DeptId).
|
||||
Where(dao.SysDept.Columns().DeptType, 1).
|
||||
Scan(&children); err != nil {
|
||||
return err
|
||||
}
|
||||
failed := 0
|
||||
if len(message.ChildDeptIds) == 0 {
|
||||
message.TotalCount = len(children)
|
||||
message.SuccessCount = 0
|
||||
}
|
||||
failedDeptIds := make([]int64, 0)
|
||||
var firstErr error
|
||||
for _, child := range children {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(message.ChildDeptIds) > 0 && !slices.Contains(message.ChildDeptIds, child.DeptId) {
|
||||
continue
|
||||
}
|
||||
err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
return syncInheritedRoleChildrenTx(ctx, tx, message.SourceRoleId, source.TenantId, source.DeptId, message.ActorId, child.DeptId)
|
||||
})
|
||||
if err != nil {
|
||||
failed++
|
||||
failedDeptIds = append(failedDeptIds, child.DeptId)
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
g.Log().Warningf(ctx, "agent role sync child failed, sourceRoleId:%d, childDeptId:%d, err:%v", source.RoleId, child.DeptId, err)
|
||||
} else {
|
||||
message.SuccessCount++
|
||||
}
|
||||
}
|
||||
message.ChildDeptIds = failedDeptIds
|
||||
message.FailedCount = len(failedDeptIds)
|
||||
if firstErr != nil {
|
||||
return gerror.Wrapf(firstErr, "agent role sync failed for %d of %d children", failed, len(children))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func syncInheritedRoleChildrenTx(ctx context.Context, tx gdb.TX, sourceRoleID int64, tenantId string, sourceDeptId, actorId, childDeptId int64) error {
|
||||
if sourceRoleID == 0 {
|
||||
return nil
|
||||
}
|
||||
var source *entity.SysRole
|
||||
sourceModel := tx.Ctx(ctx).Model(dao.SysRole.Table()).Where(dao.SysRole.Columns().RoleId, sourceRoleID)
|
||||
if tenantId != "" {
|
||||
sourceModel = sourceModel.Where(dao.SysRole.Columns().TenantId, tenantId)
|
||||
}
|
||||
if !strings.EqualFold(g.DB().GetConfig().Type, "sqlite") {
|
||||
sourceModel = sourceModel.LockUpdate()
|
||||
}
|
||||
if err := sourceModel.Scan(&source); err != nil {
|
||||
return err
|
||||
}
|
||||
if source == nil || source.RoleId == 0 || source.DeletedAt != nil || source.IsPass != 1 {
|
||||
return nil
|
||||
}
|
||||
if sourceDeptId > 0 && source.DeptId != sourceDeptId {
|
||||
return gerror.New("role sync source department changed")
|
||||
}
|
||||
var childDepts []*entity.SysDept
|
||||
childDeptModel := tx.Ctx(ctx).Model(dao.SysDept.Table()).Where(dao.SysDept.Columns().TenantId, source.TenantId).Where(dao.SysDept.Columns().ParentId, source.DeptId).Where(dao.SysDept.Columns().DeptType, 1).Where(dao.SysDept.Columns().DeptId, childDeptId)
|
||||
if !strings.EqualFold(g.DB().GetConfig().Type, "sqlite") {
|
||||
childDeptModel = childDeptModel.LockUpdate()
|
||||
}
|
||||
if err := childDeptModel.Scan(&childDepts); err != nil {
|
||||
return err
|
||||
}
|
||||
var menus []*entity.SysRoleMenu
|
||||
if err := tx.Ctx(ctx).Model(dao.SysRoleMenu.Table()).Where(dao.SysRoleMenu.Columns().RoleId, sourceRoleID).OrderAsc(dao.SysRoleMenu.Columns().MenuId).Scan(&menus); err != nil {
|
||||
return err
|
||||
}
|
||||
menuIDs := make([]int64, 0, len(menus))
|
||||
for _, menu := range menus {
|
||||
menuIDs = append(menuIDs, menu.MenuId)
|
||||
}
|
||||
allowed, err := rolepolicy.RestrictMenus(ctx, source.TenantId, source.DeptId, menuIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
template, templateErr := rolepolicy.Template(ctx)
|
||||
if templateErr != nil {
|
||||
return templateErr
|
||||
}
|
||||
filtered := make([]*entity.SysRoleMenu, 0, len(menus))
|
||||
for _, menu := range menus {
|
||||
if slices.Contains(allowed, menu.MenuId) {
|
||||
copy := *menu
|
||||
if template != nil {
|
||||
copy.DataScope = consts.SysRoleMenuDataScopeRole
|
||||
}
|
||||
filtered = append(filtered, ©)
|
||||
}
|
||||
}
|
||||
menus = filtered
|
||||
for _, childDept := range childDepts {
|
||||
var children []*entity.SysRole
|
||||
if err := tx.Ctx(ctx).Model(dao.SysRole.Table()+" r").
|
||||
Fields("r.*").
|
||||
Where("r.tenant_id", source.TenantId).
|
||||
Where("r.dept_id", childDept.DeptId).
|
||||
Where("r.created_dept", source.DeptId).
|
||||
Where("r.role_id", childDept.DeptRole).
|
||||
Scan(&children); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(children) > 1 {
|
||||
return gerror.Newf("multiple inherited roles in department %d for role %d", childDept.DeptId, source.RoleId)
|
||||
}
|
||||
if len(children) == 0 {
|
||||
var legacy *entity.SysRole
|
||||
if childDept.DeptRole > 0 {
|
||||
if err := tx.Ctx(ctx).Model(dao.SysRole.Table()).
|
||||
Where(dao.SysRole.Columns().RoleId, childDept.DeptRole).
|
||||
Where(dao.SysRole.Columns().DeptId, childDept.DeptId).
|
||||
Where(dao.SysRole.Columns().TenantId, source.TenantId).
|
||||
Scan(&legacy); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if legacy != nil && legacy.RoleId > 0 {
|
||||
if legacy.CreatedDept != source.DeptId || legacy.RoleKey != source.RoleKey {
|
||||
return gerror.Newf("cannot identify inherited role for department %d", childDept.DeptId)
|
||||
}
|
||||
children = []*entity.SysRole{legacy}
|
||||
} else {
|
||||
if childDept.DeptRole > 0 {
|
||||
return gerror.Newf("missing default role for department %d", childDept.DeptId)
|
||||
}
|
||||
return gerror.Newf("missing default role for department %d", childDept.DeptId)
|
||||
}
|
||||
}
|
||||
for _, child := range children {
|
||||
if _, err := tx.Ctx(ctx).Model(dao.SysRole.Table()).Where(dao.SysRole.Columns().RoleId, child.RoleId).Data(do.SysRole{RoleName: strings.TrimSpace(source.RoleName), RoleKey: source.RoleKey, RoleSort: source.RoleSort, DataScope: source.DataScope, MenuCheckStrictly: source.MenuCheckStrictly, DeptCheckStrictly: source.DeptCheckStrictly, Status: source.Status, Remark: source.Remark, UpdatedBy: actorId}).OmitNil().Update(); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Ctx(ctx).Model(dao.SysRoleMenu.Table()).Where(dao.SysRoleMenu.Columns().RoleId, child.RoleId).Delete(); err != nil {
|
||||
return err
|
||||
}
|
||||
menuData := make([]*do.SysRoleMenu, 0, len(menus))
|
||||
for _, menu := range menus {
|
||||
menuData = append(menuData, &do.SysRoleMenu{TenantId: source.TenantId, RoleId: child.RoleId, MenuId: menu.MenuId, DataScope: menu.DataScope})
|
||||
}
|
||||
if len(menuData) > 0 {
|
||||
if _, err := tx.Ctx(ctx).Model(dao.SysRoleMenu.Table()).Data(menuData).OmitNil().Batch(200).Insert(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
package rolesync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/library/rolepolicy"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/entity"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTemplateSwitchOverwritesDefaultRolesAndPreservesOtherRoles(t *testing.T) {
|
||||
setupAgentRoleSyncTestDB(t)
|
||||
ctx := context.Background()
|
||||
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_dept(dept_id,tenant_id,parent_id,dept_type,dept_role) VALUES (1,'000000',0,1,0),(2,'000000',1,1,21),(3,'000000',2,1,31),(4,'000000',0,1,41)")
|
||||
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role(role_id,tenant_id,dept_id,role_name,role_key,status,data_scope,is_pass,created_dept) VALUES (10,'000000',1,'模板','template','0','4',0,1),(11,'000000',1,'新模板','next','0','4',0,1),(21,'000000',2,'管理员','admin','0','4',0,1),(31,'000000',3,'三级管理员','admin','0','4',0,2),(22,'000000',2,'财务','finance','0','3',0,2),(41,'000000',4,'其他机构','other','0','4',0,4)")
|
||||
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_config(tenant_id,config_key,config_value) VALUES ('000000','agent.role.template.role_id','10')")
|
||||
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role_menu(tenant_id,role_id,menu_id,data_scope) VALUES ('000000',10,100,'0'),('000000',10,200,'0'),('000000',11,200,'0'),('000000',22,100,'3'),('000000',41,100,'0')")
|
||||
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_user_role(user_id,role_id) VALUES(99,21)")
|
||||
message := &model.AgentRoleSyncMessage{TenantId: "000000", SourceRoleId: 10, SourceDeptId: 1, TemplateRole: true}
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
if err := SyncInheritedRole(ctx, message); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if message.TotalCount != 2 || message.SuccessCount != 2 {
|
||||
t.Fatalf("unexpected hierarchy counts: %+v", message)
|
||||
}
|
||||
for _, roleID := range []int64{21, 31} {
|
||||
assertAgentRoleSyncCount(t, ctx, fmt.Sprintf("SELECT COUNT(*) FROM sys_role_menu WHERE role_id=%d AND menu_id IN(100,200)", roleID), 2)
|
||||
}
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id=22 AND menu_id=200", 0)
|
||||
mustExecAgentRoleSyncTest(t, ctx, "UPDATE sys_config SET config_value='11'")
|
||||
ids, err := rolepolicy.RestrictMenus(ctx, "000000", 3, []int64{100, 200})
|
||||
if err != nil || !reflect.DeepEqual(ids, []int64{100, 200}) {
|
||||
t.Fatalf("template switch must wait for confirmed distribution: %v %v", ids, err)
|
||||
}
|
||||
if err := SyncInheritedRole(ctx, message); err != nil || !message.Superseded {
|
||||
t.Fatalf("obsolete template must not propagate: %+v %v", message, err)
|
||||
}
|
||||
next := &model.AgentRoleSyncMessage{TenantId: "000000", SourceRoleId: 11, SourceDeptId: 1, TemplateRole: true}
|
||||
if err := SyncInheritedRole(ctx, next); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, roleID := range []int64{21, 31} {
|
||||
assertAgentRoleSyncCount(t, ctx, fmt.Sprintf("SELECT COUNT(*) FROM sys_role_menu WHERE role_id=%d AND menu_id=100", roleID), 0)
|
||||
assertAgentRoleSyncCount(t, ctx, fmt.Sprintf("SELECT COUNT(*) FROM sys_role_menu WHERE role_id=%d AND menu_id=200", roleID), 1)
|
||||
}
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role WHERE role_id=31 AND created_dept=2", 1)
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id=22 AND menu_id=100", 1)
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id=41 AND menu_id=100", 1)
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_user_role WHERE user_id=99 AND role_id=21", 1)
|
||||
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role(role_id,tenant_id,dept_id,is_pass,status,data_scope) VALUES(25,'000000',2,1,'0','4')")
|
||||
legacy := &model.AgentRoleSyncMessage{TenantId: "000000", SourceRoleId: 25, SourceDeptId: 2}
|
||||
if err := SyncInheritedRole(ctx, legacy); err != nil || !legacy.Superseded {
|
||||
t.Fatalf("legacy local templates must not overwrite configured hierarchy: %+v %v", legacy, err)
|
||||
}
|
||||
mustExecAgentRoleSyncTest(t, ctx, "UPDATE sys_config SET config_value='invalid'")
|
||||
if _, err := rolepolicy.RestrictMenus(ctx, "000000", 3, []int64{200}); err == nil {
|
||||
t.Fatal("invalid template must deny")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncInheritedRoleOnlyUpdatesDirectEnterpriseChildren(t *testing.T) {
|
||||
setupAgentRoleSyncTestDB(t)
|
||||
ctx := context.Background()
|
||||
|
||||
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, dept_role) VALUES
|
||||
(1, '000000', 0, 1, 0),
|
||||
(2, '000000', 1, 1, 21),
|
||||
(3, '000000', 2, 1, 0),
|
||||
(4, '000000', 1, 0, 0)`)
|
||||
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_role
|
||||
(role_id, tenant_id, dept_id, role_name, role_key, role_sort, data_scope, menu_check_strictly, dept_check_strictly, status, is_pass, created_dept, created_by, updated_by, remark)
|
||||
VALUES
|
||||
(10, '000000', 1, '企业客户', 'enterpriseCustomer', 1, '4', 1, 1, '0', 1, 1, 1, 1, 'source'),
|
||||
(20, '000000', 2, '下级自建角色', 'localRole', 5, '3', 1, 1, '0', 0, 2, 2, 2, 'local'),
|
||||
(21, '000000', 2, '旧下发角色', 'enterpriseCustomer', 1, '4', 1, 1, '0', 0, 1, 1, 1, 'legacy')`)
|
||||
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_role_menu (tenant_id, role_id, menu_id, data_scope) VALUES ('000000', 10, 100, '7')`)
|
||||
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_role_menu (tenant_id, role_id, menu_id, data_scope) VALUES ('000000', 21, 200, '7')`)
|
||||
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_user_role (user_id, role_id) VALUES (99, 21)`)
|
||||
|
||||
message := &model.AgentRoleSyncMessage{TenantId: "000000", SourceRoleId: 10, SourceDeptId: 1, ActorId: 9}
|
||||
if err := SyncInheritedRole(ctx, message); err != nil {
|
||||
t.Fatalf("first sync failed: %v", err)
|
||||
}
|
||||
if err := SyncInheritedRole(ctx, message); err != nil {
|
||||
t.Fatalf("second sync failed: %v", err)
|
||||
}
|
||||
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role WHERE dept_id = 2 AND created_dept = 1", 1)
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role WHERE role_id = 21 AND created_dept = 1", 1)
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_user_role WHERE user_id = 99 AND role_id = 21", 1)
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id = 21 AND menu_id = 200", 0)
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role WHERE dept_id = 3 AND created_dept = 1", 0)
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role WHERE dept_id = 4 AND created_dept = 1", 0)
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role WHERE role_id = 20 AND role_name = '下级自建角色' AND created_dept = 2", 1)
|
||||
|
||||
menuScope, err := g.DB().GetValue(ctx, `SELECT rm.data_scope FROM sys_role_menu rm JOIN sys_role r ON r.role_id = rm.role_id WHERE r.dept_id = 2 AND r.created_dept = 1 AND rm.menu_id = 100`)
|
||||
if err != nil {
|
||||
t.Fatalf("query inherited menu data scope failed: %v", err)
|
||||
}
|
||||
if menuScope.String() != "7" {
|
||||
t.Fatalf("inherited menu data scope = %q, want 7", menuScope.String())
|
||||
}
|
||||
|
||||
mustExecAgentRoleSyncTest(t, ctx, `UPDATE sys_role SET role_name = '停止下发后的名称', is_pass = 0 WHERE role_id = 10`)
|
||||
if err := SyncInheritedRole(ctx, message); err != nil {
|
||||
t.Fatalf("disabled source sync failed: %v", err)
|
||||
}
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role WHERE dept_id = 2 AND created_dept = 1 AND role_name = '企业客户'", 1)
|
||||
}
|
||||
|
||||
func TestSyncInheritedRoleRejectsUnrelatedDefaultRole(t *testing.T) {
|
||||
setupAgentRoleSyncTestDB(t)
|
||||
ctx := context.Background()
|
||||
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, dept_role) VALUES (1, '000000', 0, 1, 0), (2, '000000', 1, 1, 21)`)
|
||||
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_role (role_id, tenant_id, dept_id, role_name, role_key, is_pass, created_dept) VALUES (10, '000000', 1, '下发角色', 'source', 1, 1), (21, '000000', 2, '自建默认角色', 'local', 0, 2)`)
|
||||
err := SyncInheritedRole(ctx, &model.AgentRoleSyncMessage{TenantId: "000000", SourceRoleId: 10, SourceDeptId: 1})
|
||||
if err == nil {
|
||||
t.Fatal("expected ambiguous default role to be rejected")
|
||||
}
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role WHERE role_id = 21 AND created_dept = 2 AND role_name = '自建默认角色'", 1)
|
||||
}
|
||||
|
||||
func TestTemplateSyncReportsMissingRoles(t *testing.T) {
|
||||
setupAgentRoleSyncTestDB(t)
|
||||
ctx := context.Background()
|
||||
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_dept (dept_id,tenant_id,parent_id,dept_type,dept_role) VALUES (1,'000000',0,1,10),(2,'000000',1,1,0),(3,'000000',1,1,31)")
|
||||
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role (role_id,tenant_id,dept_id,status,data_scope,is_pass) VALUES (10,'000000',1,'0','4',1),(31,'000000',3,'0','4',0)")
|
||||
mustExecAgentRoleSyncTest(t, ctx, "UPDATE sys_role SET created_dept=1 WHERE role_id=31")
|
||||
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_config (config_id,tenant_id,config_key,config_value) VALUES (1,'000000','agent.role.template.role_id','10')")
|
||||
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role_menu (tenant_id,role_id,menu_id,data_scope) VALUES ('000000',10,200,'0')")
|
||||
if err := SyncInheritedRole(ctx, &model.AgentRoleSyncMessage{TenantId: "000000", SourceRoleId: 10, SourceDeptId: 1}); err == nil {
|
||||
t.Fatal("missing default role must report partial failure")
|
||||
}
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id=31 AND menu_id=200", 1)
|
||||
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role (role_id,tenant_id,dept_id,status,data_scope) VALUES (21,'000000',2,'0','4')")
|
||||
mustExecAgentRoleSyncTest(t, ctx, "UPDATE sys_role SET created_dept=1 WHERE role_id=21")
|
||||
mustExecAgentRoleSyncTest(t, ctx, "UPDATE sys_dept SET dept_role=21 WHERE dept_id=2")
|
||||
if err := SyncInheritedRole(ctx, &model.AgentRoleSyncMessage{TenantId: "000000", SourceRoleId: 10, SourceDeptId: 1}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id=31 AND menu_id=200", 1)
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id=21 AND menu_id=200", 1)
|
||||
}
|
||||
func setupAgentRoleSyncTestDB(t *testing.T) {
|
||||
t.Helper()
|
||||
if err := gdb.SetConfig(gdb.Config{
|
||||
"default": gdb.ConfigGroup{
|
||||
{Type: "sqlite", Link: "sqlite::@file(:memory:)", Debug: false},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("configure sqlite failed: %v", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
t.Cleanup(func() { _ = g.DB().Close(ctx) })
|
||||
mustExecAgentRoleSyncTest(t, ctx, `CREATE TABLE sys_dept (dept_id INTEGER PRIMARY KEY, tenant_id TEXT, parent_id INTEGER, dept_type INTEGER, status TEXT DEFAULT '0', dept_role INTEGER, deleted_at DATETIME)`)
|
||||
mustExecAgentRoleSyncTest(t, ctx, `CREATE TABLE sys_config (config_id INTEGER PRIMARY KEY, tenant_id TEXT, config_name TEXT, config_key TEXT, config_value TEXT, config_type TEXT, created_dept INTEGER, created_by INTEGER, created_at DATETIME, updated_by INTEGER, updated_at DATETIME, remark TEXT)`)
|
||||
mustExecAgentRoleSyncTest(t, ctx, `CREATE TABLE sys_role (
|
||||
role_id INTEGER PRIMARY KEY AUTOINCREMENT, tenant_id TEXT, dept_id INTEGER, role_name TEXT, role_key TEXT, role_sort INTEGER, data_scope TEXT,
|
||||
menu_check_strictly INTEGER, dept_check_strictly INTEGER, status TEXT, is_pass INTEGER, created_dept INTEGER, created_by INTEGER, created_at DATETIME, updated_by INTEGER, updated_at DATETIME, deleted_by INTEGER, deleted_at DATETIME, remark TEXT)`)
|
||||
mustExecAgentRoleSyncTest(t, ctx, `CREATE TABLE sys_role_menu (tenant_id TEXT, role_id INTEGER, menu_id INTEGER, data_scope TEXT)`)
|
||||
mustExecAgentRoleSyncTest(t, ctx, `CREATE TABLE sys_role_dept (tenant_id TEXT, role_id INTEGER, dept_id INTEGER)`)
|
||||
mustExecAgentRoleSyncTest(t, ctx, `CREATE TABLE sys_user_role (tenant_id TEXT DEFAULT '000000', user_id INTEGER, role_id INTEGER)`)
|
||||
}
|
||||
|
||||
func mustExecAgentRoleSyncTest(t *testing.T, ctx context.Context, sql string) {
|
||||
t.Helper()
|
||||
if _, err := g.DB().Exec(ctx, sql); err != nil {
|
||||
t.Fatalf("execute sql failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertAgentRoleSyncCount(t *testing.T, ctx context.Context, sql string, expected int) {
|
||||
t.Helper()
|
||||
count, err := g.DB().GetValue(ctx, sql)
|
||||
if err != nil {
|
||||
t.Fatalf("query count failed: %v", err)
|
||||
}
|
||||
if count.Int() != expected {
|
||||
t.Fatalf("count = %d, want %d, sql=%s", count.Int(), expected, sql)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoleSyncRetriesOnlyFailedDepartments(t *testing.T) {
|
||||
setupAgentRoleSyncTestDB(t)
|
||||
ctx := context.Background()
|
||||
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_dept (dept_id,tenant_id,parent_id,dept_type,dept_role) VALUES (1,'000000',0,1,0),(2,'000000',1,1,21),(3,'000000',1,1,31)`)
|
||||
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_role (role_id,tenant_id,dept_id,role_name,role_key,is_pass,created_dept) VALUES (10,'000000',1,'source','source',1,1),(21,'000000',2,'before','source',0,1),(31,'000000',3,'local','local',0,3)`)
|
||||
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_role_menu (tenant_id,role_id,menu_id,data_scope) VALUES ('000000',10,100,'7')`)
|
||||
message := &model.AgentRoleSyncMessage{TenantId: "000000", SourceRoleId: 10, SourceDeptId: 1}
|
||||
if err := SyncInheritedRole(ctx, message); err == nil {
|
||||
t.Fatal("expected invalid department failure")
|
||||
}
|
||||
if !reflect.DeepEqual(message.ChildDeptIds, []int64{3}) {
|
||||
t.Fatalf("unexpected failures: %v", message.ChildDeptIds)
|
||||
}
|
||||
mustExecAgentRoleSyncTest(t, ctx, `UPDATE sys_role SET role_name='already-successful' WHERE role_id=21`)
|
||||
mustExecAgentRoleSyncTest(t, ctx, `UPDATE sys_role SET created_dept=1,role_key='source' WHERE role_id=31`)
|
||||
if err := SyncInheritedRole(ctx, message); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertAgentRoleSyncCount(t, ctx, `SELECT COUNT(*) FROM sys_role WHERE role_id=21 AND role_name='already-successful'`, 1)
|
||||
assertAgentRoleSyncCount(t, ctx, `SELECT COUNT(*) FROM sys_role_menu WHERE role_id=31 AND menu_id=100`, 1)
|
||||
if len(message.ChildDeptIds) != 0 {
|
||||
t.Fatal("failure list must clear after success")
|
||||
}
|
||||
}
|
||||
|
||||
func TestObsoleteTemplateTaskDoesNotPropagateAsNormalRole(t *testing.T) {
|
||||
setupAgentRoleSyncTestDB(t)
|
||||
ctx := context.Background()
|
||||
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_dept (dept_id,tenant_id,parent_id,dept_type,dept_role) VALUES (1,'000000',0,1,10),(2,'000000',1,1,21)")
|
||||
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role (role_id,tenant_id,dept_id,role_key,is_pass,created_dept) VALUES (10,'000000',1,'source',1,1),(21,'000000',2,'source',0,1)")
|
||||
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role_menu (tenant_id,role_id,menu_id,data_scope) VALUES ('000000',10,100,'7'),('000000',21,200,'7')")
|
||||
message := &model.AgentRoleSyncMessage{TenantId: "000000", SourceRoleId: 10, SourceDeptId: 1, TemplateRole: true}
|
||||
if err := SyncInheritedRole(ctx, message); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !message.Superseded {
|
||||
t.Fatal("obsolete template task must be marked superseded")
|
||||
}
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id=21 AND menu_id=200", 1)
|
||||
}
|
||||
|
||||
func TestMissingDefaultRoleDoesNotCreateOrphanRole(t *testing.T) {
|
||||
setupAgentRoleSyncTestDB(t)
|
||||
ctx := context.Background()
|
||||
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_dept(dept_id,tenant_id,parent_id,dept_type,dept_role) VALUES(1,'000000',0,1,0),(2,'000000',1,1,0)`)
|
||||
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_role(role_id,tenant_id,dept_id,role_name,role_key,is_pass,created_dept) VALUES(10,'000000',1,'source','source',1,1)`)
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
if err := SyncInheritedRole(ctx, &model.AgentRoleSyncMessage{TenantId: "000000", SourceRoleId: 10, SourceDeptId: 1}); err == nil {
|
||||
t.Fatal("missing role must fail")
|
||||
}
|
||||
}
|
||||
assertAgentRoleSyncCount(t, ctx, `SELECT COUNT(*) FROM sys_role WHERE dept_id=2`, 0)
|
||||
}
|
||||
|
||||
func TestTemplateChildRevalidatesCurrentBindingAndSource(t *testing.T) {
|
||||
for _, change := range []string{
|
||||
"UPDATE sys_dept SET parent_id=99 WHERE dept_id=2",
|
||||
"UPDATE sys_dept SET dept_type=0 WHERE dept_id=2",
|
||||
"UPDATE sys_role SET status='1' WHERE role_id=10",
|
||||
"UPDATE sys_role_menu SET data_scope='1' WHERE role_id=10",
|
||||
} {
|
||||
t.Run(change, func(t *testing.T) {
|
||||
setupAgentRoleSyncTestDB(t)
|
||||
ctx := context.Background()
|
||||
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_dept(dept_id,tenant_id,parent_id,dept_type,dept_role) VALUES(1,'000000',0,1,0),(2,'000000',1,1,21)`)
|
||||
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_role(role_id,tenant_id,dept_id,status,data_scope,is_pass) VALUES(10,'000000',1,'0','4',1),(21,'000000',2,'0','3',0)`)
|
||||
mustExecAgentRoleSyncTest(t, ctx, "UPDATE sys_role SET created_dept=1 WHERE role_id=21")
|
||||
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_config(tenant_id,config_key,config_value) VALUES('000000','agent.role.template.role_id','10')`)
|
||||
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_role_menu(tenant_id,role_id,menu_id,data_scope) VALUES('000000',10,100,'0'),('000000',21,200,'0')`)
|
||||
mustExecAgentRoleSyncTest(t, ctx, change)
|
||||
err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
return syncTemplateChildTx(ctx, tx, &entity.SysRole{RoleId: 10, TenantId: "000000", DeptId: 1}, &entity.SysDept{DeptId: 2, DeptRole: 21, ParentId: 1}, 7)
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("changed source or binding must fail")
|
||||
}
|
||||
assertAgentRoleSyncCount(t, ctx, `SELECT COUNT(*) FROM sys_role_menu WHERE role_id=21 AND menu_id=200`, 1)
|
||||
assertAgentRoleSyncCount(t, ctx, `SELECT COUNT(*) FROM sys_role_menu WHERE role_id=21 AND menu_id=100`, 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeletedDefaultRoleIsNotRestoredBySynchronization(t *testing.T) {
|
||||
setupAgentRoleSyncTestDB(t)
|
||||
ctx := context.Background()
|
||||
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_dept(dept_id,tenant_id,parent_id,dept_type,dept_role) VALUES(1,'000000',0,1,0),(2,'000000',1,1,21)`)
|
||||
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_role(role_id,tenant_id,dept_id,role_name,role_key,is_pass,created_dept,deleted_at) VALUES(10,'000000',1,'source','source',1,1,NULL),(21,'000000',2,'deleted','source',0,1,'2026-10-13 00:00:00')`)
|
||||
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_role_menu(tenant_id,role_id,menu_id,data_scope) VALUES('000000',10,100,'0'),('000000',21,200,'0')`)
|
||||
if err := SyncInheritedRole(ctx, &model.AgentRoleSyncMessage{TenantId: "000000", SourceRoleId: 10, SourceDeptId: 1}); err == nil {
|
||||
t.Fatal("deleted default role must fail")
|
||||
}
|
||||
assertAgentRoleSyncCount(t, ctx, `SELECT COUNT(*) FROM sys_role_menu WHERE role_id=21 AND menu_id=200`, 1)
|
||||
assertAgentRoleSyncCount(t, ctx, `SELECT COUNT(*) FROM sys_role_menu WHERE role_id=21 AND menu_id=100`, 0)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package rolesync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/library/rabbitmq"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Start(ctx context.Context) (func(), error) {
|
||||
if !g.Cfg().MustGet(ctx, "roleSync.enabled", false).Bool() {
|
||||
return func() {}, nil
|
||||
}
|
||||
if !rabbitmq.Enabled(ctx) {
|
||||
return nil, gerror.New("权限同步 Worker 必须启用 RabbitMQ 并配置连接")
|
||||
}
|
||||
workerCtx, cancel := context.WithCancel(ctx)
|
||||
go superviseConsumer(workerCtx)
|
||||
g.Log().Info(ctx, "role sync Worker started")
|
||||
return cancel, nil
|
||||
}
|
||||
|
||||
func handleMessage(ctx context.Context, message model.AgentRoleSyncMessage) error {
|
||||
if err := syncRoleMessage(ctx, &message); err != nil {
|
||||
g.Log().Errorf(ctx, "role sync failed, taskId:%s, roleId:%d, attempt:%d, err:%+v", message.TaskId, message.SourceRoleId, message.RetryCount, err)
|
||||
message.RetryCount++
|
||||
body, marshalErr := json.Marshal(message)
|
||||
if marshalErr != nil {
|
||||
return marshalErr
|
||||
}
|
||||
if message.RetryCount > 5 {
|
||||
deadLetter, marshalErr := json.Marshal(model.AgentRoleSyncDeadLetterMessage{
|
||||
Message: message, Error: err.Error(), FailedAt: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
if marshalErr != nil {
|
||||
return marshalErr
|
||||
}
|
||||
return publishRoleDeadLetter(ctx, deadLetter)
|
||||
}
|
||||
delay := time.Duration(1<<uint(message.RetryCount-1)) * 5 * time.Second
|
||||
if publishErr := publishRoleRetry(ctx, delay, body); publishErr != nil {
|
||||
return gerror.Wrap(publishErr, fmt.Sprintf("role sync retry publish failed: %v", err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var consumeRoleSync = rabbitmq.ConsumeWithDone
|
||||
|
||||
var syncRoleMessage = SyncInheritedRole
|
||||
|
||||
var publishRoleRetry = func(ctx context.Context, delay time.Duration, body []byte) error {
|
||||
return rabbitmq.PublishDelayedBytesWithHeaders(ctx, consts.QueueAgentRoleSync+".retry", consts.QueueAgentRoleSync, delay, body, nil)
|
||||
}
|
||||
|
||||
var publishRoleDeadLetter = func(ctx context.Context, body []byte) error {
|
||||
return rabbitmq.PublishBytesWithHeaders(ctx, consts.QueueAgentRoleSync+".dlq", body, nil)
|
||||
}
|
||||
|
||||
func superviseConsumer(ctx context.Context) {
|
||||
for ctx.Err() == nil {
|
||||
done, err := consumeRoleSync(ctx, consts.QueueAgentRoleSync, "iotlcm-worker-agent-role-sync", func(ctx context.Context, body []byte) error {
|
||||
var message model.AgentRoleSyncMessage
|
||||
invalid := json.Unmarshal(body, &message)
|
||||
if invalid != nil || message.SourceRoleId <= 0 || message.SourceDeptId <= 0 || strings.TrimSpace(message.TenantId) == "" || message.RetryCount < 0 {
|
||||
return quarantineRoleSync(ctx, body)
|
||||
}
|
||||
if err := handleMessage(ctx, message); err != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err == nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-done:
|
||||
}
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
g.Log().Warningf(ctx, "role sync consumer disconnected, restarting: %v", err)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(3 * time.Second):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var quarantineRoleSync = func(ctx context.Context, body []byte) error {
|
||||
if err := publishRoleDeadLetter(ctx, body); err != nil {
|
||||
return gerror.Wrap(err, "保存无效权限消息到死信队列失败")
|
||||
}
|
||||
g.Log().Warning(ctx, "无效权限消息已隔离到死信队列")
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package rolesync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gcfg"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/library/rabbitmq"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestMessageRetryAndDeadLetter(t *testing.T) {
|
||||
originalSync, originalRetry, originalDead := syncRoleMessage, publishRoleRetry, publishRoleDeadLetter
|
||||
t.Cleanup(func() {
|
||||
syncRoleMessage, publishRoleRetry, publishRoleDeadLetter = originalSync, originalRetry, originalDead
|
||||
})
|
||||
syncRoleMessage = func(_ context.Context, message *model.AgentRoleSyncMessage) error {
|
||||
message.ChildDeptIds = []int64{200}
|
||||
return errors.New("child failed")
|
||||
}
|
||||
for attempt := 0; attempt <= 5; attempt++ {
|
||||
retries, deadLetters := 0, 0
|
||||
publishRoleRetry = func(_ context.Context, delay time.Duration, body []byte) error {
|
||||
retries++
|
||||
var message model.AgentRoleSyncMessage
|
||||
if err := json.Unmarshal(body, &message); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if message.RetryCount != attempt+1 || len(message.ChildDeptIds) != 1 || message.ChildDeptIds[0] != 200 || delay != time.Duration(1<<uint(attempt))*5*time.Second {
|
||||
t.Fatalf("invalid retry: %+v, delay=%s", message, delay)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
publishRoleDeadLetter = func(_ context.Context, body []byte) error {
|
||||
deadLetters++
|
||||
var dead model.AgentRoleSyncDeadLetterMessage
|
||||
if err := json.Unmarshal(body, &dead); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if dead.Error != "child failed" || dead.FailedAt == "" || dead.Message.RetryCount != 6 {
|
||||
t.Fatalf("invalid dead letter: %+v", dead)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := handleMessage(context.Background(), model.AgentRoleSyncMessage{RetryCount: attempt}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if (attempt < 5 && (retries != 1 || deadLetters != 0)) || (attempt == 5 && (retries != 0 || deadLetters != 1)) {
|
||||
t.Fatalf("unexpected destinations: retry=%d, dead=%d", retries, deadLetters)
|
||||
}
|
||||
}
|
||||
brokerError := errors.New("broker offline")
|
||||
publishRoleRetry = func(context.Context, time.Duration, []byte) error { return brokerError }
|
||||
publishRoleDeadLetter = func(context.Context, []byte) error { return brokerError }
|
||||
for _, attempt := range []int{0, 5} {
|
||||
if err := handleMessage(context.Background(), model.AgentRoleSyncMessage{RetryCount: attempt}); err == nil {
|
||||
t.Fatal("publication failure must prevent acknowledgement")
|
||||
}
|
||||
}
|
||||
syncRoleMessage = func(context.Context, *model.AgentRoleSyncMessage) error { return nil }
|
||||
if err := handleMessage(context.Background(), model.AgentRoleSyncMessage{}); err != nil {
|
||||
t.Fatalf("successful sync should not publish retries: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeConfiguration(t *testing.T) {
|
||||
original := g.Cfg().GetAdapter()
|
||||
t.Cleanup(func() { g.Cfg().SetAdapter(original) })
|
||||
for _, test := range []struct {
|
||||
content string
|
||||
wantError bool
|
||||
}{
|
||||
{"roleSync:\n enabled: false\n", false},
|
||||
{"roleSync:\n enabled: true\nrabbitmq:\n enabled: false\n", true},
|
||||
} {
|
||||
adapter, err := gcfg.NewAdapterContent(test.content)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
g.Cfg().SetAdapter(adapter)
|
||||
stop, err := Start(context.Background())
|
||||
if (err != nil) != test.wantError {
|
||||
t.Fatalf("unexpected initialization: %v", err)
|
||||
}
|
||||
if stop != nil {
|
||||
stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsumerReconnectsAfterConnectionFailureAndChannelClose(t *testing.T) {
|
||||
original := consumeRoleSync
|
||||
t.Cleanup(func() { consumeRoleSync = original })
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
reconnected := make(chan struct{})
|
||||
calls := 0
|
||||
consumeRoleSync = func(context.Context, string, string, rabbitmq.Handler) (<-chan struct{}, error) {
|
||||
calls++
|
||||
if calls == 1 {
|
||||
return nil, errors.New("broker offline")
|
||||
}
|
||||
if calls == 2 {
|
||||
closed := make(chan struct{})
|
||||
close(closed)
|
||||
return closed, nil
|
||||
}
|
||||
close(reconnected)
|
||||
return make(chan struct{}), nil
|
||||
}
|
||||
finished := make(chan struct{})
|
||||
go func() { defer close(finished); superviseConsumer(ctx) }()
|
||||
select {
|
||||
case <-reconnected:
|
||||
case <-time.After(10 * time.Second):
|
||||
cancel()
|
||||
<-finished
|
||||
t.Fatal("consumer did not reconnect")
|
||||
}
|
||||
cancel()
|
||||
<-finished
|
||||
if calls != 3 {
|
||||
t.Fatalf("unexpected restarts: %d", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidMessagesAreQuarantinedAndPublicationFailureIsRetried(t *testing.T) {
|
||||
originalConsume, originalQuarantine := consumeRoleSync, quarantineRoleSync
|
||||
t.Cleanup(func() { consumeRoleSync = originalConsume; quarantineRoleSync = originalQuarantine })
|
||||
for _, body := range []string{`broken json`, `{"sourceRoleId":18}`} {
|
||||
for _, publishFails := range []bool{false, true} {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
calls := 0
|
||||
quarantineRoleSync = func(context.Context, []byte) error {
|
||||
calls++
|
||||
if publishFails {
|
||||
return errors.New("broker offline")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
consumeRoleSync = func(ctx context.Context, queue, consumer string, handler rabbitmq.Handler) (<-chan struct{}, error) {
|
||||
cancel()
|
||||
err := handler(ctx, []byte(body))
|
||||
if (err != nil) != publishFails {
|
||||
t.Fatalf("unexpected quarantine result: %v", err)
|
||||
}
|
||||
done := make(chan struct{})
|
||||
close(done)
|
||||
return done, nil
|
||||
}
|
||||
superviseConsumer(ctx)
|
||||
cancel()
|
||||
if calls != 1 {
|
||||
t.Fatalf("invalid message not isolated: %d", calls)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package rolesync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/dao"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/library/rolepolicy"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/entity"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func syncTemplateRoles(ctx context.Context, template *entity.SysRole, message *model.AgentRoleSyncMessage) error {
|
||||
departments, err := templateDescendants(ctx, template)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
retrying := len(message.ChildDeptIds) > 0
|
||||
if !retrying {
|
||||
message.TotalCount = len(departments)
|
||||
message.SuccessCount = 0
|
||||
}
|
||||
pending := make(map[int64]bool, len(message.ChildDeptIds))
|
||||
for _, deptID := range message.ChildDeptIds {
|
||||
pending[deptID] = true
|
||||
}
|
||||
failed := make(map[int64]bool)
|
||||
failedDeptIDs := make([]int64, 0)
|
||||
var firstErr error
|
||||
for _, child := range departments {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if retrying && !pending[child.DeptId] {
|
||||
continue
|
||||
}
|
||||
delete(pending, child.DeptId)
|
||||
var syncErr error
|
||||
if failed[child.ParentId] {
|
||||
syncErr = gerror.Newf("上级机构%d尚未同步成功", child.ParentId)
|
||||
} else {
|
||||
syncErr = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
return syncTemplateChildTx(ctx, tx, template, child, message.ActorId)
|
||||
})
|
||||
}
|
||||
if syncErr != nil {
|
||||
failed[child.DeptId] = true
|
||||
failedDeptIDs = append(failedDeptIDs, child.DeptId)
|
||||
if firstErr == nil {
|
||||
firstErr = syncErr
|
||||
}
|
||||
g.Log().Errorf(ctx, "agent template sync child failed, deptId:%d, err:%v", child.DeptId, syncErr)
|
||||
continue
|
||||
}
|
||||
message.SuccessCount++
|
||||
}
|
||||
for deptID := range pending {
|
||||
failedDeptIDs = append(failedDeptIDs, deptID)
|
||||
if firstErr == nil {
|
||||
firstErr = gerror.Newf("待同步机构%d已不在当前模板的下级范围,请核对机构关系", deptID)
|
||||
}
|
||||
}
|
||||
slices.Sort(failedDeptIDs)
|
||||
message.ChildDeptIds = failedDeptIDs
|
||||
message.FailedCount = len(failedDeptIDs)
|
||||
if firstErr != nil {
|
||||
return gerror.Wrapf(firstErr, "代理模板同步失败企业数:%d", len(failedDeptIDs))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func templateDescendants(ctx context.Context, template *entity.SysRole) ([]*entity.SysDept, error) {
|
||||
queue := []int64{template.DeptId}
|
||||
seen := map[int64]bool{template.DeptId: true}
|
||||
departments := make([]*entity.SysDept, 0)
|
||||
for len(queue) > 0 {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parentID := queue[0]
|
||||
queue = queue[1:]
|
||||
var children []*entity.SysDept
|
||||
if err := dao.SysDept.Ctx(ctx).Master().Where("tenant_id", template.TenantId).Where("parent_id", parentID).Where("dept_type", 1).OrderAsc("dept_id").Scan(&children); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, child := range children {
|
||||
if seen[child.DeptId] {
|
||||
return nil, gerror.New("机构上下级存在循环")
|
||||
}
|
||||
seen[child.DeptId] = true
|
||||
queue = append(queue, child.DeptId)
|
||||
departments = append(departments, child)
|
||||
}
|
||||
}
|
||||
return departments, nil
|
||||
}
|
||||
|
||||
func syncTemplateChildTx(ctx context.Context, tx gdb.TX, template *entity.SysRole, child *entity.SysDept, actorID int64) error {
|
||||
if child.DeptRole <= 0 {
|
||||
return gerror.New("下级机构缺失默认角色")
|
||||
}
|
||||
lock := tx.Ctx(ctx).Model(dao.SysDept.Table()).Where("dept_id", child.DeptId).Where("tenant_id", template.TenantId)
|
||||
if !strings.EqualFold(g.DB().GetConfig().Type, "sqlite") {
|
||||
lock = lock.LockUpdate()
|
||||
}
|
||||
var locked *entity.SysDept
|
||||
if err := lock.Scan(&locked); err != nil {
|
||||
return err
|
||||
}
|
||||
if locked == nil || locked.DeptRole != child.DeptRole || locked.ParentId != child.ParentId || locked.DeptType != 1 {
|
||||
return gerror.New("下级机构角色绑定已变化")
|
||||
}
|
||||
currentLock := tx.Ctx(ctx).Model(dao.SysRole.Table()).Where("role_id", child.DeptRole).Where("tenant_id", template.TenantId).Where("dept_id", child.DeptId)
|
||||
if !strings.EqualFold(g.DB().GetConfig().Type, "sqlite") {
|
||||
currentLock = currentLock.LockUpdate()
|
||||
}
|
||||
var current *entity.SysRole
|
||||
if err := currentLock.Scan(¤t); err != nil {
|
||||
return err
|
||||
}
|
||||
if current == nil || current.Status != "0" {
|
||||
return gerror.New("下级机构默认角色不存在或已删除")
|
||||
}
|
||||
if current.CreatedDept != child.ParentId {
|
||||
return gerror.New("下级机构默认角色非直属上级继承角色,请核对角色来源")
|
||||
}
|
||||
configLock := tx.Ctx(ctx).Model(dao.SysConfig.Table()).Where("tenant_id", template.TenantId).Where("config_key", rolepolicy.TemplateConfigKey)
|
||||
if !strings.EqualFold(g.DB().GetConfig().Type, "sqlite") {
|
||||
configLock = configLock.LockUpdate()
|
||||
}
|
||||
var config *entity.SysConfig
|
||||
if err := configLock.Scan(&config); err != nil {
|
||||
return err
|
||||
}
|
||||
if config == nil || strings.TrimSpace(config.ConfigValue) != strconv.FormatInt(template.RoleId, 10) {
|
||||
return gerror.New("模板已切换,请重新同步")
|
||||
}
|
||||
templateLock := tx.Ctx(ctx).Model(dao.SysRole.Table()).Where("role_id", template.RoleId)
|
||||
if !strings.EqualFold(g.DB().GetConfig().Type, "sqlite") {
|
||||
templateLock = templateLock.LockUpdate()
|
||||
}
|
||||
var latest *entity.SysRole
|
||||
if err := templateLock.Scan(&latest); err != nil {
|
||||
return err
|
||||
}
|
||||
if latest == nil || latest.Status != "0" || latest.TenantId != template.TenantId || latest.DeptId != template.DeptId || latest.DataScope != consts.SysRoleDataScopeDeptAndBelow || (latest.CreatedDept > 0 && latest.CreatedDept != latest.DeptId) {
|
||||
return gerror.New("模板不可用")
|
||||
}
|
||||
menuLock := tx.Ctx(ctx).Model(dao.SysRoleMenu.Table()).Where("tenant_id", template.TenantId).Where("role_id", template.RoleId)
|
||||
if !strings.EqualFold(g.DB().GetConfig().Type, "sqlite") {
|
||||
menuLock = menuLock.LockUpdate()
|
||||
}
|
||||
var menus []*entity.SysRoleMenu
|
||||
if err := menuLock.Scan(&menus); err != nil {
|
||||
return err
|
||||
}
|
||||
// Every linked default role receives the same template snapshot. Individual
|
||||
// changes are intentionally replaced; unrelated roles are never updated.
|
||||
if _, err := tx.Ctx(ctx).Model(dao.SysRole.Table()).Where("tenant_id", template.TenantId).Where("role_id", current.RoleId).Data(do.SysRole{DataScope: consts.SysRoleDataScopeDeptAndBelow, UpdatedBy: actorID}).Update(); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Ctx(ctx).Model(dao.SysRoleMenu.Table()).Where("tenant_id", template.TenantId).Where("role_id", current.RoleId).Delete(); err != nil {
|
||||
return err
|
||||
}
|
||||
menuData := make([]*do.SysRoleMenu, 0, len(menus))
|
||||
for _, menu := range menus {
|
||||
if menu.DataScope == consts.SysRoleDataScopeAll || menu.DataScope == consts.SysRoleDataScopeCustom {
|
||||
return gerror.New("模板按钮数据范围已变化,请重新同步")
|
||||
}
|
||||
menuData = append(menuData, &do.SysRoleMenu{TenantId: template.TenantId, RoleId: current.RoleId, MenuId: menu.MenuId, DataScope: consts.SysRoleMenuDataScopeRole})
|
||||
}
|
||||
if len(menuData) > 0 {
|
||||
if _, err := tx.Ctx(ctx).Model(dao.SysRoleMenu.Table()).Data(menuData).Batch(200).Insert(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package rolesync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model"
|
||||
)
|
||||
|
||||
func TestTemplateHierarchyRetriesFailedParentAndDescendants(t *testing.T) {
|
||||
setupAgentRoleSyncTestDB(t)
|
||||
ctx := context.Background()
|
||||
for _, statement := range []string{
|
||||
"INSERT INTO sys_dept(dept_id,tenant_id,parent_id,dept_type,dept_role) VALUES(1,'000000',0,1,0),(2,'000000',1,1,0),(3,'000000',2,1,31),(4,'000000',1,1,41),(5,'other',1,1,51),(6,'000000',1,0,61)",
|
||||
"INSERT INTO sys_role(role_id,tenant_id,dept_id,status,is_pass,created_dept,data_scope) VALUES(10,'000000',1,'0',0,1,'4'),(21,'000000',2,'0',0,1,'4'),(31,'000000',3,'0',0,2,'4'),(41,'000000',4,'0',0,1,'4'),(51,'other',5,'0',0,1,'4'),(61,'000000',6,'0',0,1,'4')",
|
||||
"INSERT INTO sys_config(tenant_id,config_key,config_value) VALUES('000000','agent.role.template.role_id','10')",
|
||||
"INSERT INTO sys_role_menu(tenant_id,role_id,menu_id,data_scope) VALUES('000000',10,100,'0'),('000000',31,999,'0')",
|
||||
} {
|
||||
mustExecAgentRoleSyncTest(t, ctx, statement)
|
||||
}
|
||||
message := &model.AgentRoleSyncMessage{TenantId: "000000", SourceRoleId: 10, SourceDeptId: 1, TemplateRole: true}
|
||||
if err := SyncInheritedRole(ctx, message); err == nil {
|
||||
t.Fatal("missing parent default role must fail")
|
||||
}
|
||||
if !reflect.DeepEqual(message.ChildDeptIds, []int64{2, 3}) || message.TotalCount != 3 || message.SuccessCount != 1 || message.FailedCount != 2 {
|
||||
t.Fatalf("unexpected failed hierarchy: %+v", message)
|
||||
}
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id=31 AND menu_id=999", 1)
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id IN(51,61)", 0)
|
||||
mustExecAgentRoleSyncTest(t, ctx, "UPDATE sys_dept SET dept_role=21 WHERE dept_id=2")
|
||||
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role_menu(tenant_id,role_id,menu_id,data_scope) VALUES('000000',41,777,'0')")
|
||||
if err := SyncInheritedRole(ctx, message); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if message.SuccessCount != 3 || message.FailedCount != 0 || len(message.ChildDeptIds) != 0 {
|
||||
t.Fatalf("retry counts: %+v", message)
|
||||
}
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id=31 AND menu_id=100", 1)
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id=31 AND menu_id=999", 0)
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id=41 AND menu_id=777", 1)
|
||||
message.ChildDeptIds = []int64{3}
|
||||
mustExecAgentRoleSyncTest(t, ctx, "UPDATE sys_dept SET parent_id=99 WHERE dept_id=3")
|
||||
if err := SyncInheritedRole(ctx, message); err == nil || !reflect.DeepEqual(message.ChildDeptIds, []int64{3}) {
|
||||
t.Fatalf("removed retry target must not be reported as successful: %+v %v", message, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateHierarchyRejectsCycleBeforeWriting(t *testing.T) {
|
||||
setupAgentRoleSyncTestDB(t)
|
||||
ctx := context.Background()
|
||||
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_dept(dept_id,tenant_id,parent_id,dept_type,dept_role) VALUES(1,'000000',2,1,0),(2,'000000',1,1,21)")
|
||||
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role(role_id,tenant_id,dept_id,status,is_pass,created_dept,data_scope) VALUES(10,'000000',1,'0',0,1,'4'),(21,'000000',2,'0',0,1,'4')")
|
||||
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_config(tenant_id,config_key,config_value) VALUES('000000','agent.role.template.role_id','10')")
|
||||
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role_menu(tenant_id,role_id,menu_id,data_scope) VALUES('000000',10,100,'0'),('000000',21,999,'0')")
|
||||
if err := SyncInheritedRole(ctx, &model.AgentRoleSyncMessage{TenantId: "000000", SourceRoleId: 10, SourceDeptId: 1}); err == nil {
|
||||
t.Fatal("cycle must fail before updating roles")
|
||||
}
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id=21 AND menu_id=999", 1)
|
||||
}
|
||||
|
||||
func TestTemplateOverwriteResetsIndividualDefaultsOnly(t *testing.T) {
|
||||
setupAgentRoleSyncTestDB(t)
|
||||
ctx := context.Background()
|
||||
for _, statement := range []string{
|
||||
"INSERT INTO sys_dept(dept_id,tenant_id,parent_id,dept_type,dept_role) VALUES(1,'000000',0,1,0),(2,'000000',1,1,21),(3,'000000',2,1,31)",
|
||||
"INSERT INTO sys_role(role_id,tenant_id,dept_id,status,is_pass,created_dept,data_scope) VALUES(10,'000000',1,'0',0,1,'4'),(21,'000000',2,'0',0,1,'4'),(31,'000000',3,'0',0,2,'4'),(22,'000000',2,'0',0,2,'3'),(32,'000000',3,'0',0,3,'5')",
|
||||
"INSERT INTO sys_config(tenant_id,config_key,config_value) VALUES('000000','agent.role.template.role_id','10')",
|
||||
"INSERT INTO sys_role_menu VALUES('000000',10,100,'0'),('000000',10,200,'0'),('000000',22,999,'3'),('000000',32,888,'5')",
|
||||
"INSERT INTO sys_user_role(user_id,role_id) VALUES(99,21),(100,31),(101,22)",
|
||||
} {
|
||||
mustExecAgentRoleSyncTest(t, ctx, statement)
|
||||
}
|
||||
sync := func() {
|
||||
t.Helper()
|
||||
if err := SyncInheritedRole(ctx, &model.AgentRoleSyncMessage{TenantId: "000000", SourceRoleId: 10, SourceDeptId: 1, TemplateRole: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
sync()
|
||||
// Platform expands one role, removes a default permission from another, and
|
||||
// gives unrelated staff roles independent permissions.
|
||||
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role_menu VALUES('000000',21,999,'0'),('000000',31,888,'0')")
|
||||
mustExecAgentRoleSyncTest(t, ctx, "DELETE FROM sys_role_menu WHERE role_id=21 AND menu_id=200")
|
||||
mustExecAgentRoleSyncTest(t, ctx, "DELETE FROM sys_role_menu WHERE role_id=10 AND menu_id=100")
|
||||
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role_menu VALUES('000000',10,300,'0')")
|
||||
sync()
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id IN(21,31)", 4)
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id IN(21,31) AND menu_id IN(200,300)", 4)
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id IN(21,31) AND menu_id IN(100,888,999)", 0)
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE (role_id=22 AND menu_id=999 AND data_scope='3') OR (role_id=32 AND menu_id=888 AND data_scope='5')", 2)
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_user_role", 3)
|
||||
// A repeated distribution must also replace individual default-role edits.
|
||||
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role_menu VALUES('000000',31,999,'0')")
|
||||
sync()
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id IN(21,31)", 4)
|
||||
}
|
||||
Reference in New Issue
Block a user