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,82 @@
|
||||
// Package source связывает пакет прав с хранилищем: реализует permissions.Source
|
||||
// поверх store, чтобы движок прав не зависел от деталей доступа к данным.
|
||||
package source
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"glchat/internal/permissions"
|
||||
"glchat/internal/store"
|
||||
)
|
||||
|
||||
// Adapter реализует permissions.Source.
|
||||
type Adapter struct {
|
||||
store *store.Store
|
||||
}
|
||||
|
||||
func New(st *store.Store) *Adapter { return &Adapter{store: st} }
|
||||
|
||||
var _ permissions.Source = (*Adapter)(nil)
|
||||
|
||||
func (a *Adapter) GuildRoles(ctx context.Context, guildID uint64) ([]permissions.RoleData, error) {
|
||||
roles, err := a.store.ListGuildRoles(ctx, guildID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]permissions.RoleData, 0, len(roles))
|
||||
for _, role := range roles {
|
||||
result = append(result, permissions.RoleData{
|
||||
ID: role.ID,
|
||||
Permissions: permissions.Permission(role.Permissions),
|
||||
IsDefault: role.IsDefault,
|
||||
Position: role.Position,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (a *Adapter) MemberRoleIDs(ctx context.Context, guildID, userID uint64) ([]uint64, error) {
|
||||
return a.store.MemberRoleIDs(ctx, guildID, userID)
|
||||
}
|
||||
|
||||
func (a *Adapter) ChannelOverrides(ctx context.Context, channelID uint64) ([]permissions.OverrideData, error) {
|
||||
overrides, err := a.store.ListChannelOverrides(ctx, channelID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]permissions.OverrideData, 0, len(overrides))
|
||||
for _, override := range overrides {
|
||||
result = append(result, permissions.OverrideData{
|
||||
TargetType: override.TargetType,
|
||||
TargetID: override.TargetID,
|
||||
Allow: permissions.Permission(override.Allow),
|
||||
Deny: permissions.Permission(override.Deny),
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (a *Adapter) GuildOwnerID(ctx context.Context, guildID uint64) (uint64, error) {
|
||||
guild, err := a.store.GetGuild(ctx, guildID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return guild.OwnerID, nil
|
||||
}
|
||||
|
||||
func (a *Adapter) MemberTimeout(ctx context.Context, guildID, userID uint64) (bool, error) {
|
||||
member, err := a.store.GetGuildMember(ctx, guildID, userID)
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
// Не участник сервера: тайм-аут не применяется, правами рулит VIEW_GUILD.
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
if member.TimeoutUntil == nil {
|
||||
return false, nil
|
||||
}
|
||||
return member.TimeoutUntil.After(time.Now().UTC()), nil
|
||||
}
|
||||
Reference in New Issue
Block a user