优化
This commit is contained in:
+70
-39
@@ -32,10 +32,33 @@ func (r *apiRepo) APIRoleIDs(ctx context.Context, path, method string) ([]uint,
|
||||
return ids, nil
|
||||
}
|
||||
func (r *apiRepo) SetAPIRoles(ctx context.Context, path, method string, ids []uint) error {
|
||||
access := &authorityAccessRepo{data: r.data}
|
||||
_, allowedAuthorities, strict, err := access.strictAuthorityAccess(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strict {
|
||||
for _, id := range ids {
|
||||
if !allowedAuthorities[id] {
|
||||
return errors.New("您提交的角色ID不合法")
|
||||
}
|
||||
}
|
||||
if err := r.checkPolicyPathsAuth(ctx, []*biz.API{{Path: path, Method: method}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// A path/method need not be present in sys_apis when replacing
|
||||
// all matching policy rows. Do not validate or deduplicate authority IDs.
|
||||
if err := deletePoliciesForPath(tx, path, method); err != nil {
|
||||
if strict {
|
||||
authorityIDs := make([]string, 0, len(allowedAuthorities))
|
||||
for authorityID := range allowedAuthorities {
|
||||
authorityIDs = append(authorityIDs, strconv.FormatUint(uint64(authorityID), 10))
|
||||
}
|
||||
if len(authorityIDs) > 0 {
|
||||
if err := policyScope(tx).Where("v1 = ? AND v2 = ? AND v0 IN ?", path, method, authorityIDs).Delete(&casbinRulePO{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if err := deletePoliciesForPath(tx, path, method); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
@@ -100,42 +123,8 @@ func (r *apiRepo) SetPolicyPaths(ctx context.Context, aid uint, paths []*biz.API
|
||||
if err := (&authorityAccessRepo{data: r.data}).checkAuthorityIDAuth(ctx, aid); err != nil {
|
||||
return err
|
||||
}
|
||||
config := r.data.runtime.Admin()
|
||||
if actor, ok := biz.ActorFromContext(ctx); ok && config != nil && config.System != nil && config.System.UseStrictAuth {
|
||||
var authority authorityPO
|
||||
if err := r.data.gormDB.WithContext(ctx).Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var registered []apiPO
|
||||
if err := r.data.gormDB.WithContext(ctx).Find(®istered).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
allowedSet := make(map[string]bool, len(registered))
|
||||
if authority.ParentID == nil || *authority.ParentID == 0 {
|
||||
for _, item := range registered {
|
||||
allowedSet[item.Path+"\x00"+item.Method] = true
|
||||
}
|
||||
} else {
|
||||
policies, err := policyRowsForAuthority(r.data.gormDB.WithContext(ctx), actor.AuthorityID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
policySet := make(map[string]bool, len(policies))
|
||||
for _, item := range policies {
|
||||
policySet[item.V1+"\x00"+item.V2] = true
|
||||
}
|
||||
for _, item := range registered {
|
||||
key := item.Path + "\x00" + item.Method
|
||||
if policySet[key] {
|
||||
allowedSet[key] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, item := range paths {
|
||||
if !allowedSet[item.Path+"\x00"+item.Method] {
|
||||
return errors.New("存在api不在权限列表中")
|
||||
}
|
||||
}
|
||||
if err := r.checkPolicyPathsAuth(ctx, paths); err != nil {
|
||||
return err
|
||||
}
|
||||
db := r.data.gormDB.WithContext(ctx)
|
||||
// The reference enforcer removes the old authority policies before it
|
||||
@@ -165,3 +154,45 @@ func (r *apiRepo) SetPolicyPaths(ctx context.Context, aid uint, paths []*biz.API
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *apiRepo) checkPolicyPathsAuth(ctx context.Context, paths []*biz.API) error {
|
||||
actor, _, strict, err := (&authorityAccessRepo{data: r.data}).strictAuthorityAccess(ctx)
|
||||
if err != nil || !strict {
|
||||
return err
|
||||
}
|
||||
var authority authorityPO
|
||||
if err := r.data.gormDB.WithContext(ctx).Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var registered []apiPO
|
||||
if err := r.data.gormDB.WithContext(ctx).Find(®istered).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
allowedSet := make(map[string]bool, len(registered))
|
||||
if authority.ParentID == nil || *authority.ParentID == 0 {
|
||||
for _, item := range registered {
|
||||
allowedSet[item.Path+"\x00"+item.Method] = true
|
||||
}
|
||||
} else {
|
||||
policies, err := policyRowsForAuthority(r.data.gormDB.WithContext(ctx), actor.AuthorityID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
policySet := make(map[string]bool, len(policies))
|
||||
for _, item := range policies {
|
||||
policySet[item.V1+"\x00"+item.V2] = true
|
||||
}
|
||||
for _, item := range registered {
|
||||
key := item.Path + "\x00" + item.Method
|
||||
if policySet[key] {
|
||||
allowedSet[key] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, item := range paths {
|
||||
if !allowedSet[item.Path+"\x00"+item.Method] {
|
||||
return errors.New("存在api不在权限列表中")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -126,3 +126,40 @@ func TestCheckPolicyStore(t *testing.T) {
|
||||
t.Fatal("CheckPolicyStore succeeded after casbin_rule was dropped")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetAPIRolesStrictOnlyChangesManagedAuthorities(t *testing.T) {
|
||||
data := newPolicyTestData(t)
|
||||
db := data.gormDB.WithContext(context.Background())
|
||||
rootID, actorID, childID, siblingID := uint(888), uint(2001), uint(2002), uint(3001)
|
||||
if err := db.Create(&[]authorityPO{
|
||||
{AuthorityID: rootID, ParentID: apiPolicyUintPointer(0)},
|
||||
{AuthorityID: actorID, ParentID: &rootID},
|
||||
{AuthorityID: childID, ParentID: &actorID},
|
||||
{AuthorityID: siblingID, ParentID: &rootID},
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&apiPO{Path: "/known", Method: "POST"}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&[]casbinRulePO{
|
||||
newPolicyRule(actorID, "/known", "POST"),
|
||||
newPolicyRule(siblingID, "/known", "POST"),
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
|
||||
repo := &apiRepo{data: data}
|
||||
if err := repo.SetAPIRoles(ctx, "/known", "POST", []uint{siblingID}); err == nil {
|
||||
t.Fatal("SetAPIRoles() accepted an out-of-scope authority")
|
||||
}
|
||||
if err := repo.SetAPIRoles(ctx, "/known", "POST", []uint{childID}); err != nil {
|
||||
t.Fatalf("SetAPIRoles() rejected a managed authority: %v", err)
|
||||
}
|
||||
if exists, err := policyExists(db, siblingID, "/known", "POST"); err != nil || !exists {
|
||||
t.Fatalf("out-of-scope policy exists=%v err=%v, want preserved", exists, err)
|
||||
}
|
||||
if exists, err := policyExists(db, childID, "/known", "POST"); err != nil || !exists {
|
||||
t.Fatalf("managed policy exists=%v err=%v, want added", exists, err)
|
||||
}
|
||||
}
|
||||
|
||||
+391
-38
@@ -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(¤t).Error; err != nil {
|
||||
@@ -249,6 +402,9 @@ func (r *authorityAccessRepo) UpdateAuthority(ctx context.Context, value *biz.Au
|
||||
return db.Model(¤t).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
|
||||
|
||||
@@ -177,3 +177,405 @@ func TestAuthorityCustomDataScopeUsesCompatibleColumns(t *testing.T) {
|
||||
t.Fatalf("non-custom scope retained departments: %v", ids)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetAuthorityUsersStrictRejectsUsersOutsideManagedRoles(t *testing.T) {
|
||||
data := newTransactionTestData(t)
|
||||
enableStrictAuthorityTestMode(data)
|
||||
db := data.gormDB.WithContext(context.Background())
|
||||
rootID, actorID, childID, siblingID := uint(888), uint(1000), uint(1001), uint(2000)
|
||||
if err := db.Create(&[]authorityPO{
|
||||
{AuthorityID: rootID, ParentID: authorityUintPointer(0)},
|
||||
{AuthorityID: actorID, ParentID: &rootID},
|
||||
{AuthorityID: childID, ParentID: &actorID},
|
||||
{AuthorityID: siblingID, ParentID: &rootID},
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
users := []userPO{
|
||||
{Username: "managed", Password: "hash", AuthorityID: childID, Enable: 1},
|
||||
{Username: "outside", Password: "hash", AuthorityID: siblingID, Enable: 1},
|
||||
}
|
||||
if err := db.Create(&users).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
|
||||
repo := &authorityAccessRepo{data: data}
|
||||
if err := repo.SetAuthorityUsers(ctx, childID, []uint{users[1].ID}); err == nil {
|
||||
t.Fatal("SetAuthorityUsers() accepted a user outside the managed role tree")
|
||||
}
|
||||
if err := repo.SetAuthorityUsers(ctx, childID, []uint{users[0].ID}); err != nil {
|
||||
t.Fatalf("SetAuthorityUsers() rejected a managed user: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetAuthorityUsersStrictRejectsMixedRoleUserAlreadyLinked(t *testing.T) {
|
||||
data := newTransactionTestData(t)
|
||||
enableStrictAuthorityTestMode(data)
|
||||
db := data.gormDB.WithContext(context.Background())
|
||||
rootID, actorID, childID, siblingID := uint(888), uint(1100), uint(1101), uint(2100)
|
||||
if err := db.Create(&[]authorityPO{
|
||||
{AuthorityID: rootID, ParentID: authorityUintPointer(0)},
|
||||
{AuthorityID: actorID, ParentID: &rootID},
|
||||
{AuthorityID: childID, ParentID: &actorID},
|
||||
{AuthorityID: siblingID, ParentID: &rootID},
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
user := userPO{Username: "mixed", Password: "hash", AuthorityID: childID, Enable: 1}
|
||||
if err := db.Create(&user).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&[]userAuthorityPO{
|
||||
{SysUserID: user.ID, SysAuthorityAuthorityID: childID},
|
||||
{SysUserID: user.ID, SysAuthorityAuthorityID: siblingID},
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
|
||||
if err := (&authorityAccessRepo{data: data}).SetAuthorityUsers(ctx, childID, nil); err == nil {
|
||||
t.Fatal("SetAuthorityUsers() modified a linked user that also has an out-of-scope role")
|
||||
}
|
||||
var count int64
|
||||
if err := db.Model(&userAuthorityPO{}).Where("sys_user_id = ? AND sys_authority_authority_id = ?", user.ID, childID).Count(&count).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count != 1 {
|
||||
t.Fatalf("rejected update changed the existing target-role link: count=%d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateAuthorityStrictRejectsHierarchyCycle(t *testing.T) {
|
||||
data := newTransactionTestData(t)
|
||||
enableStrictAuthorityTestMode(data)
|
||||
db := data.gormDB.WithContext(context.Background())
|
||||
rootID, actorID, targetID, childID := uint(888), uint(1200), uint(1201), uint(1202)
|
||||
if err := db.Create(&[]authorityPO{
|
||||
{AuthorityID: rootID, ParentID: authorityUintPointer(0)},
|
||||
{AuthorityID: actorID, ParentID: &rootID},
|
||||
{AuthorityID: targetID, AuthorityName: "target", ParentID: &actorID},
|
||||
{AuthorityID: childID, ParentID: &targetID},
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
|
||||
err := (&authorityAccessRepo{data: data}).UpdateAuthority(ctx, &biz.Authority{AuthorityID: targetID, AuthorityName: "target", ParentID: &childID})
|
||||
if err == nil {
|
||||
t.Fatal("UpdateAuthority() accepted a parent that forms a cycle")
|
||||
}
|
||||
var stored authorityPO
|
||||
if err := db.Where("authority_id = ?", targetID).First(&stored).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stored.ParentID == nil || *stored.ParentID != actorID {
|
||||
t.Fatalf("rejected update changed parent_id: %+v", stored.ParentID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetDataScopeValidatesScopeAndStrictDepartments(t *testing.T) {
|
||||
data := newTransactionTestData(t)
|
||||
enableStrictAuthorityTestMode(data)
|
||||
db := data.gormDB.WithContext(context.Background())
|
||||
rootID, actorID, childID := uint(888), uint(1300), uint(1301)
|
||||
if err := db.Create(&[]authorityPO{
|
||||
{AuthorityID: rootID, ParentID: authorityUintPointer(0)},
|
||||
{AuthorityID: actorID, ParentID: &rootID},
|
||||
{AuthorityID: childID, ParentID: &actorID},
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
|
||||
repo := &authorityAccessRepo{data: data}
|
||||
if err := repo.SetDataScope(ctx, childID, 0, nil); !errors.Is(err, errInvalidDataScope) {
|
||||
t.Fatalf("SetDataScope() invalid scope error = %v", err)
|
||||
}
|
||||
if err := repo.SetDataScope(ctx, childID, 5, []uint{999999}); err == nil {
|
||||
t.Fatal("SetDataScope() accepted a missing department in strict mode")
|
||||
}
|
||||
department := departmentPO{Name: "managed"}
|
||||
if err := db.Create(&department).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repo.SetDataScope(ctx, childID, 5, []uint{department.ID}); err != nil {
|
||||
t.Fatalf("SetDataScope() rejected an existing department: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetDataScopeStrictRejectsDepartmentsOutsideActorScope(t *testing.T) {
|
||||
data := newTransactionTestData(t)
|
||||
enableStrictAuthorityTestMode(data)
|
||||
db := data.gormDB.WithContext(context.Background())
|
||||
rootID, actorID, childID := uint(888), uint(1350), uint(1351)
|
||||
if err := db.Create(&[]authorityPO{
|
||||
{AuthorityID: rootID, ParentID: authorityUintPointer(0)},
|
||||
{AuthorityID: actorID, ParentID: &rootID, DataScope: 5},
|
||||
{AuthorityID: childID, ParentID: &actorID},
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
departments := []departmentPO{{Name: "allowed"}, {Name: "outside"}}
|
||||
if err := db.Create(&departments).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
actorUser := userPO{Username: "scope-actor", Password: "hash", AuthorityID: actorID, DeptID: departments[0].ID, Enable: 1}
|
||||
if err := db.Create(&actorUser).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&authorityDepartmentPO{AuthorityID: actorID, DepartmentID: departments[0].ID}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := biz.NewActorContext(context.Background(), biz.Actor{UserID: actorUser.ID, AuthorityID: actorID})
|
||||
repo := &authorityAccessRepo{data: data}
|
||||
if err := repo.SetDataScope(ctx, childID, 5, []uint{departments[1].ID}); err == nil {
|
||||
t.Fatal("SetDataScope() accepted a department outside the actor's data scope")
|
||||
}
|
||||
if err := repo.SetDataScope(ctx, childID, 5, []uint{departments[0].ID}); err != nil {
|
||||
t.Fatalf("SetDataScope() rejected a department inside the actor's data scope: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrictDataScopeGrantRejectsBroaderChildScopes(t *testing.T) {
|
||||
data := newTransactionTestData(t)
|
||||
enableStrictAuthorityTestMode(data)
|
||||
db := data.gormDB.WithContext(context.Background())
|
||||
rootID, actorID, sourceID, targetID := uint(888), uint(1370), uint(1371), uint(1372)
|
||||
if err := db.Create(&[]authorityPO{
|
||||
{AuthorityID: rootID, ParentID: authorityUintPointer(0), DataScope: 1},
|
||||
{AuthorityID: actorID, ParentID: &rootID, DataScope: 3},
|
||||
{AuthorityID: sourceID, ParentID: &actorID, DataScope: 4},
|
||||
{AuthorityID: targetID, AuthorityName: "target", ParentID: &actorID, DataScope: 4},
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
actorUser := userPO{Username: "limited-scope-actor", Password: "hash", AuthorityID: actorID, Enable: 1}
|
||||
if err := db.Create(&actorUser).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := biz.NewActorContext(context.Background(), biz.Actor{UserID: actorUser.ID, AuthorityID: actorID})
|
||||
repo := &authorityAccessRepo{data: data}
|
||||
|
||||
created := &biz.Authority{AuthorityID: 1373, AuthorityName: "created", ParentID: &actorID}
|
||||
if err := repo.CreateAuthority(ctx, created); !errors.Is(err, errInvalidDataScope) {
|
||||
t.Fatalf("CreateAuthority() broader scope error = %v", err)
|
||||
}
|
||||
if created.DataScope != 1 {
|
||||
t.Fatalf("CreateAuthority() did not normalize zero scope before validation: %d", created.DataScope)
|
||||
}
|
||||
copied := &biz.Authority{AuthorityID: 1374, AuthorityName: "copied", ParentID: &actorID}
|
||||
if err := repo.CopyAuthority(ctx, sourceID, copied); !errors.Is(err, errInvalidDataScope) {
|
||||
t.Fatalf("CopyAuthority() broader scope error = %v", err)
|
||||
}
|
||||
if copied.DataScope != 1 {
|
||||
t.Fatalf("CopyAuthority() did not normalize zero scope before validation: %d", copied.DataScope)
|
||||
}
|
||||
if err := repo.UpdateAuthority(ctx, &biz.Authority{AuthorityID: targetID, AuthorityName: "target", ParentID: &actorID, DataScope: 2}); !errors.Is(err, errInvalidDataScope) {
|
||||
t.Fatalf("UpdateAuthority() broader scope error = %v", err)
|
||||
}
|
||||
if err := repo.SetDataScope(ctx, targetID, 1, nil); !errors.Is(err, errInvalidDataScope) {
|
||||
t.Fatalf("SetDataScope() broader scope error = %v", err)
|
||||
}
|
||||
|
||||
allowed := &biz.Authority{AuthorityID: 1375, AuthorityName: "self-only", ParentID: &actorID, DataScope: 4}
|
||||
if err := repo.CreateAuthority(ctx, allowed); err != nil {
|
||||
t.Fatalf("CreateAuthority() rejected a narrower scope: %v", err)
|
||||
}
|
||||
var stored authorityPO
|
||||
if err := db.Where("authority_id = ?", targetID).First(&stored).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stored.DataScope != 4 {
|
||||
t.Fatalf("rejected grants changed target data scope to %d", stored.DataScope)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateAuthorityStrictDefaultsStayWithinActorPermissions(t *testing.T) {
|
||||
data := newTransactionTestData(t)
|
||||
enableStrictAuthorityTestMode(data)
|
||||
db := data.gormDB.WithContext(context.Background())
|
||||
rootID, actorID := uint(888), uint(1400)
|
||||
if err := db.Create(&[]authorityPO{
|
||||
{AuthorityID: rootID, ParentID: authorityUintPointer(0)},
|
||||
{AuthorityID: actorID, ParentID: &rootID},
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dashboard := menuPO{Name: "dashboard", Path: "dashboard"}
|
||||
if err := db.Create(&dashboard).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&apiPO{Path: "/menu/getMenu", Method: "POST"}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
|
||||
repo := &authorityAccessRepo{data: data}
|
||||
first := &biz.Authority{AuthorityID: 1401, AuthorityName: "no-defaults", ParentID: &actorID}
|
||||
if err := repo.CreateAuthority(ctx, first); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var menuLinks int64
|
||||
if err := db.Model(&authorityMenuPO{}).Where("sys_authority_authority_id = ?", first.AuthorityID).Count(&menuLinks).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if menuLinks != 0 {
|
||||
t.Fatalf("new child inherited an unowned default menu: count=%d", menuLinks)
|
||||
}
|
||||
if exists, err := policyExists(db, first.AuthorityID, "/menu/getMenu", "POST"); err != nil || exists {
|
||||
t.Fatalf("new child inherited an unowned default API: exists=%v err=%v", exists, err)
|
||||
}
|
||||
|
||||
if err := db.Create(&authorityMenuPO{SysAuthorityAuthorityID: actorID, SysBaseMenuID: dashboard.ID}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
policy := newPolicyRule(actorID, "/menu/getMenu", "POST")
|
||||
if err := db.Create(&policy).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second := &biz.Authority{AuthorityID: 1402, AuthorityName: "owned-defaults", ParentID: &actorID}
|
||||
if err := repo.CreateAuthority(ctx, second); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Model(&authorityMenuPO{}).Where("sys_authority_authority_id = ? AND sys_base_menu_id = ?", second.AuthorityID, dashboard.ID).Count(&menuLinks).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if menuLinks != 1 {
|
||||
t.Fatalf("new child did not inherit the owned dashboard menu: count=%d", menuLinks)
|
||||
}
|
||||
if exists, err := policyExists(db, second.AuthorityID, "/menu/getMenu", "POST"); err != nil || !exists {
|
||||
t.Fatalf("new child owned default API exists=%v err=%v", exists, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyAuthorityStrictValidatesCopiedMenusAndButtons(t *testing.T) {
|
||||
data := newTransactionTestData(t)
|
||||
enableStrictAuthorityTestMode(data)
|
||||
db := data.gormDB.WithContext(context.Background())
|
||||
rootID, actorID, sourceID := uint(888), uint(1500), uint(1501)
|
||||
if err := db.Create(&[]authorityPO{
|
||||
{AuthorityID: rootID, ParentID: authorityUintPointer(0)},
|
||||
{AuthorityID: actorID, ParentID: &rootID},
|
||||
{AuthorityID: sourceID, ParentID: &actorID},
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&[]menuPO{{ID: 10, Name: "owned"}, {ID: 11, Name: "unowned"}}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&[]menuButtonPO{{ID: 31, MenuID: 10, Name: "owned"}, {ID: 32, MenuID: 10, Name: "unowned"}}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&[]authorityMenuPO{
|
||||
{SysAuthorityAuthorityID: actorID, SysBaseMenuID: 10},
|
||||
{SysAuthorityAuthorityID: sourceID, SysBaseMenuID: 10},
|
||||
{SysAuthorityAuthorityID: sourceID, SysBaseMenuID: 11},
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&[]authorityButtonPO{
|
||||
{AuthorityID: actorID, MenuID: 10, ButtonID: 31},
|
||||
{AuthorityID: sourceID, MenuID: 10, ButtonID: 31},
|
||||
{AuthorityID: sourceID, MenuID: 10, ButtonID: 32},
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
|
||||
repo := &authorityAccessRepo{data: data}
|
||||
if err := repo.CopyAuthority(ctx, sourceID, &biz.Authority{AuthorityID: 1510, AuthorityName: "menu-fail", ParentID: &actorID}); err == nil {
|
||||
t.Fatal("CopyAuthority() copied a menu not assigned to the actor")
|
||||
}
|
||||
if err := db.Where("sys_authority_authority_id = ? AND sys_base_menu_id = ?", sourceID, 11).Delete(&authorityMenuPO{}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repo.CopyAuthority(ctx, sourceID, &biz.Authority{AuthorityID: 1511, AuthorityName: "button-fail", ParentID: &actorID}); err == nil {
|
||||
t.Fatal("CopyAuthority() copied a button not assigned to the actor")
|
||||
}
|
||||
if err := db.Where("authority_id = ? AND sys_base_menu_btn_id = ?", sourceID, 32).Delete(&authorityButtonPO{}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repo.CopyAuthority(ctx, sourceID, &biz.Authority{AuthorityID: 1512, AuthorityName: "valid", ParentID: &actorID}); err != nil {
|
||||
t.Fatalf("CopyAuthority() rejected owned permissions: %v", err)
|
||||
}
|
||||
for _, failedID := range []uint{1510, 1511} {
|
||||
var count int64
|
||||
if err := db.Unscoped().Model(&authorityPO{}).Where("authority_id = ?", failedID).Count(&count).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count != 0 {
|
||||
t.Fatalf("failed copy persisted authority %d", failedID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorityCreateAndCopyNormalizeZeroDataScope(t *testing.T) {
|
||||
data := newTransactionTestData(t)
|
||||
db := data.gormDB.WithContext(context.Background())
|
||||
repo := &authorityAccessRepo{data: data}
|
||||
|
||||
created := &biz.Authority{AuthorityID: 1600, AuthorityName: "created"}
|
||||
if err := repo.CreateAuthority(context.Background(), created); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if created.DataScope != 1 {
|
||||
t.Fatalf("created data scope = %d, want 1", created.DataScope)
|
||||
}
|
||||
|
||||
if err := db.Create(&authorityPO{AuthorityID: 1601, AuthorityName: "source", DataScope: 3}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
copied := &biz.Authority{AuthorityID: 1602, AuthorityName: "copied"}
|
||||
if err := repo.CopyAuthority(context.Background(), 1601, copied); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if copied.DataScope != 1 {
|
||||
t.Fatalf("copied data scope = %d, want 1", copied.DataScope)
|
||||
}
|
||||
|
||||
var stored []authorityPO
|
||||
if err := db.Where("authority_id IN ?", []uint{created.AuthorityID, copied.AuthorityID}).Order("authority_id").Find(&stored).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(stored) != 2 || stored[0].DataScope != 1 || stored[1].DataScope != 1 {
|
||||
t.Fatalf("stored normalized data scopes = %+v", stored)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorityMutationsRejectInvalidDataScope(t *testing.T) {
|
||||
data := newTransactionTestData(t)
|
||||
db := data.gormDB.WithContext(context.Background())
|
||||
repo := &authorityAccessRepo{data: data}
|
||||
if err := db.Create(&[]authorityPO{
|
||||
{AuthorityID: 1700, AuthorityName: "source", DataScope: 3},
|
||||
{AuthorityID: 1701, AuthorityName: "target", DataScope: 3},
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, scope := range []int{-1, 6} {
|
||||
if err := repo.CreateAuthority(context.Background(), &biz.Authority{AuthorityID: uint(1800 + scope + 1), AuthorityName: "invalid-create", DataScope: scope}); !errors.Is(err, errInvalidDataScope) {
|
||||
t.Fatalf("CreateAuthority() scope %d error = %v", scope, err)
|
||||
}
|
||||
if err := repo.CopyAuthority(context.Background(), 1700, &biz.Authority{AuthorityID: uint(1900 + scope + 1), AuthorityName: "invalid-copy", DataScope: scope}); !errors.Is(err, errInvalidDataScope) {
|
||||
t.Fatalf("CopyAuthority() scope %d error = %v", scope, err)
|
||||
}
|
||||
if err := repo.UpdateAuthority(context.Background(), &biz.Authority{AuthorityID: 1701, AuthorityName: "invalid-update", DataScope: scope}); !errors.Is(err, errInvalidDataScope) {
|
||||
t.Fatalf("UpdateAuthority() scope %d error = %v", scope, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateAuthorityZeroDataScopeKeepsStoredValue(t *testing.T) {
|
||||
data := newTransactionTestData(t)
|
||||
db := data.gormDB.WithContext(context.Background())
|
||||
if err := db.Create(&authorityPO{AuthorityID: 2000, AuthorityName: "before", DataScope: 3}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := (&authorityAccessRepo{data: data}).UpdateAuthority(context.Background(), &biz.Authority{AuthorityID: 2000, AuthorityName: "after", DataScope: 0}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var stored authorityPO
|
||||
if err := db.Where("authority_id = ?", 2000).First(&stored).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stored.DataScope != 3 || stored.AuthorityName != "after" {
|
||||
t.Fatalf("updated authority = %+v, want data scope 3 and name after", stored)
|
||||
}
|
||||
}
|
||||
|
||||
+53
-21
@@ -2,6 +2,7 @@ package data
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"reflect"
|
||||
"strings"
|
||||
@@ -18,6 +19,11 @@ import (
|
||||
// Casbin, while ownership columns on business tables are row-level scope.
|
||||
type dataScopeAuditEnqueue func(dataAccessLogPO)
|
||||
|
||||
var (
|
||||
errDataScopeRequired = errors.New("受控表访问缺少数据权限上下文")
|
||||
errInvalidDataScope = errors.New("数据权限范围不合法")
|
||||
)
|
||||
|
||||
func registerDataScopeCallbacks(db *gorm.DB, enqueue dataScopeAuditEnqueue) {
|
||||
if db == nil {
|
||||
return
|
||||
@@ -40,7 +46,7 @@ func registerDataScopeCallbacks(db *gorm.DB, enqueue dataScopeAuditEnqueue) {
|
||||
}
|
||||
c := db.Callback().Create()
|
||||
if c.Get("data_scope:stamp") == nil {
|
||||
_ = c.Before("gorm:create").Register("data_scope:stamp", stampOwnership)
|
||||
_ = c.Before("gorm:create").Register("data_scope:stamp", stampOwnership(enqueue))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +58,16 @@ func isControlledTable(db *gorm.DB) bool {
|
||||
return db.Statement.Schema != nil && !strings.HasPrefix(db.Statement.Table, "sys_") && (hasScopeField(db, "dept_id") || hasScopeField(db, "created_by"))
|
||||
}
|
||||
|
||||
func skipDataScope(db *gorm.DB) bool {
|
||||
skip, ok := db.Get("data_scope:skip")
|
||||
value, _ := skip.(bool)
|
||||
return ok && value
|
||||
}
|
||||
|
||||
func validDataScope(scope biz.DataScope) bool {
|
||||
return scope.UserID != 0 && scope.AuthorityID != 0 && scope.Scope >= 1 && scope.Scope <= 5 && scope.All == (scope.Scope == 1)
|
||||
}
|
||||
|
||||
func applyDataScope(operation string, enqueue dataScopeAuditEnqueue) func(*gorm.DB) {
|
||||
return func(db *gorm.DB) {
|
||||
if !isControlledTable(db) {
|
||||
@@ -60,15 +76,19 @@ func applyDataScope(operation string, enqueue dataScopeAuditEnqueue) func(*gorm.
|
||||
if _, done := db.Statement.Clauses["data_scope:applied"]; done {
|
||||
return
|
||||
}
|
||||
if skip, ok := db.Get("data_scope:skip"); ok {
|
||||
if value, _ := skip.(bool); value {
|
||||
return
|
||||
}
|
||||
if skipDataScope(db) {
|
||||
return
|
||||
}
|
||||
scope, ok := biz.DataScopeFromContext(db.Statement.Context)
|
||||
if !ok {
|
||||
slog.WarnContext(db.Statement.Context, "数据权限: 业务表访问无身份上下文, 已放行(待补 ctx / 或使用系统上下文)", "mod", "data-scope", "table", db.Statement.Table)
|
||||
recordDataScopeEvent(db, enqueue, "no_identity", operation, "无身份上下文访问受控表, 已放行", biz.DataScope{})
|
||||
slog.WarnContext(db.Statement.Context, "数据权限: 业务表访问无身份上下文, 已拒绝", "mod", "data-scope", "table", db.Statement.Table)
|
||||
recordDataScopeEvent(db, enqueue, "no_identity", operation, "无身份上下文访问受控表, 已拒绝", biz.DataScope{})
|
||||
_ = db.AddError(errDataScopeRequired)
|
||||
return
|
||||
}
|
||||
if !validDataScope(scope) {
|
||||
recordDataScopeEvent(db, enqueue, "invalid_scope", operation, "数据权限上下文不合法, 已拒绝", scope)
|
||||
_ = db.AddError(errInvalidDataScope)
|
||||
return
|
||||
}
|
||||
if (operation == "update" || operation == "delete") && !db.AllowGlobalUpdate && !hasWriteConditions(db) {
|
||||
@@ -86,10 +106,13 @@ func applyDataScope(operation string, enqueue dataScopeAuditEnqueue) func(*gorm.
|
||||
if hasScopeField(db, "dept_id") {
|
||||
ids := scope.DepartmentIDs
|
||||
if len(ids) == 0 {
|
||||
ids = []uint{0}
|
||||
db.Where("1 = 0")
|
||||
return
|
||||
}
|
||||
db.Where(table+".dept_id IN ?", ids)
|
||||
return
|
||||
}
|
||||
db.Where("1 = 0")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,19 +134,28 @@ func auditBlockedWrite(operation string, enqueue dataScopeAuditEnqueue) func(*go
|
||||
}
|
||||
}
|
||||
|
||||
func stampOwnership(db *gorm.DB) {
|
||||
if !isControlledTable(db) {
|
||||
return
|
||||
}
|
||||
scope, ok := biz.DataScopeFromContext(db.Statement.Context)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if hasScopeField(db, "created_by") && scope.UserID != 0 {
|
||||
db.Statement.SetColumn("created_by", scope.UserID, true)
|
||||
}
|
||||
if hasScopeField(db, "dept_id") && scope.PrimaryDeptID != 0 {
|
||||
db.Statement.SetColumn("dept_id", scope.PrimaryDeptID, true)
|
||||
func stampOwnership(enqueue dataScopeAuditEnqueue) func(*gorm.DB) {
|
||||
return func(db *gorm.DB) {
|
||||
if !isControlledTable(db) || skipDataScope(db) {
|
||||
return
|
||||
}
|
||||
scope, ok := biz.DataScopeFromContext(db.Statement.Context)
|
||||
if !ok {
|
||||
recordDataScopeEvent(db, enqueue, "no_identity", "create", "无身份上下文访问受控表, 已拒绝", biz.DataScope{})
|
||||
_ = db.AddError(errDataScopeRequired)
|
||||
return
|
||||
}
|
||||
if !validDataScope(scope) {
|
||||
recordDataScopeEvent(db, enqueue, "invalid_scope", "create", "数据权限上下文不合法, 已拒绝", scope)
|
||||
_ = db.AddError(errInvalidDataScope)
|
||||
return
|
||||
}
|
||||
if hasScopeField(db, "created_by") {
|
||||
db.Statement.SetColumn("created_by", scope.UserID, true)
|
||||
}
|
||||
if hasScopeField(db, "dept_id") {
|
||||
db.Statement.SetColumn("dept_id", scope.PrimaryDeptID, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,12 @@ func openDataScopeAuditTestDB(t *testing.T, name string) *gorm.DB {
|
||||
if err = db.AutoMigrate(&dataAccessLogPO{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The in-memory SQLite URI is intentionally named so the audit writer can
|
||||
// use a second GORM handle. Clear rows left by a prior -count iteration so
|
||||
// each test starts with an isolated logical database.
|
||||
if err = db.Unscoped().Where("1 = 1").Delete(&dataAccessLogPO{}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"kra/internal/biz"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type dataScopeRecord struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
DeptID uint
|
||||
CreatedBy uint
|
||||
Name string
|
||||
}
|
||||
|
||||
func (dataScopeRecord) TableName() string { return "business_scope_records" }
|
||||
|
||||
func newDataScopeTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.AutoMigrate(&dataScopeRecord{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
registerDataScopeCallbacks(db, nil)
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
||||
return db
|
||||
}
|
||||
|
||||
func TestDataScopeCallbacksFailClosedAndAllowExplicitSystemBypass(t *testing.T) {
|
||||
db := newDataScopeTestDB(t)
|
||||
seed := []dataScopeRecord{
|
||||
{DeptID: 10, CreatedBy: 7, Name: "visible"},
|
||||
{DeptID: 20, CreatedBy: 8, Name: "hidden"},
|
||||
{DeptID: 0, CreatedBy: 9, Name: "unassigned"},
|
||||
}
|
||||
if err := db.Set("data_scope:skip", true).Create(&seed).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var rows []dataScopeRecord
|
||||
if err := db.WithContext(context.Background()).Find(&rows).Error; !errors.Is(err, errDataScopeRequired) {
|
||||
t.Fatalf("query without data scope error = %v", err)
|
||||
}
|
||||
|
||||
invalidCtx := biz.NewDataScopeContext(context.Background(), biz.DataScope{UserID: 7, AuthorityID: 1, Scope: 1, All: false})
|
||||
if err := db.WithContext(invalidCtx).Find(&rows).Error; !errors.Is(err, errInvalidDataScope) {
|
||||
t.Fatalf("query with invalid data scope error = %v", err)
|
||||
}
|
||||
|
||||
scopedCtx := biz.NewDataScopeContext(context.Background(), biz.DataScope{UserID: 7, AuthorityID: 1, Scope: 3, DepartmentIDs: []uint{10}})
|
||||
rows = nil
|
||||
if err := db.WithContext(scopedCtx).Order("id").Find(&rows).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(rows) != 1 || rows[0].Name != "visible" {
|
||||
t.Fatalf("scoped query rows = %+v", rows)
|
||||
}
|
||||
|
||||
emptyCtx := biz.NewDataScopeContext(context.Background(), biz.DataScope{UserID: 7, AuthorityID: 1, Scope: 5})
|
||||
rows = nil
|
||||
if err := db.WithContext(emptyCtx).Find(&rows).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(rows) != 0 {
|
||||
t.Fatalf("empty custom scope exposed rows: %+v", rows)
|
||||
}
|
||||
|
||||
rows = nil
|
||||
if err := db.Set("data_scope:skip", true).Order("id").Find(&rows).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(rows) != len(seed) {
|
||||
t.Fatalf("explicit system bypass rows = %d, want %d", len(rows), len(seed))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDataScopeCreateRequiresIdentityAndStampsOwnership(t *testing.T) {
|
||||
db := newDataScopeTestDB(t)
|
||||
if err := db.Create(&dataScopeRecord{Name: "missing"}).Error; !errors.Is(err, errDataScopeRequired) {
|
||||
t.Fatalf("create without data scope error = %v", err)
|
||||
}
|
||||
|
||||
ctx := biz.NewDataScopeContext(context.Background(), biz.DataScope{UserID: 7, AuthorityID: 1, Scope: 3, PrimaryDeptID: 10, DepartmentIDs: []uint{10}})
|
||||
created := dataScopeRecord{Name: "owned", DeptID: 999, CreatedBy: 999}
|
||||
if err := db.WithContext(ctx).Create(&created).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var stored dataScopeRecord
|
||||
if err := db.Set("data_scope:skip", true).First(&stored, created.ID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stored.DeptID != 10 || stored.CreatedBy != 7 {
|
||||
t.Fatalf("stamped ownership = dept:%d user:%d, want 10/7", stored.DeptID, stored.CreatedBy)
|
||||
}
|
||||
}
|
||||
@@ -97,7 +97,10 @@ func (r *mediaRepo) DeleteUploadSession(ctx context.Context, id uint) error {
|
||||
}
|
||||
func (r *mediaRepo) UpsertChunk(ctx context.Context, uploadID uint, v *biz.UploadChunk) error {
|
||||
po := uploadChunkPO{UploadID: uploadID, ChunkIndex: v.Index, ChunkHash: v.Hash, Size: v.Size}
|
||||
return r.data.gormDB.WithContext(ctx).Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "upload_id"}, {Name: "chunk_index"}}, DoNothing: true}).Create(&po).Error
|
||||
return r.data.gormDB.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "upload_id"}, {Name: "chunk_index"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"chunk_hash", "size", "updated_at", "deleted_at"}),
|
||||
}).Create(&po).Error
|
||||
}
|
||||
func (r *mediaRepo) ListChunks(ctx context.Context, uploadID uint) ([]*biz.UploadChunk, error) {
|
||||
var pos []uploadChunkPO
|
||||
|
||||
+75
-22
@@ -263,27 +263,8 @@ func (r *menuRepo) SetAuthorityMenus(ctx context.Context, id uint, ids []uint) e
|
||||
if err := r.data.gormDB.WithContext(ctx).Where("authority_id = ?", id).First(&authorityPO{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
config := r.data.runtime.Admin()
|
||||
if actor, ok := biz.ActorFromContext(ctx); ok && config != nil && config.System != nil && config.System.UseStrictAuth {
|
||||
var authority authorityPO
|
||||
if err := r.data.gormDB.WithContext(ctx).Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if authority.ParentID != nil && *authority.ParentID != 0 {
|
||||
var allowedIDs []uint
|
||||
if err := r.data.gormDB.WithContext(ctx).Model(&authorityMenuPO{}).Where("sys_authority_authority_id = ?", actor.AuthorityID).Pluck("sys_base_menu_id", &allowedIDs).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
allowed := make(map[uint]bool, len(allowedIDs))
|
||||
for _, menuID := range allowedIDs {
|
||||
allowed[menuID] = true
|
||||
}
|
||||
for _, menuID := range ids {
|
||||
if !allowed[menuID] {
|
||||
return errors.New("添加失败,请勿跨级操作")
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := r.checkMenuAssignmentAuth(ctx, ids); err != nil {
|
||||
return err
|
||||
}
|
||||
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("sys_authority_authority_id = ?", id).Delete(&authorityMenuPO{}).Error; err != nil {
|
||||
@@ -346,8 +327,29 @@ func (r *menuRepo) DefaultRouterRoleIDs(ctx context.Context, id uint) ([]uint, e
|
||||
}
|
||||
|
||||
func (r *menuRepo) SetMenuRoles(ctx context.Context, id uint, ids []uint) error {
|
||||
access := &authorityAccessRepo{data: r.data}
|
||||
_, allowedAuthorities, strict, err := access.strictAuthorityAccess(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.checkMenuAssignmentAuth(ctx, []uint{id}); err != nil {
|
||||
return err
|
||||
}
|
||||
if strict {
|
||||
for _, authorityID := range ids {
|
||||
if !allowedAuthorities[authorityID] {
|
||||
return errors.New("您提交的角色ID不合法")
|
||||
}
|
||||
}
|
||||
}
|
||||
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("sys_base_menu_id = ?", id).Delete(&authorityMenuPO{}).Error; err != nil {
|
||||
if strict {
|
||||
if len(allowedAuthorities) > 0 {
|
||||
if err := tx.Where("sys_base_menu_id = ? AND sys_authority_authority_id IN ?", id, authorityIDs(allowedAuthorities)).Delete(&authorityMenuPO{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if err := tx.Where("sys_base_menu_id = ?", id).Delete(&authorityMenuPO{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
links := make([]authorityMenuPO, 0, len(ids))
|
||||
@@ -360,3 +362,54 @@ func (r *menuRepo) SetMenuRoles(ctx context.Context, id uint, ids []uint) error
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func authorityIDs(allowed map[uint]bool) []uint {
|
||||
ids := make([]uint, 0, len(allowed))
|
||||
for id := range allowed {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func (r *menuRepo) checkMenuAssignmentAuth(ctx context.Context, menuIDs []uint) error {
|
||||
actor, allowedAuthorities, strict, err := (&authorityAccessRepo{data: r.data}).strictAuthorityAccess(ctx)
|
||||
if err != nil || !strict {
|
||||
return err
|
||||
}
|
||||
return checkMenuAssignment(r.data.gormDB.WithContext(ctx), actor.AuthorityID, allowedAuthorities[actor.AuthorityID], menuIDs)
|
||||
}
|
||||
|
||||
func checkMenuAssignment(db *gorm.DB, actorID uint, root bool, menuIDs []uint) error {
|
||||
if len(menuIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
unique := make(map[uint]struct{}, len(menuIDs))
|
||||
for _, menuID := range menuIDs {
|
||||
unique[menuID] = struct{}{}
|
||||
}
|
||||
if root {
|
||||
var count int64
|
||||
if err := db.Model(&menuPO{}).Where("id IN ?", menuIDs).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count != int64(len(unique)) {
|
||||
return errors.New("添加失败,菜单不存在")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
var allowedIDs []uint
|
||||
if err := db.Model(&authorityMenuPO{}).
|
||||
Where("sys_authority_authority_id = ?", actorID).Pluck("sys_base_menu_id", &allowedIDs).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
allowed := make(map[uint]bool, len(allowedIDs))
|
||||
for _, menuID := range allowedIDs {
|
||||
allowed[menuID] = true
|
||||
}
|
||||
for menuID := range unique {
|
||||
if !allowed[menuID] {
|
||||
return errors.New("添加失败,请勿跨级操作")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -139,3 +139,46 @@ func TestMenuUpdateRebuildsRelationsWithoutChangingRequestedIdentity(t *testing.
|
||||
t.Fatalf("authority-button links = %d, want 1", links)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetMenuRolesStrictOnlyChangesManagedAuthorities(t *testing.T) {
|
||||
data := newMenuTestData(t)
|
||||
enableStrictAuthorityTestMode(data)
|
||||
db := data.gormDB.WithContext(context.Background())
|
||||
rootID, actorID, childID, siblingID := uint(888), uint(4001), uint(4002), uint(5001)
|
||||
if err := db.Create(&[]authorityPO{
|
||||
{AuthorityID: rootID, ParentID: authorityUintPointer(0)},
|
||||
{AuthorityID: actorID, ParentID: &rootID},
|
||||
{AuthorityID: childID, ParentID: &actorID},
|
||||
{AuthorityID: siblingID, ParentID: &rootID},
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
menu := menuPO{ID: 10, Name: "managed", Path: "managed"}
|
||||
if err := db.Create(&menu).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&[]authorityMenuPO{
|
||||
{SysAuthorityAuthorityID: actorID, SysBaseMenuID: menu.ID},
|
||||
{SysAuthorityAuthorityID: siblingID, SysBaseMenuID: menu.ID},
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
|
||||
repo := &menuRepo{data: data}
|
||||
if err := repo.SetMenuRoles(ctx, menu.ID, []uint{siblingID}); err == nil {
|
||||
t.Fatal("SetMenuRoles() accepted an out-of-scope authority")
|
||||
}
|
||||
if err := repo.SetMenuRoles(ctx, menu.ID, []uint{childID}); err != nil {
|
||||
t.Fatalf("SetMenuRoles() rejected a managed authority: %v", err)
|
||||
}
|
||||
var siblingCount, childCount int64
|
||||
if err := db.Model(&authorityMenuPO{}).Where("sys_authority_authority_id = ? AND sys_base_menu_id = ?", siblingID, menu.ID).Count(&siblingCount).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Model(&authorityMenuPO{}).Where("sys_authority_authority_id = ? AND sys_base_menu_id = ?", childID, menu.ID).Count(&childCount).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if siblingCount != 1 || childCount != 1 {
|
||||
t.Fatalf("menu-role counts sibling/child = %d/%d, want 1/1", siblingCount, childCount)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,12 @@ func (r *permissionRepo) Buttons(ctx context.Context, menuID uint) ([]*biz.MenuB
|
||||
return out, nil
|
||||
}
|
||||
func (r *permissionRepo) SetAuthorityButtons(ctx context.Context, aid uint, buttons map[uint][]uint) error {
|
||||
if err := (&authorityAccessRepo{data: r.data}).checkAuthorityIDAuth(ctx, aid); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.checkButtonAssignmentAuth(ctx, buttons); err != nil {
|
||||
return err
|
||||
}
|
||||
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("authority_id = ?", aid).Delete(&authorityButtonPO{}).Error; err != nil {
|
||||
return err
|
||||
@@ -74,6 +80,12 @@ func (r *permissionRepo) SelectedButtons(ctx context.Context, aid, menuID uint)
|
||||
return ids, nil
|
||||
}
|
||||
func (r *permissionRepo) SetSelectedButtons(ctx context.Context, aid, menuID uint, ids []uint) error {
|
||||
if err := (&authorityAccessRepo{data: r.data}).checkAuthorityIDAuth(ctx, aid); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.checkButtonAssignmentAuth(ctx, map[uint][]uint{menuID: ids}); err != nil {
|
||||
return err
|
||||
}
|
||||
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("authority_id = ? AND sys_menu_id = ?", aid, menuID).Delete(&authorityButtonPO{}).Error; err != nil {
|
||||
return err
|
||||
@@ -104,3 +116,53 @@ func (r *permissionRepo) AuthorityButtonIDs(ctx context.Context, aid uint) ([]ui
|
||||
err := r.data.gormDB.WithContext(ctx).Model(&authorityButtonPO{}).Where("authority_id = ?", aid).Pluck("sys_base_menu_btn_id", &ids).Error
|
||||
return ids, err
|
||||
}
|
||||
|
||||
func (r *permissionRepo) checkButtonAssignmentAuth(ctx context.Context, buttons map[uint][]uint) error {
|
||||
actor, allowedAuthorities, strict, err := (&authorityAccessRepo{data: r.data}).strictAuthorityAccess(ctx)
|
||||
if err != nil || !strict {
|
||||
return err
|
||||
}
|
||||
return checkButtonAssignment(r.data.gormDB.WithContext(ctx), actor.AuthorityID, allowedAuthorities[actor.AuthorityID], buttons)
|
||||
}
|
||||
|
||||
func checkButtonAssignment(db *gorm.DB, actorID uint, root bool, buttons map[uint][]uint) error {
|
||||
requested := make(map[[2]uint]struct{})
|
||||
buttonIDs := make([]uint, 0)
|
||||
for menuID, ids := range buttons {
|
||||
for _, buttonID := range ids {
|
||||
key := [2]uint{menuID, buttonID}
|
||||
if _, exists := requested[key]; exists {
|
||||
continue
|
||||
}
|
||||
requested[key] = struct{}{}
|
||||
buttonIDs = append(buttonIDs, buttonID)
|
||||
}
|
||||
}
|
||||
if len(requested) == 0 {
|
||||
return nil
|
||||
}
|
||||
allowed := make(map[[2]uint]bool, len(requested))
|
||||
if root {
|
||||
var rows []menuButtonPO
|
||||
if err := db.Where("id IN ?", buttonIDs).Find(&rows).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, row := range rows {
|
||||
allowed[[2]uint{row.MenuID, row.ID}] = true
|
||||
}
|
||||
} else {
|
||||
var rows []authorityButtonPO
|
||||
if err := db.Where("authority_id = ?", actorID).Find(&rows).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, row := range rows {
|
||||
allowed[[2]uint{row.MenuID, row.ButtonID}] = true
|
||||
}
|
||||
}
|
||||
for key := range requested {
|
||||
if !allowed[key] {
|
||||
return errors.New("添加失败,请勿跨级操作")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"kra/internal/biz"
|
||||
"kra/internal/conf"
|
||||
)
|
||||
|
||||
@@ -81,3 +82,41 @@ func TestPermissionSetSelectedButtonsPreservesDuplicates(t *testing.T) {
|
||||
t.Fatalf("selected = %#v, want duplicate button IDs preserved", selected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetSelectedButtonsStrictRequiresManagedRoleAndOwnedButton(t *testing.T) {
|
||||
data := newMenuTestData(t)
|
||||
enableStrictAuthorityTestMode(data)
|
||||
db := data.gormDB.WithContext(context.Background())
|
||||
rootID, actorID, childID, siblingID := uint(888), uint(6001), uint(6002), uint(7001)
|
||||
if err := db.Create(&[]authorityPO{
|
||||
{AuthorityID: rootID, ParentID: authorityUintPointer(0)},
|
||||
{AuthorityID: actorID, ParentID: &rootID},
|
||||
{AuthorityID: childID, ParentID: &actorID},
|
||||
{AuthorityID: siblingID, ParentID: &rootID},
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&menuPO{ID: 10, Name: "managed", Path: "managed"}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&[]menuButtonPO{
|
||||
{ID: 31, MenuID: 10, Name: "owned"},
|
||||
{ID: 32, MenuID: 10, Name: "unowned"},
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&authorityButtonPO{AuthorityID: actorID, MenuID: 10, ButtonID: 31}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
|
||||
repo := &permissionRepo{data: data}
|
||||
if err := repo.SetSelectedButtons(ctx, siblingID, 10, []uint{31}); err == nil {
|
||||
t.Fatal("SetSelectedButtons() accepted an out-of-scope authority")
|
||||
}
|
||||
if err := repo.SetSelectedButtons(ctx, childID, 10, []uint{32}); err == nil {
|
||||
t.Fatal("SetSelectedButtons() accepted a button not assigned to the actor")
|
||||
}
|
||||
if err := repo.SetSelectedButtons(ctx, childID, 10, []uint{31}); err != nil {
|
||||
t.Fatalf("SetSelectedButtons() rejected a managed assignment: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,11 @@ func (i *tokenIssuer) IssueToken(user *biz.User, authorityID uint, mustChangePas
|
||||
if expires <= 0 {
|
||||
expires = settings.Expires
|
||||
}
|
||||
token, claims, err := adminauth.Generate(settings.SigningKey, settings.Issuer, expires, settings.Buffer, user.ID, authorityID, user.UUID, user.Username, user.NickName, mustChangePassword)
|
||||
passwordVersion := int64(0)
|
||||
if user.PasswordUpdatedAt != nil {
|
||||
passwordVersion = user.PasswordUpdatedAt.UnixNano()
|
||||
}
|
||||
token, claims, err := adminauth.Generate(settings.SigningKey, settings.Issuer, expires, settings.Buffer, user.ID, authorityID, user.UUID, user.Username, user.NickName, mustChangePassword, passwordVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -102,17 +106,19 @@ func (i *tokenIssuer) ReissueToken(source *biz.AuthClaims, authorityID uint) (*b
|
||||
}
|
||||
settings := i.settings.JWTSettings()
|
||||
claims := &adminauth.Claims{
|
||||
UUID: source.UUID,
|
||||
ID: source.ID,
|
||||
Username: source.Username,
|
||||
NickName: source.NickName,
|
||||
AuthorityID: authorityID,
|
||||
BufferTime: int64(source.BufferTime / time.Second),
|
||||
UserType: source.UserType,
|
||||
MustChangePwd: source.MustChangePwd,
|
||||
UUID: source.UUID,
|
||||
ID: source.ID,
|
||||
Username: source.Username,
|
||||
NickName: source.NickName,
|
||||
AuthorityID: authorityID,
|
||||
BufferTime: int64(source.BufferTime / time.Second),
|
||||
UserType: source.UserType,
|
||||
MustChangePwd: source.MustChangePwd,
|
||||
PasswordVersion: source.PasswordVersion,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Audience: jwt.ClaimStrings(append([]string(nil), source.Audience...)),
|
||||
Issuer: source.Issuer,
|
||||
IssuedAt: jwt.NewNumericDate(source.IssuedAt),
|
||||
NotBefore: jwt.NewNumericDate(source.NotBefore),
|
||||
ExpiresAt: jwt.NewNumericDate(source.ExpiresAt),
|
||||
},
|
||||
@@ -142,5 +148,9 @@ func (i *tokenIssuer) ParseToken(token string) (*biz.AuthClaims, error) {
|
||||
}
|
||||
audience := make([]string, len(claims.Audience))
|
||||
copy(audience, claims.Audience)
|
||||
return &biz.AuthClaims{UUID: claims.UUID, ID: claims.ID, Username: claims.Username, NickName: claims.NickName, AuthorityID: claims.AuthorityID, UserType: claims.UserType, BufferTime: time.Duration(claims.BufferTime) * time.Second, MustChangePwd: claims.MustChangePwd, Issuer: claims.Issuer, Audience: audience, NotBefore: claims.NotBefore.Time, ExpiresAt: claims.ExpiresAt.Time}, nil
|
||||
issuedAt := time.Time{}
|
||||
if claims.IssuedAt != nil {
|
||||
issuedAt = claims.IssuedAt.Time
|
||||
}
|
||||
return &biz.AuthClaims{UUID: claims.UUID, ID: claims.ID, Username: claims.Username, NickName: claims.NickName, AuthorityID: claims.AuthorityID, UserType: claims.UserType, BufferTime: time.Duration(claims.BufferTime) * time.Second, MustChangePwd: claims.MustChangePwd, PasswordVersion: claims.PasswordVersion, Issuer: claims.Issuer, Audience: audience, IssuedAt: issuedAt, NotBefore: claims.NotBefore.Time, ExpiresAt: claims.ExpiresAt.Time}, nil
|
||||
}
|
||||
|
||||
+38
-6
@@ -318,6 +318,10 @@ func (r *userRepo) CreateUser(ctx context.Context, user *biz.User) (*biz.User, e
|
||||
}
|
||||
|
||||
func (r *userRepo) CreateUserWithAuthorities(ctx context.Context, user *biz.User, authorityIDs []uint) (*biz.User, error) {
|
||||
requestedAuthorities := append([]uint{user.AuthorityID}, authorityIDs...)
|
||||
if err := (&authorityAccessRepo{data: r.data}).checkAuthorityIDsAuth(ctx, requestedAuthorities); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user.UUID == "" {
|
||||
user.UUID = uuid.NewString()
|
||||
}
|
||||
@@ -377,9 +381,23 @@ func (r *userRepo) CreateUserWithAuthorities(ctx context.Context, user *biz.User
|
||||
}
|
||||
|
||||
func (r *userRepo) UpdateUser(ctx context.Context, user *biz.User) error {
|
||||
if err := r.checkUserUpdateAuth(ctx, user); err != nil {
|
||||
return err
|
||||
}
|
||||
return r.updateUser(r.data.gormDB.WithContext(ctx), user)
|
||||
}
|
||||
|
||||
func (r *userRepo) checkUserUpdateAuth(ctx context.Context, user *biz.User) error {
|
||||
access := &authorityAccessRepo{data: r.data}
|
||||
if err := access.checkUserIDAuth(ctx, user.ID, false); err != nil {
|
||||
return err
|
||||
}
|
||||
if user.AuthorityID != 0 {
|
||||
return access.checkAuthorityIDAuth(ctx, user.AuthorityID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *userRepo) UpdateSelfUser(ctx context.Context, user *biz.User) error {
|
||||
updates := make(map[string]any)
|
||||
if user.NickName != "" {
|
||||
@@ -453,6 +471,13 @@ func (r *userRepo) updateUser(tx *gorm.DB, user *biz.User) error {
|
||||
}
|
||||
|
||||
func (r *userRepo) UpdateUserWithAuthorities(ctx context.Context, user *biz.User, authorityIDs []uint) error {
|
||||
access := &authorityAccessRepo{data: r.data}
|
||||
if err := r.checkUserUpdateAuth(ctx, user); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := access.checkAuthorityIDsAuth(ctx, authorityIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := r.updateUser(tx, user); err != nil {
|
||||
return err
|
||||
@@ -464,6 +489,9 @@ func (r *userRepo) UpdateUserWithAuthorities(ctx context.Context, user *biz.User
|
||||
})
|
||||
}
|
||||
func (r *userRepo) DeleteUser(ctx context.Context, id uint) error {
|
||||
if err := (&authorityAccessRepo{data: r.data}).checkUserIDAuth(ctx, id, false); err != nil {
|
||||
return err
|
||||
}
|
||||
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("id = ?", id).Delete(&userPO{}).Error; err != nil {
|
||||
return err
|
||||
@@ -481,6 +509,9 @@ func (r *userRepo) DeleteUser(ctx context.Context, id uint) error {
|
||||
})
|
||||
}
|
||||
func (r *userRepo) UpdatePassword(ctx context.Context, id uint, password string, clearMustChange bool) error {
|
||||
if err := (&authorityAccessRepo{data: r.data}).checkUserIDAuth(ctx, id, true); err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
updates := map[string]any{"password": password, "password_updated_at": now}
|
||||
if clearMustChange {
|
||||
@@ -502,17 +533,18 @@ func (r *userRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error
|
||||
}
|
||||
|
||||
func (r *userRepo) SetUserAuthorities(ctx context.Context, id uint, authorityIDs []uint) error {
|
||||
access := &authorityAccessRepo{data: r.data}
|
||||
if err := access.checkUserIDAuth(ctx, id, false); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := access.checkAuthorityIDsAuth(ctx, authorityIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var user userPO
|
||||
if err := tx.Where("id = ?", id).First(&user).Error; err != nil {
|
||||
return errors.New("查询用户数据失败")
|
||||
}
|
||||
access := &authorityAccessRepo{data: r.data}
|
||||
for _, authorityID := range authorityIDs {
|
||||
if err := access.checkAuthorityIDAuth(ctx, authorityID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return setUserAuthorities(tx, id, authorityIDs)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"kra/internal/biz"
|
||||
)
|
||||
|
||||
func seedStrictUserTree(t *testing.T, data *Data) (actorID, childID, siblingID uint, users []userPO) {
|
||||
t.Helper()
|
||||
enableStrictAuthorityTestMode(data)
|
||||
db := data.gormDB.WithContext(context.Background())
|
||||
rootID := uint(888)
|
||||
actorID, childID, siblingID = 8001, 8002, 9001
|
||||
if err := db.Create(&[]authorityPO{
|
||||
{AuthorityID: rootID, ParentID: authorityUintPointer(0)},
|
||||
{AuthorityID: actorID, ParentID: &rootID},
|
||||
{AuthorityID: childID, ParentID: &actorID},
|
||||
{AuthorityID: siblingID, ParentID: &rootID},
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
users = []userPO{
|
||||
{Username: "actor-user", Password: "hash", AuthorityID: actorID, Enable: 1},
|
||||
{Username: "managed-user", Password: "hash", AuthorityID: childID, Enable: 1},
|
||||
{Username: "outside-user", Password: "hash", AuthorityID: siblingID, Enable: 1},
|
||||
}
|
||||
if err := db.Create(&users).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return actorID, childID, siblingID, users
|
||||
}
|
||||
|
||||
func TestStrictUserMutationsRejectOutsideTargetAndAllowSelfPassword(t *testing.T) {
|
||||
data := newTransactionTestData(t)
|
||||
actorID, _, _, users := seedStrictUserTree(t, data)
|
||||
ctx := biz.NewActorContext(context.Background(), biz.Actor{UserID: users[0].ID, AuthorityID: actorID})
|
||||
repo := &userRepo{data: data}
|
||||
|
||||
if err := repo.UpdateUser(ctx, &biz.User{ID: users[2].ID, NickName: "changed"}); err == nil {
|
||||
t.Fatal("UpdateUser() accepted an out-of-scope target")
|
||||
}
|
||||
if err := repo.DeleteUser(ctx, users[2].ID); err == nil {
|
||||
t.Fatal("DeleteUser() accepted an out-of-scope target")
|
||||
}
|
||||
if err := repo.UpdatePassword(ctx, users[2].ID, "new-hash", false); err == nil {
|
||||
t.Fatal("UpdatePassword() accepted an out-of-scope target")
|
||||
}
|
||||
if err := repo.UpdatePassword(ctx, users[0].ID, "self-hash", true); err != nil {
|
||||
t.Fatalf("UpdatePassword() rejected the current user: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrictUserCreationAndRoleAssignmentRequireManagedAuthorities(t *testing.T) {
|
||||
data := newTransactionTestData(t)
|
||||
actorID, childID, siblingID, users := seedStrictUserTree(t, data)
|
||||
ctx := biz.NewActorContext(context.Background(), biz.Actor{UserID: users[0].ID, AuthorityID: actorID})
|
||||
repo := &userRepo{data: data}
|
||||
|
||||
if _, err := repo.CreateUserWithAuthorities(ctx, &biz.User{Username: "outside-create", Password: "hash", AuthorityID: siblingID, Enable: 1}, nil); err == nil {
|
||||
t.Fatal("CreateUserWithAuthorities() accepted an out-of-scope primary authority")
|
||||
}
|
||||
if _, err := repo.CreateUserWithAuthorities(ctx, &biz.User{Username: "managed-create", Password: "hash", AuthorityID: childID, Enable: 1}, []uint{childID}); err != nil {
|
||||
t.Fatalf("CreateUserWithAuthorities() rejected managed authorities: %v", err)
|
||||
}
|
||||
if err := repo.SetUserAuthorities(ctx, users[1].ID, []uint{siblingID}); err == nil {
|
||||
t.Fatal("SetUserAuthorities() accepted an out-of-scope authority")
|
||||
}
|
||||
if err := repo.SetUserAuthorities(ctx, users[2].ID, []uint{childID}); err == nil {
|
||||
t.Fatal("SetUserAuthorities() accepted an out-of-scope user target")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrictUserUpdateRejectsPrimaryAuthorityOutsideManagedTree(t *testing.T) {
|
||||
data := newTransactionTestData(t)
|
||||
actorID, childID, siblingID, users := seedStrictUserTree(t, data)
|
||||
ctx := biz.NewActorContext(context.Background(), biz.Actor{UserID: users[0].ID, AuthorityID: actorID})
|
||||
repo := &userRepo{data: data}
|
||||
|
||||
if err := repo.UpdateUser(ctx, &biz.User{ID: users[1].ID, NickName: "changed", AuthorityID: siblingID}); err == nil {
|
||||
t.Fatal("UpdateUser() accepted a primary authority outside the managed tree")
|
||||
}
|
||||
if err := repo.UpdateUserWithAuthorities(ctx, &biz.User{ID: users[1].ID, NickName: "changed", AuthorityID: siblingID}, []uint{childID}); err == nil {
|
||||
t.Fatal("UpdateUserWithAuthorities() accepted a primary authority outside the managed tree")
|
||||
}
|
||||
|
||||
var stored userPO
|
||||
if err := data.gormDB.WithContext(context.Background()).First(&stored, users[1].ID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stored.AuthorityID != childID || stored.NickName == "changed" {
|
||||
t.Fatalf("rejected update changed user: %+v", stored)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user