package data import ( "context" "errors" "kra/internal/biz" "gorm.io/gorm" ) type authorityAccessRepo struct{ data *Data } func NewAuthorityAccessRepo(data *Data) biz.AuthorityAccessRepo { return &authorityAccessRepo{data: data} } func (r *authorityAccessRepo) strictAuthorityIDs(ctx context.Context, actorID uint) (map[uint]bool, error) { allowed := make(map[uint]bool) if actorID == 0 { return allowed, nil } var actor authorityPO if err := r.data.gormDB.WithContext(ctx).Where("authority_id = ?", actorID).First(&actor).Error; err != nil { return nil, err } var authorities []authorityPO if err := r.data.gormDB.WithContext(ctx).Find(&authorities).Error; err != nil { return nil, err } children := make(map[uint][]uint) for _, authority := range authorities { if authority.ParentID != nil { children[*authority.ParentID] = append(children[*authority.ParentID], authority.AuthorityID) } } var walk func(uint) walk = func(id uint) { for _, child := range children[id] { if !allowed[child] { allowed[child] = true walk(child) } } } if actor.ParentID == nil || *actor.ParentID == 0 { allowed[actorID] = true } walk(actorID) return allowed, nil } func (r *authorityAccessRepo) checkAuthorityIDAuth(ctx context.Context, targetID uint) error { config := r.data.runtime.Admin() if config == nil || config.System == nil || !config.System.UseStrictAuth { return nil } actor, ok := biz.ActorFromContext(ctx) if !ok { return errors.New("您提交的角色ID不合法") } allowed, err := r.strictAuthorityIDs(ctx, actor.AuthorityID) if err != nil { return err } if !allowed[targetID] { return errors.New("您提交的角色ID不合法") } 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 } } return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { // The administration contract checks existence with First and treats every result other than // ErrRecordNotFound as a duplicate-role error. Keep that precedence so // duplicate IDs win over strict-tree validation and the API message stays // compatible. var existing authorityPO if err := tx.Where("authority_id = ?", value.AuthorityID).First(&existing).Error; !errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("存在相同角色id") } if value.DefaultRouter == "" { value.DefaultRouter = "dashboard" } 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 { return err } value.CreatedAt, value.UpdatedAt = po.CreatedAt, po.UpdatedAt value.DataScope, value.DefaultRouter = po.DataScope, po.DefaultRouter 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 } } 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 { rules = append(rules, newPolicyRule(value.AuthorityID, item.path, item.method)) } 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 { 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 if err := tx.Where("authority_id = ?", value.AuthorityID).First(&existing).Error; !errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("存在相同角色id") } // UpdateCasbin checks that the newly-created role is in the actor's // strict-authority subtree. Before inserting it, the equivalent check is // 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 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 { return err } value.CreatedAt, value.UpdatedAt = po.CreatedAt, po.UpdatedAt value.DataScope, value.DefaultRouter = po.DataScope, po.DefaultRouter copyLinks := func(table string, destination any, columns map[string]any) error { return tx.Table(table).Where(columns).Find(destination).Error } var menus []authorityMenuPO if err := copyLinks("sys_authority_menus", &menus, map[string]any{"sys_authority_authority_id": sourceID}); err != nil { return err } for i := range menus { menus[i].SysAuthorityAuthorityID = value.AuthorityID } if len(menus) > 0 { if err := tx.Create(&menus).Error; err != nil { return err } } if len(menus) > 0 { menuIDs := make([]uint, 0, len(menus)) for _, menu := range menus { menuIDs = append(menuIDs, menu.SysBaseMenuID) } var copiedMenus []menuPO if err := tx.Where("id IN ?", menuIDs).Order("sort").Find(&copiedMenus).Error; err != nil { return err } value.Menus = make([]*biz.Menu, 0, len(copiedMenus)) for _, menu := range copiedMenus { value.Menus = append(value.Menus, menuFromPO(menu)) } } apis, err := policyRowsForAuthority(tx, sourceID) if err != nil { return err } copiedRules := make([]casbinRulePO, 0, len(apis)) seenPolicies := make(map[string]struct{}, len(apis)) for _, api := range apis { if strict { allowed, err := r.copyPolicyAllowed(ctx, tx, actor.AuthorityID, api.V1, api.V2) if err != nil { return err } if !allowed { return errors.New("存在api不在权限列表中") } } key := api.V1 + "\x00" + api.V2 if _, ok := seenPolicies[key]; ok { continue } seenPolicies[key] = struct{}{} copiedRules = append(copiedRules, newPolicyRule(value.AuthorityID, api.V1, api.V2)) } if len(copiedRules) > 0 { if err := tx.Create(&copiedRules).Error; err != nil { return err } } var buttons []authorityButtonPO if err := copyLinks("sys_authority_btns", &buttons, map[string]any{"authority_id": sourceID}); err != nil { return err } for i := range buttons { buttons[i].AuthorityID = value.AuthorityID } if len(buttons) > 0 { if err := tx.Create(&buttons).Error; err != nil { return err } } return nil }) } // copyPolicyAllowed mirrors the administration API-list and policy validation. // A policy must refer to a currently registered API. Root roles // may copy any registered API, while non-root roles may copy only APIs present // in their own Casbin policy set. func (r *authorityAccessRepo) copyPolicyAllowed(ctx context.Context, tx *gorm.DB, actorID uint, path, method string) (bool, error) { var actor authorityPO if err := tx.WithContext(ctx).Where("authority_id = ?", actorID).First(&actor).Error; err != nil { return false, err } var apiCount int64 if err := tx.WithContext(ctx).Model(&apiPO{}).Where("path = ? AND method = ?", path, method).Count(&apiCount).Error; err != nil { return false, err } if apiCount == 0 { return false, nil } if actor.ParentID == nil || *actor.ParentID == 0 { return true, nil } return policyExists(tx.WithContext(ctx), actorID, path, method) } func (r *authorityAccessRepo) UpdateAuthority(ctx context.Context, value *biz.Authority) error { db := r.data.gormDB.WithContext(ctx) var current authorityPO if err := db.Where("authority_id = ?", value.AuthorityID).First(¤t).Error; err != nil { return errors.New("查询角色数据失败") } updates := &authorityPO{AuthorityName: value.AuthorityName, ParentID: value.ParentID, DataScope: value.DataScope, DefaultRouter: value.DefaultRouter} return db.Model(¤t).Updates(updates).Error } func (r *authorityAccessRepo) DeleteAuthority(ctx context.Context, id uint) error { 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 { if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("该角色不存在") } return err } var users, children int64 // Associated-user checks only block on users that still // exist (and are not soft-deleted). Counting raw join rows would make an // orphaned association impossible to clean up, which is observably // different from the administration contract. var linkedUserIDs []uint if err := tx.Model(&userAuthorityPO{}).Where("sys_authority_authority_id = ?", id).Pluck("sys_user_id", &linkedUserIDs).Error; err != nil { return err } if len(linkedUserIDs) > 0 { if err := tx.Model(&userPO{}).Where("id IN ?", linkedUserIDs).Count(&users).Error; err != nil { return err } if users > 0 { return errors.New("此角色有用户正在使用禁止删除") } } if err := tx.Model(&userPO{}).Where("authority_id = ?", id).Count(&users).Error; err != nil { return err } if users > 0 { return errors.New("此角色有用户正在使用禁止删除") } if err := tx.Model(&authorityPO{}).Where("parent_id = ?", id).Count(&children).Error; err != nil { return err } if children > 0 { return errors.New("此角色存在子角色不允许删除") } if err := tx.Where("sys_authority_authority_id = ?", id).Delete(&userAuthorityPO{}).Error; err != nil { return err } if err := tx.Where("sys_authority_authority_id = ?", id).Delete(&authorityMenuPO{}).Error; err != nil { return err } if err := deletePoliciesForAuthority(tx, id); err != nil { return err } if err := tx.Where("authority_id = ?", id).Delete(&authorityAPIPO{}).Error; err != nil { return err } if err := tx.Where("authority_id = ?", id).Delete(&authorityButtonPO{}).Error; err != nil { return err } // The compatible delete removes the role row permanently with Unscoped().Delete, // so reusing an authority ID after deletion behaves the same way. return tx.Unscoped().Delete(&authorityPO{}, "authority_id = ?", id).Error }) } func (r *authorityAccessRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error) { db := r.data.gormDB.WithContext(ctx) var allowed map[uint]bool config := r.data.runtime.Admin() if actor, ok := biz.ActorFromContext(ctx); ok { // The current authority is loaded even when strict mode is disabled; // an invalid token authority therefore fails the list request instead of // exposing the complete role tree. var current authorityPO if err := db.Where("authority_id = ?", actor.AuthorityID).First(¤t).Error; err != nil { return nil, err } if config != nil && config.System != nil && config.System.UseStrictAuth { var err error allowed, err = r.strictAuthorityIDs(ctx, actor.AuthorityID) if err != nil { return nil, err } } } var pos []authorityPO if err := db.Find(&pos).Error; err != nil { return nil, err } out := make([]*biz.Authority, 0, len(pos)) for _, po := range pos { if allowed != nil && !allowed[po.AuthorityID] { continue } v := toBizAuthority(po) out = append(out, &v) } return out, nil } func (r *authorityAccessRepo) SetAuthorityUsers(ctx context.Context, id uint, ids []uint) error { 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 err := tx.Where("sys_authority_authority_id = ?", id).Delete(&userAuthorityPO{}).Error; err != nil { return err } selected := map[uint]bool{} for _, uid := range ids { selected[uid] = true } for _, uid := range oldIDs { if selected[uid] { continue } var user userPO if err := tx.First(&user, uid).Error; err == nil && user.AuthorityID == id { var another userAuthorityPO if err = tx.Where("sys_user_id = ?", uid).First(&another).Error; err != nil { // Missing users and association lookup errors are ignored // while choosing a replacement role. continue } if err = tx.Model(&user).Update("authority_id", another.SysAuthorityAuthorityID).Error; err != nil { return err } } } links := make([]userAuthorityPO, 0, len(ids)) for _, uid := range ids { links = append(links, userAuthorityPO{SysUserID: uid, SysAuthorityAuthorityID: id}) } if len(links) > 0 { if err := tx.Create(&links).Error; err != nil { return err } } 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 { 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 } if err := tx.Where("sys_authority_authority_id = ?", id).Delete(&authorityDepartmentPO{}).Error; err != nil { return err } if scope == 5 { links := make([]authorityDepartmentPO, 0, len(deptIDs)) for _, did := range deptIDs { links = append(links, authorityDepartmentPO{AuthorityID: id, DepartmentID: did}) } if len(links) > 0 { return tx.Create(&links).Error } } 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 return ids, err } 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 identity.PrimaryDeptID = user.DeptID var authority authorityPO _ = r.data.gormDB.WithContext(ctx).Select("authority_id", "data_scope").First(&authority, "authority_id = ?", authorityID).Error identity.Scope = authority.DataScope if identity.Scope == 0 { identity.Scope = 1 } 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) } 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 children := make(map[uint][]uint, len(departments)) for _, department := range departments { children[department.ParentID] = append(children[department.ParentID], department.ID) } queue := make([]uint, 0, len(selected)) for id := range selected { queue = append(queue, id) } visited := make(map[uint]struct{}, len(selected)) for len(queue) > 0 { current := queue[0] queue = queue[1:] if _, ok := visited[current]; ok { continue } visited[current] = struct{}{} for _, child := range children[current] { if _, ok := visited[child]; !ok { queue = append(queue, child) } } } ids = ids[:0] for id := range visited { ids = append(ids, id) } } else if identity.Scope == 5 { ids, _ = r.DataScopeDepartmentIDs(ctx, authorityID) } identity.DepartmentIDs = ids return identity, nil }