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,149 @@
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Cache — простой LRU-кэш вычисленных прав с инвалидацией при записи
|
||||
// (AGENT.md 9.1). Ключи: сервер+пользователь и комната+пользователь.
|
||||
type Cache struct {
|
||||
mu sync.Mutex
|
||||
capacity int
|
||||
items map[guildKey]*list.Element
|
||||
channels map[channelKey]*list.Element
|
||||
order *list.List
|
||||
}
|
||||
|
||||
type guildKey struct {
|
||||
guildID uint64
|
||||
userID uint64
|
||||
}
|
||||
|
||||
type channelKey struct {
|
||||
channelID uint64
|
||||
userID uint64
|
||||
}
|
||||
|
||||
type cacheEntry struct {
|
||||
guild guildKey
|
||||
channel channelKey
|
||||
value Resolved
|
||||
isChan bool
|
||||
}
|
||||
|
||||
func NewCache(capacity int) *Cache {
|
||||
if capacity <= 0 {
|
||||
capacity = 1024
|
||||
}
|
||||
return &Cache{
|
||||
capacity: capacity,
|
||||
items: make(map[guildKey]*list.Element, capacity),
|
||||
channels: make(map[channelKey]*list.Element, capacity),
|
||||
order: list.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) GetGuild(guildID, userID uint64) (Resolved, bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
element, ok := c.items[guildKey{guildID: guildID, userID: userID}]
|
||||
if !ok {
|
||||
return Resolved{}, false
|
||||
}
|
||||
c.order.MoveToFront(element)
|
||||
return element.Value.(*cacheEntry).value, true
|
||||
}
|
||||
|
||||
func (c *Cache) PutGuild(guildID, userID uint64, value Resolved) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
key := guildKey{guildID: guildID, userID: userID}
|
||||
if element, ok := c.items[key]; ok {
|
||||
element.Value.(*cacheEntry).value = value
|
||||
c.order.MoveToFront(element)
|
||||
return
|
||||
}
|
||||
entry := &cacheEntry{guild: key, value: value}
|
||||
c.items[key] = c.order.PushFront(entry)
|
||||
c.evictLocked()
|
||||
}
|
||||
|
||||
func (c *Cache) GetChannel(channelID, userID uint64) (Resolved, bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
element, ok := c.channels[channelKey{channelID: channelID, userID: userID}]
|
||||
if !ok {
|
||||
return Resolved{}, false
|
||||
}
|
||||
c.order.MoveToFront(element)
|
||||
return element.Value.(*cacheEntry).value, true
|
||||
}
|
||||
|
||||
func (c *Cache) PutChannel(channelID, userID uint64, value Resolved) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
key := channelKey{channelID: channelID, userID: userID}
|
||||
if element, ok := c.channels[key]; ok {
|
||||
element.Value.(*cacheEntry).value = value
|
||||
c.order.MoveToFront(element)
|
||||
return
|
||||
}
|
||||
entry := &cacheEntry{channel: key, value: value, isChan: true}
|
||||
c.channels[key] = c.order.PushFront(entry)
|
||||
c.evictLocked()
|
||||
}
|
||||
|
||||
// InvalidateGuild сбрасывает права всех пользователей сервера.
|
||||
func (c *Cache) InvalidateGuild(guildID uint64) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
for key, element := range c.items {
|
||||
if key.guildID == guildID {
|
||||
c.order.Remove(element)
|
||||
delete(c.items, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// InvalidateChannel сбрасывает кэш комнаты.
|
||||
func (c *Cache) InvalidateChannel(channelID uint64) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
for key, element := range c.channels {
|
||||
if key.channelID == channelID {
|
||||
c.order.Remove(element)
|
||||
delete(c.channels, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) Clear() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.items = make(map[guildKey]*list.Element, c.capacity)
|
||||
c.channels = make(map[channelKey]*list.Element, c.capacity)
|
||||
c.order.Init()
|
||||
}
|
||||
|
||||
func (c *Cache) Len() int {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.order.Len()
|
||||
}
|
||||
|
||||
func (c *Cache) evictLocked() {
|
||||
for c.order.Len() > c.capacity {
|
||||
oldest := c.order.Back()
|
||||
if oldest == nil {
|
||||
return
|
||||
}
|
||||
entry := oldest.Value.(*cacheEntry)
|
||||
if entry.isChan {
|
||||
delete(c.channels, entry.channel)
|
||||
} else {
|
||||
delete(c.items, entry.guild)
|
||||
}
|
||||
c.order.Remove(oldest)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user