fix(roles): роли на себя выдавать можно, самомодерация запрещена
Владелец сервера или участник с MANAGE_ROLES не мог выдать роль себе: общий запрет самомодерации (кик, бан, тайм-аут) распространялся и на оформление. Проверка иерархии для ролей вынесена отдельно: себя менять можно, владельца и администратора инстанса — по-прежнему нет.
This commit is contained in:
@@ -957,7 +957,9 @@ func (s *Server) changeMemberRole(ctx context.Context, guildIDRaw, userIDRaw, ro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := s.checkMemberHierarchy(ctx, guildID, user, resolved, targetID); err != nil {
|
// Роли на себя выдавать можно (MANAGE_ROLES уже проверено): запрет
|
||||||
|
// самомодерации касается кика, бана и тайм-аута, а не оформления.
|
||||||
|
if err := s.checkRoleHierarchy(ctx, guildID, user, resolved, targetID); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
role, err := s.store.GetRole(ctx, roleID)
|
role, err := s.store.GetRole(ctx, roleID)
|
||||||
@@ -1006,6 +1008,40 @@ func (s *Server) changeMemberRole(ctx context.Context, guildIDRaw, userIDRaw, ro
|
|||||||
return newOKOutput(), nil
|
return newOKOutput(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkRoleHierarchy — проверка иерархии для выдачи и снятия ролей: в отличие
|
||||||
|
// от модерации, свои роли менять можно (владелец сервера не теряет владение),
|
||||||
|
// но владельца и администратора инстанса по-прежнему не трогаем.
|
||||||
|
func (s *Server) checkRoleHierarchy(ctx context.Context, guildID uint64, actor *store.User, resolved permissions.Resolved, targetID uint64) error {
|
||||||
|
if actor.IsInstanceAdmin {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guild, err := s.store.GetGuild(ctx, guildID)
|
||||||
|
if err != nil {
|
||||||
|
return humaError(err)
|
||||||
|
}
|
||||||
|
if targetID == guild.OwnerID && actor.ID != guild.OwnerID {
|
||||||
|
return humaErrorStatus(http.StatusForbidden, "perm.denied", "guild owner cannot be moderated")
|
||||||
|
}
|
||||||
|
if target, err := s.store.GetUser(ctx, targetID); err == nil && target.IsInstanceAdmin && actor.ID != targetID {
|
||||||
|
return humaErrorStatus(http.StatusForbidden, "instance.admin_protected", "instance administrator cannot be moderated")
|
||||||
|
}
|
||||||
|
if targetID == actor.ID {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
actorRoles, err := s.roleData(ctx, guildID, actor.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
targetRoles, err := s.roleData(ctx, guildID, targetID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !permissions.OutranksMember(actorRoles, targetRoles, resolved.IsOwner, targetID == guild.OwnerID) {
|
||||||
|
return humaErrorStatus(http.StatusForbidden, "perm.denied", "member has a higher role")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// checkMemberHierarchy запрещает действовать против владельца и участников с
|
// checkMemberHierarchy запрещает действовать против владельца и участников с
|
||||||
// более высокими ролями (AGENT.md 6.3, 7.17).
|
// более высокими ролями (AGENT.md 6.3, 7.17).
|
||||||
func (s *Server) checkMemberHierarchy(ctx context.Context, guildID uint64, actor *store.User, resolved permissions.Resolved, targetID uint64) error {
|
func (s *Server) checkMemberHierarchy(ctx context.Context, guildID uint64, actor *store.User, resolved permissions.Resolved, targetID uint64) error {
|
||||||
|
|||||||
@@ -302,3 +302,49 @@ func TestPersonalStyleAvailability(t *testing.T) {
|
|||||||
t.Fatalf("сброс стиля = %d", rec.Code)
|
t.Fatalf("сброс стиля = %d", rec.Code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSelfRoleAssignmentForCosmetics(t *testing.T) {
|
||||||
|
srv, owner, _, guildID, _ := cosmeticsFixture(t)
|
||||||
|
frameID, status := uploadCosmetic(t, srv, guildID, "frame", "Своя", owner)
|
||||||
|
if status != http.StatusOK {
|
||||||
|
t.Fatalf("загрузка рамки: статус %d", status)
|
||||||
|
}
|
||||||
|
// Свою роль выдать можно: иначе владелец не примерит оформление.
|
||||||
|
created := doJSON(t, srv, http.MethodPost, "/api/v1/guilds/"+guildID+"/roles",
|
||||||
|
`{"name":"Своя роль","cosmetic_frame_id":"`+frameID+`"}`, owner)
|
||||||
|
roleID := decodeResponse[struct {
|
||||||
|
Role struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"role"`
|
||||||
|
}](t, created).Role.ID
|
||||||
|
|
||||||
|
me := doJSON(t, srv, http.MethodGet, "/api/v1/users/@me", "", owner)
|
||||||
|
ownerID := decodeResponse[struct {
|
||||||
|
User struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"user"`
|
||||||
|
}](t, me).User.ID
|
||||||
|
|
||||||
|
granted := doJSON(t, srv, http.MethodPut,
|
||||||
|
"/api/v1/guilds/"+guildID+"/members/"+ownerID+"/roles/"+roleID, "", owner)
|
||||||
|
if granted.Code != http.StatusOK {
|
||||||
|
t.Fatalf("выдача роли себе = %d (%s)", granted.Code, granted.Body.String())
|
||||||
|
}
|
||||||
|
effective := memberCosmetics(t, srv, guildID, ownerID, owner)
|
||||||
|
if effective.FrameID != frameID {
|
||||||
|
t.Fatalf("рамка не применилась: %+v", effective)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Свою роль можно и снять (владелец остаётся владельцем).
|
||||||
|
revoked := doJSON(t, srv, http.MethodDelete,
|
||||||
|
"/api/v1/guilds/"+guildID+"/members/"+ownerID+"/roles/"+roleID, "", owner)
|
||||||
|
if revoked.Code != http.StatusOK {
|
||||||
|
t.Fatalf("снятие своей роли = %d (%s)", revoked.Code, revoked.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Самомодерация по-прежнему запрещена: кик себя отклоняется.
|
||||||
|
kick := doJSON(t, srv, http.MethodDelete, "/api/v1/guilds/"+guildID+"/members/"+ownerID, "", owner)
|
||||||
|
if kick.Code != http.StatusForbidden {
|
||||||
|
t.Fatalf("кик себя = %d, ожидался 403", kick.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user