提交分组成员管理权限和前端入口调整,作为继续开发前的安全检查点。
This commit is contained in:
parent
bd0ce77fa3
commit
b7a1e44dc6
@ -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
|
||||
}
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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},
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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('普通用户只能属于一个分组,请在分组管理中选择一个分组')
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user