Files
glchat/internal/permissions/channel.go
T
grendervill a5205cc054 feat(store,permissions): схема Фазы 1, доступ к данным и движок прав
- миграция 00002: users, sessions, totp_secrets, webauthn_credentials,
  security_events, guilds, guild_members, roles, member_roles, channels,
  channel_overrides, audit_log + дефолтные instance_settings (AGENT.md 6.1)
- internal/store: Snowflake-идентификаторы, CRUD пользователей и сессий
  (ротация, step-up, logout-all), TOTP и события безопасности, серверы,
  участники, роли, комнаты и оверрайды, настройки инстанса и аудит
- internal/permissions: 37 прав битмаской, вычисление по правилам §6.2
  (баз role @user → оверрайды ролей → оверрайд пользователя → ADMINISTRATOR),
  иерархия ролей и участников, тайм-ауты, обход для инстанс-админа, LRU-кэш
  с инвалидацией
- internal/source: адаптер permissions.Source поверх store
- тесты: 18 unit-тестов прав + интеграционный набор на реальной SQLite
  (приватная комната, модератор, владелец, инстанс-админ, тайм-аут)
- golangci: обоснованное исключение gosec для пакета store (конверсии
  Snowflake и сборка SQL из константных шаблонов)
2026-09-19 21:24:17 +03:00

79 lines
2.5 KiB
Go

package permissions
import (
"context"
"fmt"
)
// Channel вычисляет права пользователя в конкретной комнате: базовые права
// сервера, затем оверрайды ролей, затем оверрайд пользователя (AGENT.md 6.2).
func (c *Calculator) Channel(ctx context.Context, guildID, channelID, userID uint64, instanceAdmin bool) (Resolved, error) {
if cached, ok := c.cache.GetChannel(channelID, userID); ok {
cached.IsInstanceAdmin = instanceAdmin
return cached, nil
}
resolved, err := c.Guild(ctx, guildID, userID, instanceAdmin)
if err != nil {
return Resolved{}, err
}
resolved.Channel = resolved.Guild
if resolved.IsInstanceAdmin || resolved.Guild.Has(Administrator) {
c.cache.PutChannel(channelID, userID, resolved)
return resolved, nil
}
overrides, err := c.source.ChannelOverrides(ctx, channelID)
if err != nil {
return Resolved{}, fmt.Errorf("load channel overrides: %w", err)
}
roles, err := c.source.GuildRoles(ctx, guildID)
if err != nil {
return Resolved{}, fmt.Errorf("load guild roles: %w", err)
}
memberRoleIDs, err := c.source.MemberRoleIDs(ctx, guildID, userID)
if err != nil {
return Resolved{}, fmt.Errorf("load member roles: %w", err)
}
assigned := make(map[uint64]bool, len(memberRoleIDs)+1)
for _, roleID := range memberRoleIDs {
assigned[roleID] = true
}
defaultRoleID := uint64(0)
for _, role := range roles {
if role.IsDefault {
defaultRoleID = role.ID
assigned[role.ID] = true
break
}
}
// Порядок как в AGENT.md 6.2: базовые права роли @user → оверрайды ролей →
// персональный оверрайд пользователя.
denied, allowed := Permission(0), Permission(0)
for _, override := range overrides {
if override.TargetType != "role" {
continue
}
if override.TargetID == defaultRoleID || assigned[override.TargetID] {
denied |= override.Deny
allowed |= override.Allow
}
}
computed := (resolved.Guild &^ denied) | allowed
// Персональный deny обязан перебить ролевой allow, поэтому применяется
// к уже вычисленному результату.
var userDeny, userAllow Permission
for _, override := range overrides {
if override.TargetType == "user" && override.TargetID == userID {
userDeny |= override.Deny
userAllow |= override.Allow
}
}
resolved.Channel = (computed &^ userDeny) | userAllow
c.cache.PutChannel(channelID, userID, resolved)
return resolved, nil
}