This commit is contained in:
Yvan
2026-08-18 15:04:07 +08:00
parent 10ccc005be
commit 1ef8ecb7b2
42 changed files with 3027 additions and 296 deletions
+391 -38
View File
@@ -47,33 +47,105 @@ func (r *authorityAccessRepo) strictAuthorityIDs(ctx context.Context, actorID ui
allowed[actorID] = true
}
walk(actorID)
if actor.ParentID != nil && *actor.ParentID != 0 {
// A non-root authority manages descendants, never itself. Removing the
// actor explicitly also keeps malformed cyclic trees from reopening the
// self-management path.
delete(allowed, actorID)
}
return allowed, nil
}
func (r *authorityAccessRepo) checkAuthorityIDAuth(ctx context.Context, targetID uint) error {
func (r *authorityAccessRepo) strictAuthorityAccess(ctx context.Context) (biz.Actor, map[uint]bool, bool, error) {
config := r.data.runtime.Admin()
if config == nil || config.System == nil || !config.System.UseStrictAuth {
return nil
return biz.Actor{}, nil, false, nil
}
actor, ok := biz.ActorFromContext(ctx)
if !ok {
return errors.New("您提交的角色ID不合法")
return biz.Actor{}, nil, true, errors.New("您提交的角色ID不合法")
}
allowed, err := r.strictAuthorityIDs(ctx, actor.AuthorityID)
if err != nil {
return biz.Actor{}, nil, true, err
}
return actor, allowed, true, nil
}
func (r *authorityAccessRepo) checkAuthorityIDsAuth(ctx context.Context, targetIDs []uint) error {
_, allowed, strict, err := r.strictAuthorityAccess(ctx)
if err != nil || !strict {
return err
}
if !allowed[targetID] {
return errors.New("您提交的角色ID不合法")
for _, targetID := range targetIDs {
if !allowed[targetID] {
return errors.New("您提交的角色ID不合法")
}
}
return nil
}
func (r *authorityAccessRepo) checkAuthorityIDAuth(ctx context.Context, targetID uint) error {
return r.checkAuthorityIDsAuth(ctx, []uint{targetID})
}
func managedAuthorityParent(actor biz.Actor, allowed map[uint]bool, targetID uint, parentID *uint, creating bool) (*uint, error) {
if creating && (parentID == nil || *parentID == 0) {
value := actor.AuthorityID
return &value, nil
}
if !creating && targetID == actor.AuthorityID && allowed[actor.AuthorityID] && (parentID == nil || *parentID == 0) {
return parentID, nil
}
if parentID == nil || *parentID == 0 || *parentID == targetID || (*parentID != actor.AuthorityID && !allowed[*parentID]) {
return nil, errors.New("您提交的角色ID不合法")
}
return parentID, nil
}
func (r *authorityAccessRepo) ensureAuthorityParentAcyclic(ctx context.Context, targetID uint, parentID *uint) error {
if parentID == nil || *parentID == 0 {
return nil
}
current := *parentID
visited := make(map[uint]struct{})
for current != 0 {
if current == targetID {
return errors.New("角色父级不能形成循环")
}
if _, seen := visited[current]; seen {
return errors.New("角色层级存在循环")
}
visited[current] = struct{}{}
var authority authorityPO
if err := r.data.gormDB.WithContext(ctx).Select("authority_id", "parent_id").Where("authority_id = ?", current).First(&authority).Error; err != nil {
return err
}
if authority.ParentID == nil {
return nil
}
current = *authority.ParentID
}
return nil
}
func (r *authorityAccessRepo) CreateAuthority(ctx context.Context, value *biz.Authority) error {
config := r.data.runtime.Admin()
if config != nil && config.System != nil && config.System.UseStrictAuth && (value.ParentID == nil || *value.ParentID == 0) {
if actor, ok := biz.ActorFromContext(ctx); ok {
value.ParentID = &actor.AuthorityID
if value.DataScope == 0 {
value.DataScope = 1
} else if value.DataScope < 1 || value.DataScope > 5 {
return errInvalidDataScope
}
if err := r.checkDataScopeGrant(ctx, value.DataScope, nil); err != nil {
return err
}
actor, allowed, strict, err := r.strictAuthorityAccess(ctx)
if err != nil {
return err
}
if strict {
value.ParentID, err = managedAuthorityParent(actor, allowed, value.AuthorityID, value.ParentID, true)
if err != nil {
return err
}
}
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
@@ -97,25 +169,58 @@ func (r *authorityAccessRepo) CreateAuthority(ctx context.Context, value *biz.Au
value.Menus = []*biz.Menu{{ID: 1, Path: "dashboard", Name: "dashboard", Component: "view/dashboard/index.vue", Sort: 1, Title: "仪表盘", Icon: "setting"}}
var dashboard menuPO
if err := tx.Where("name = ?", "dashboard").First(&dashboard).Error; err == nil {
if err = tx.Create(&authorityMenuPO{SysAuthorityAuthorityID: value.AuthorityID, SysBaseMenuID: dashboard.ID}).Error; err != nil {
return err
grantDashboard := true
if strict && !allowed[actor.AuthorityID] {
var count int64
if err = tx.Model(&authorityMenuPO{}).Where("sys_authority_authority_id = ? AND sys_base_menu_id = ?", actor.AuthorityID, dashboard.ID).Count(&count).Error; err != nil {
return err
}
grantDashboard = count > 0
}
if grantDashboard {
if err = tx.Create(&authorityMenuPO{SysAuthorityAuthorityID: value.AuthorityID, SysBaseMenuID: dashboard.ID}).Error; err != nil {
return err
}
} else {
value.Menus = nil
}
}
defaults := []struct{ path, method string }{{"/menu/getMenu", "POST"}, {"/jwt/jsonInBlacklist", "POST"}, {"/base/login", "POST"}, {"/user/changePassword", "POST"}, {"/user/setUserAuthority", "POST"}, {"/user/getUserInfo", "GET"}, {"/user/setSelfInfo", "PUT"}, {"/fileUploadAndDownload/upload", "POST"}, {"/sysDictionary/findSysDictionary", "GET"}}
rules := make([]casbinRulePO, 0, len(defaults))
for _, item := range defaults {
if strict {
permitted, policyErr := r.copyPolicyAllowed(ctx, tx, actor.AuthorityID, item.path, item.method)
if policyErr != nil {
return policyErr
}
if !permitted {
continue
}
}
rules = append(rules, newPolicyRule(value.AuthorityID, item.path, item.method))
}
if err := tx.Create(&rules).Error; err != nil {
return err
if len(rules) > 0 {
if err := tx.Create(&rules).Error; err != nil {
return err
}
}
return nil
})
}
func (r *authorityAccessRepo) CopyAuthority(ctx context.Context, sourceID uint, value *biz.Authority) error {
if value.DataScope == 0 {
value.DataScope = 1
} else if value.DataScope < 1 || value.DataScope > 5 {
return errInvalidDataScope
}
if err := r.checkDataScopeGrant(ctx, value.DataScope, nil); err != nil {
return err
}
actor, allowed, strict, err := r.strictAuthorityAccess(ctx)
if err != nil {
return err
}
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
config := r.data.runtime.Admin()
actor, hasActor := biz.ActorFromContext(ctx)
// Reject a duplicate target ID before performing the
// hierarchy/Casbin checks that happen later in UpdateCasbin.
var existing authorityPO
@@ -128,16 +233,14 @@ func (r *authorityAccessRepo) CopyAuthority(ctx context.Context, sourceID uint,
// that its requested parent is the actor or one of the actor's allowed
// descendants. This preserves the compatible error and prevents a transient role
// from being visible when the check is guaranteed to fail.
strict := hasActor && config != nil && config.System != nil && config.System.UseStrictAuth
if strict {
allowed, err := r.strictAuthorityIDs(ctx, actor.AuthorityID)
if sourceID != actor.AuthorityID && !allowed[sourceID] {
return errors.New("您提交的角色ID不合法")
}
value.ParentID, err = managedAuthorityParent(actor, allowed, value.AuthorityID, value.ParentID, true)
if err != nil {
return err
}
parentAllowed := value.ParentID != nil && (allowed[*value.ParentID] || *value.ParentID == actor.AuthorityID)
if !parentAllowed {
return errors.New("您提交的角色ID不合法")
}
}
po := authorityPO{AuthorityID: value.AuthorityID, AuthorityName: value.AuthorityName, ParentID: value.ParentID, DataScope: value.DataScope, DefaultRouter: value.DefaultRouter}
if err := tx.Create(&po).Error; err != nil {
@@ -152,6 +255,15 @@ func (r *authorityAccessRepo) CopyAuthority(ctx context.Context, sourceID uint,
if err := copyLinks("sys_authority_menus", &menus, map[string]any{"sys_authority_authority_id": sourceID}); err != nil {
return err
}
if strict && len(menus) > 0 {
menuIDs := make([]uint, 0, len(menus))
for _, menu := range menus {
menuIDs = append(menuIDs, menu.SysBaseMenuID)
}
if err := checkMenuAssignment(tx.WithContext(ctx), actor.AuthorityID, allowed[actor.AuthorityID], menuIDs); err != nil {
return err
}
}
for i := range menus {
menus[i].SysAuthorityAuthorityID = value.AuthorityID
}
@@ -206,6 +318,15 @@ func (r *authorityAccessRepo) CopyAuthority(ctx context.Context, sourceID uint,
if err := copyLinks("sys_authority_btns", &buttons, map[string]any{"authority_id": sourceID}); err != nil {
return err
}
if strict && len(buttons) > 0 {
requested := make(map[uint][]uint)
for _, button := range buttons {
requested[button.MenuID] = append(requested[button.MenuID], button.ButtonID)
}
if err := checkButtonAssignment(tx.WithContext(ctx), actor.AuthorityID, allowed[actor.AuthorityID], requested); err != nil {
return err
}
}
for i := range buttons {
buttons[i].AuthorityID = value.AuthorityID
}
@@ -240,6 +361,38 @@ func (r *authorityAccessRepo) copyPolicyAllowed(ctx context.Context, tx *gorm.DB
return policyExists(tx.WithContext(ctx), actorID, path, method)
}
func (r *authorityAccessRepo) UpdateAuthority(ctx context.Context, value *biz.Authority) error {
if err := r.checkAuthorityIDAuth(ctx, value.AuthorityID); err != nil {
return err
}
if value.DataScope < 0 || value.DataScope > 5 {
return errInvalidDataScope
}
if value.DataScope != 0 {
var departmentIDs []uint
if value.DataScope == 5 {
var err error
departmentIDs, err = r.DataScopeDepartmentIDs(ctx, value.AuthorityID)
if err != nil {
return err
}
}
if err := r.checkDataScopeGrant(ctx, value.DataScope, departmentIDs); err != nil {
return err
}
}
actor, allowed, strict, err := r.strictAuthorityAccess(ctx)
if err != nil {
return err
}
if strict {
value.ParentID, err = managedAuthorityParent(actor, allowed, value.AuthorityID, value.ParentID, false)
if err != nil {
return err
}
}
if err = r.ensureAuthorityParentAcyclic(ctx, value.AuthorityID, value.ParentID); err != nil {
return err
}
db := r.data.gormDB.WithContext(ctx)
var current authorityPO
if err := db.Where("authority_id = ?", value.AuthorityID).First(&current).Error; err != nil {
@@ -249,6 +402,9 @@ func (r *authorityAccessRepo) UpdateAuthority(ctx context.Context, value *biz.Au
return db.Model(&current).Updates(updates).Error
}
func (r *authorityAccessRepo) DeleteAuthority(ctx context.Context, id uint) error {
if err := r.checkAuthorityIDAuth(ctx, id); err != nil {
return err
}
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var authority authorityPO
if err := tx.Where("authority_id = ?", id).First(&authority).Error; err != nil {
@@ -341,11 +497,26 @@ func (r *authorityAccessRepo) ListAuthorities(ctx context.Context) ([]*biz.Autho
return out, nil
}
func (r *authorityAccessRepo) SetAuthorityUsers(ctx context.Context, id uint, ids []uint) error {
_, allowed, strict, err := r.strictAuthorityAccess(ctx)
if err != nil {
return err
}
if strict && !allowed[id] {
return errors.New("您提交的角色ID不合法")
}
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var oldIDs []uint
if err := tx.Model(&userAuthorityPO{}).Where("sys_authority_authority_id = ?", id).Pluck("sys_user_id", &oldIDs).Error; err != nil {
return err
}
if strict {
if err := checkManagedUserIDs(tx, ids, allowed, true); err != nil {
return err
}
if err := checkManagedUserIDs(tx, oldIDs, allowed, false); err != nil {
return err
}
}
if err := tx.Where("sys_authority_authority_id = ?", id).Delete(&userAuthorityPO{}).Error; err != nil {
return err
}
@@ -382,12 +553,111 @@ func (r *authorityAccessRepo) SetAuthorityUsers(ctx context.Context, id uint, id
return nil
})
}
func checkManagedUserIDs(tx *gorm.DB, ids []uint, allowedAuthorities map[uint]bool, requireAll bool) error {
unique := make(map[uint]struct{}, len(ids))
for _, id := range ids {
if id == 0 {
if requireAll {
return errors.New("您提交的用户ID不合法")
}
continue
}
unique[id] = struct{}{}
}
if len(unique) == 0 {
return nil
}
userIDs := make([]uint, 0, len(unique))
for id := range unique {
userIDs = append(userIDs, id)
}
var users []userPO
if err := tx.Select("id", "authority_id").Where("id IN ?", userIDs).Find(&users).Error; err != nil {
return err
}
if requireAll && len(users) != len(unique) {
return errors.New("您提交的用户ID不合法")
}
managedUsers := make(map[uint]bool, len(users))
for _, user := range users {
if !allowedAuthorities[user.AuthorityID] {
return errors.New("您提交的用户ID不合法")
}
managedUsers[user.ID] = true
}
if len(managedUsers) == 0 {
return nil
}
var links []userAuthorityPO
if err := tx.Where("sys_user_id IN ?", userIDs).Find(&links).Error; err != nil {
return err
}
for _, link := range links {
if managedUsers[link.SysUserID] && !allowedAuthorities[link.SysAuthorityAuthorityID] {
return errors.New("您提交的用户ID不合法")
}
}
return nil
}
func (r *authorityAccessRepo) checkUserIDAuth(ctx context.Context, id uint, allowSelf bool) error {
actor, allowed, strict, err := r.strictAuthorityAccess(ctx)
if err != nil || !strict {
return err
}
if allowSelf && actor.UserID != 0 && actor.UserID == id {
return nil
}
return checkManagedUserIDs(r.data.gormDB.WithContext(ctx), []uint{id}, allowed, true)
}
func (r *authorityAccessRepo) checkDepartmentIDsAuth(ctx context.Context, ids []uint) error {
actor, allowedAuthorities, strict, err := r.strictAuthorityAccess(ctx)
if err != nil || !strict || len(ids) == 0 {
return err
}
if allowedAuthorities[actor.AuthorityID] {
return nil
}
var authority authorityPO
if err := r.data.gormDB.WithContext(ctx).Select("authority_id", "data_scope").Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil {
return err
}
if authority.DataScope == 1 {
return nil
}
scope, err := r.ResolveDataScope(ctx, actor.AuthorityID, actor.UserID)
if err != nil {
return err
}
visible := make(map[uint]bool, len(scope.DepartmentIDs))
for _, id := range scope.DepartmentIDs {
visible[id] = true
}
for _, id := range ids {
if id == 0 || !visible[id] {
return errors.New("您提交的部门ID不合法")
}
}
return nil
}
func (r *authorityAccessRepo) AuthorityUserIDs(ctx context.Context, id uint) ([]uint, error) {
var ids []uint
err := r.data.gormDB.WithContext(ctx).Model(&userAuthorityPO{}).Where("sys_authority_authority_id = ?", id).Pluck("sys_user_id", &ids).Error
return ids, err
}
func (r *authorityAccessRepo) SetDataScope(ctx context.Context, id uint, scope int, deptIDs []uint) error {
if err := r.checkAuthorityIDAuth(ctx, id); err != nil {
return err
}
if scope < 1 || scope > 5 {
return errInvalidDataScope
}
if err := r.checkDataScopeGrant(ctx, scope, deptIDs); err != nil {
return err
}
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&authorityPO{}).Where("authority_id = ?", id).Update("data_scope", scope).Error; err != nil {
return err
@@ -407,6 +677,74 @@ func (r *authorityAccessRepo) SetDataScope(ctx context.Context, id uint, scope i
return nil
})
}
func (r *authorityAccessRepo) checkDataScopeGrant(ctx context.Context, requestedScope int, departmentIDs []uint) error {
actor, allowedAuthorities, strict, err := r.strictAuthorityAccess(ctx)
if err != nil || !strict {
return err
}
requestedDepartments := make(map[uint]struct{}, len(departmentIDs))
if requestedScope == 5 {
for _, departmentID := range departmentIDs {
if departmentID == 0 {
return errors.New("您提交的部门ID不合法")
}
requestedDepartments[departmentID] = struct{}{}
}
if len(requestedDepartments) > 0 {
var count int64
if err := r.data.gormDB.WithContext(ctx).Model(&departmentPO{}).Where("id IN ?", departmentIDs).Count(&count).Error; err != nil {
return err
}
if count != int64(len(requestedDepartments)) {
return errors.New("您提交的部门ID不合法")
}
}
}
if allowedAuthorities[actor.AuthorityID] {
return nil
}
var actorAuthority authorityPO
if err := r.data.gormDB.WithContext(ctx).Select("authority_id", "data_scope").Where("authority_id = ?", actor.AuthorityID).First(&actorAuthority).Error; err != nil {
return err
}
grantable := false
switch actorAuthority.DataScope {
case 1:
grantable = true
case 2:
grantable = requestedScope == 2 || requestedScope == 3 || requestedScope == 4 || requestedScope == 5
case 3:
grantable = requestedScope == 3 || requestedScope == 4 || requestedScope == 5
case 4:
grantable = requestedScope == 4
case 5:
grantable = requestedScope == 4 || requestedScope == 5
default:
return errInvalidDataScope
}
if !grantable {
return errInvalidDataScope
}
if requestedScope != 5 || len(requestedDepartments) == 0 || actorAuthority.DataScope == 1 {
return nil
}
actorScope, err := r.ResolveDataScope(ctx, actor.AuthorityID, actor.UserID)
if err != nil {
return err
}
allowedDepartments := make(map[uint]bool, len(actorScope.DepartmentIDs))
for _, departmentID := range actorScope.DepartmentIDs {
allowedDepartments[departmentID] = true
}
for departmentID := range requestedDepartments {
if !allowedDepartments[departmentID] {
return errors.New("您提交的部门ID不合法")
}
}
return nil
}
func (r *authorityAccessRepo) DataScopeDepartmentIDs(ctx context.Context, id uint) ([]uint, error) {
var ids []uint
err := r.data.gormDB.WithContext(ctx).Model(&authorityDepartmentPO{}).Where("sys_authority_authority_id = ?", id).Pluck("sys_department_id", &ids).Error
@@ -415,37 +753,48 @@ func (r *authorityAccessRepo) DataScopeDepartmentIDs(ctx context.Context, id uin
func (r *authorityAccessRepo) ResolveDataScope(ctx context.Context, authorityID, userID uint) (biz.DataScope, error) {
identity := biz.DataScope{UserID: userID, AuthorityID: authorityID}
var user userPO
_ = r.data.gormDB.WithContext(ctx).Select("id", "dept_id").First(&user, userID).Error
if err := r.data.gormDB.WithContext(ctx).Select("id", "dept_id").First(&user, userID).Error; err != nil {
return identity, err
}
identity.PrimaryDeptID = user.DeptID
var authority authorityPO
_ = r.data.gormDB.WithContext(ctx).Select("authority_id", "data_scope").First(&authority, "authority_id = ?", authorityID).Error
if err := r.data.gormDB.WithContext(ctx).Select("authority_id", "data_scope").First(&authority, "authority_id = ?", authorityID).Error; err != nil {
return identity, err
}
identity.Scope = authority.DataScope
if identity.Scope == 0 {
identity.Scope = 1
if identity.Scope < 1 || identity.Scope > 5 {
return identity, errInvalidDataScope
}
identity.All = identity.Scope == 1
if identity.Scope == 4 {
identity.OwnerUserID = userID
}
var ids []uint
_ = r.data.gormDB.WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_user_id = ?", userID).Pluck("sys_department_id", &ids).Error
selected := make(map[uint]bool, len(ids)+1)
for _, id := range ids {
selected[id] = true
}
if user.DeptID != 0 {
selected[user.DeptID] = true
}
ids = ids[:0]
for id := range selected {
ids = append(ids, id)
selected := make(map[uint]bool)
if identity.Scope == 2 || identity.Scope == 3 {
if err := r.data.gormDB.WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_user_id = ?", userID).Pluck("sys_department_id", &ids).Error; err != nil {
return identity, err
}
selected = make(map[uint]bool, len(ids)+1)
for _, id := range ids {
selected[id] = true
}
if user.DeptID != 0 {
selected[user.DeptID] = true
}
ids = ids[:0]
for id := range selected {
ids = append(ids, id)
}
}
if identity.Scope == 2 && len(ids) > 0 {
// Match the administration subtree-union semantics: derive the visible set from
// parent_id edges at read time, rather than trusting the denormalized
// ancestors string (which may be stale after a department move).
var departments []departmentPO
_ = r.data.gormDB.WithContext(ctx).Select("id", "parent_id").Find(&departments).Error
if err := r.data.gormDB.WithContext(ctx).Select("id", "parent_id").Find(&departments).Error; err != nil {
return identity, err
}
children := make(map[uint][]uint, len(departments))
for _, department := range departments {
children[department.ParentID] = append(children[department.ParentID], department.ID)
@@ -473,7 +822,11 @@ func (r *authorityAccessRepo) ResolveDataScope(ctx context.Context, authorityID,
ids = append(ids, id)
}
} else if identity.Scope == 5 {
ids, _ = r.DataScopeDepartmentIDs(ctx, authorityID)
var err error
ids, err = r.DataScopeDepartmentIDs(ctx, authorityID)
if err != nil {
return identity, err
}
}
identity.DepartmentIDs = ids
return identity, nil