修复管理员成员分组权限检查点
Some checks are pending
Security Checks / security (push) Waiting to run

提交分组成员管理权限和前端入口调整,作为继续开发前的安全检查点。
This commit is contained in:
smail 2026-10-02 03:48:27 +08:00
parent bd0ce77fa3
commit b7a1e44dc6
7 changed files with 69 additions and 11 deletions

View File

@ -49,6 +49,14 @@ func requireAccountGroupManagement(ctx *context.Context, w http.ResponseWriter)
return true
}
func requireAccountGroupMembershipManagement(ctx *context.Context, w http.ResponseWriter) bool {
if err := authorizeCapability(ctx, access.CapabilityAccountGroupMemberManage, "", false); err != nil {
writeUserAuthorizationError(w, err, "无权管理账号所属分组")
return false
}
return true
}
func SaveAccountGroup(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
if !requireAccountGroupManagement(ctx, w) {
return
@ -91,7 +99,7 @@ func SaveAccountGroup(ctx *context.Context, w http.ResponseWriter, req *http.Req
}
func SetAccountGroupMembership(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
if !requireAccountGroupManagement(ctx, w) {
if !requireAccountGroupMembershipManagement(ctx, w) {
return
}
var input struct {
@ -117,7 +125,13 @@ func SetAccountGroupMembership(ctx *context.Context, w http.ResponseWriter, req
writeAccountGroupError(w, accountgroup.ErrConflict)
return
}
if err := accountgroup.SetMembership(db.Instance, user, input.GroupIDs, accountWriteGuard(ctx, user)); err != nil {
if err := accountgroup.SetMembership(
db.Instance,
user,
input.GroupIDs,
accountWriteGuard(ctx, user),
accountgroup.MembershipScopeGuard(ctx.UserID, ctx.EffectiveRole(), input.GroupIDs),
); err != nil {
writeAccountGroupError(w, err)
return
}

View File

@ -99,7 +99,7 @@ func CreateUser(ctx *context.Context, w http.ResponseWriter, req *http.Request)
groupIDs = *reqData.GroupIDs
}
if len(groupIDs) > 0 && (len(groupIDs) != 1 || groupIDs[0] != 0) {
if !requireAccountGroupManagement(ctx, w) {
if !requireAccountGroupMembershipManagement(ctx, w) {
return
}
}
@ -378,7 +378,7 @@ func EditUser(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
targetRole := user.EffectiveRole()
writeGuard := accountWriteGuard(ctx, user)
if reqData.GroupIDs != nil {
if !requireAccountGroupManagement(ctx, w) {
if !requireAccountGroupMembershipManagement(ctx, w) {
return
}
if reqData.MembershipVersion < 1 || reqData.MembershipVersion != user.SecurityVersion {
@ -492,6 +492,13 @@ func EditUser(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
writeAccountGroupError(w, err)
return
}
if reqData.GroupIDs != nil {
if err := accountgroup.CheckWrite(tx, accountgroup.MembershipScopeGuard(ctx.UserID, ctx.EffectiveRole(), *reqData.GroupIDs)); err != nil {
_ = tx.Rollback()
writeAccountGroupError(w, err)
return
}
}
if reqData.Password != "" {
updateColumns = append(updateColumns, "password", "must_change_password", "password_changed_at")
}

View File

@ -28,6 +28,7 @@ const (
CapabilityNotificationManage Capability = "notification.manage"
CapabilityNotificationReveal Capability = "notification.reveal"
CapabilityAccountGroupManage Capability = "account_group.manage"
CapabilityAccountGroupMemberManage Capability = "account_group.member_manage"
)
type Policy struct {
@ -41,6 +42,7 @@ type Policy struct {
var policies = map[Capability]Policy{
CapabilityNotificationReveal: {Capability: CapabilityNotificationReveal, HighRisk: true, RequiresTarget: true},
CapabilityAccountGroupManage: {Capability: CapabilityAccountGroupManage, HighRisk: true, RequiresRecentAuth: true},
CapabilityAccountGroupMemberManage: {Capability: CapabilityAccountGroupMemberManage, HighRisk: true, RequiresRecentAuth: true},
CapabilityNotificationManage: {Capability: CapabilityNotificationManage, HighRisk: true, RequiresRecentAuth: true, RequiresRecoveryCode: true, RequiresTarget: true},
CapabilityConfigRead: {Capability: CapabilityConfigRead},
CapabilityConfigWriteGlobal: {Capability: CapabilityConfigWriteGlobal, HighRisk: true, RequiresRecentAuth: true, RequiresRecoveryCode: true},
@ -70,6 +72,7 @@ var grants = map[Role]map[Capability]struct{}{
RoleSuperAdmin: grant(
CapabilityNotificationReveal,
CapabilityAccountGroupManage,
CapabilityAccountGroupMemberManage,
CapabilityNotificationManage,
CapabilityPluginReadGlobal, CapabilityUserCreate, CapabilityProfileWriteOwn,
CapabilityConfigRead, CapabilityConfigWriteGlobal, CapabilityConfigRollback,
@ -81,6 +84,7 @@ var grants = map[Role]map[Capability]struct{}{
),
RoleAdmin: grant(
CapabilityNotificationReveal,
CapabilityAccountGroupMemberManage,
CapabilityNotificationManage,
CapabilityPluginReadGlobal, CapabilityPluginRead, CapabilityPluginWriteGlobal, CapabilityProfileWriteOwn,
CapabilityPluginWriteAnyUser, CapabilityPluginWriteOwn, CapabilityUserRead,

View File

@ -14,9 +14,9 @@ var managementEndpoints = []Endpoint{
{Method: "POST", Path: "/api/system/notification/authorization", Capability: CapabilityNotificationManage},
{Method: "POST", Path: "/api/system/notification/credential", Capability: CapabilityNotificationManage},
{Method: "POST", Path: "/api/v1/user/groups/save", Capability: CapabilityAccountGroupManage},
{Method: "POST", Path: "/api/v1/user/groups/membership", Capability: CapabilityAccountGroupManage},
{Method: "POST", Path: "/api/v1/user/groups/membership", Capability: CapabilityAccountGroupMemberManage},
{Method: "POST", Path: "/api/user/groups/save", Capability: CapabilityAccountGroupManage},
{Method: "POST", Path: "/api/user/groups/membership", Capability: CapabilityAccountGroupManage},
{Method: "POST", Path: "/api/user/groups/membership", Capability: CapabilityAccountGroupMemberManage},
{Method: "POST", Path: "/api/v1/system/notification/authorization", Capability: CapabilityNotificationManage},
{Method: "POST", Path: "/api/v1/system/notification/credential", Capability: CapabilityNotificationManage},
{Method: "POST", Path: "/api/settings/modify_password", Capability: CapabilityPasswordChangeOwn},

View File

@ -170,6 +170,38 @@ func ValidateMembership(tx *xorm.Session, role access.Role, input []int) ([]int,
return ids, nil
}
// MembershipScopeGuard 限制管理员只能把成员放入自己所属的分组。
func MembershipScopeGuard(actorID int, role access.Role, input []int) WriteGuard {
return func(tx *xorm.Session) error {
if tx == nil || actorID <= 0 || !role.Valid() {
return ErrDenied
}
if role == access.RoleSuperAdmin {
return nil
}
if role != access.RoleAdmin {
return ErrDenied
}
allowedRows := make([]models.AccountGroupMember, 0)
if err := tx.Where("user_id = ?", actorID).Find(&allowedRows); err != nil {
return err
}
allowed := make(map[int]struct{}, len(allowedRows))
for _, row := range allowedRows {
allowed[row.GroupID] = struct{}{}
}
if len(allowed) == 0 {
allowed[0] = struct{}{}
}
for _, groupID := range input {
if _, ok := allowed[groupID]; !ok {
return ErrDenied
}
}
return nil
}
}
func ReplaceMembership(tx *xorm.Session, userID int, ids []int) error {
if _, err := tx.Where("user_id = ?", userID).Delete(&models.AccountGroupMember{}); err != nil {
return err

View File

@ -8,6 +8,7 @@ export function useAccountGroups(globalStatus, onMembershipSaved = () => {}) {
const groupsLoading = ref(false)
const groupsError = ref('')
const canManageGroups = computed(() => globalStatus.hasCapability('account_group.manage'))
const canManageMembership = computed(() => globalStatus.hasCapability('account_group.member_manage'))
const groupSaving = ref(false)
const membershipVisible = ref(false)
const membershipUser = ref(null)
@ -128,7 +129,7 @@ export function useAccountGroups(globalStatus, onMembershipSaved = () => {}) {
}
const editMembership = (user, options = {}) => {
if (!canManageGroups.value || groupSaving.value) return
if (!canManageMembership.value || groupSaving.value) return
membershipUser.value = { ...user, Role: options.role || user.Role }
membershipSelection.value = [...(options.groupIDs || user.GroupIDs || [0])]
applyMembershipDraft = options.onApply || null
@ -137,7 +138,7 @@ export function useAccountGroups(globalStatus, onMembershipSaved = () => {}) {
}
const saveMembership = async () => {
if (!canManageGroups.value || groupSaving.value || !membershipUser.value) return
if (!canManageMembership.value || groupSaving.value || !membershipUser.value) return
if (membershipSelection.value.length === 0) membershipSelection.value = [0]
if (membershipSelection.value.length > membershipLimit.value) {
ElMessage.error('普通用户只能选择一个分组,请重新选择')
@ -174,7 +175,7 @@ export function useAccountGroups(globalStatus, onMembershipSaved = () => {}) {
}
return {
accountGroups, groupsLoading, groupsError, canManageGroups, groupSaving,
accountGroups, groupsLoading, groupsError, canManageGroups, canManageMembership, groupSaving,
membershipVisible, membershipUser, membershipSelection, membershipLimit,
membershipDraft, membershipSingleSelection,
refreshGroups, loadGroup, toggleGroup, saveGroup, createMembershipGroup, editMembership, saveMembership

View File

@ -69,7 +69,7 @@ export function usePage() {
const user = selectedUser.value
if (!user && editModel.value !== 'create') return []
return groups.accountGroups.value.filter(group =>
groups.canManageGroups.value
groups.canManageMembership.value
? editingGroupIDs.value.includes(group.id)
: group.rows.some(member => member.ID === user.ID)
)
@ -243,7 +243,7 @@ export function usePage() {
}
if (creating || roleChanged.value) newData.role = editUserInfo.role
if (creating || editUserInfo.password !== '') newData.password = editUserInfo.password
if (groups.canManageGroups.value && (creating || membershipChanged.value || roleChanged.value)) {
if (groups.canManageMembership.value && (creating || membershipChanged.value || roleChanged.value)) {
if (editUserInfo.role !== 'admin' && editingGroupIDs.value.length !== 1) {
throw new Error('普通用户只能属于一个分组,请在分组管理中选择一个分组')
}