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 из константных шаблонов)
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user