# Conflicts: # account/Makefile # account/go.mod # docs/account-admin-settings.md # docs/account-svc-plus.md
197 lines
4.6 KiB
Go
197 lines
4.6 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"sync"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"account/internal/model"
|
|
)
|
|
|
|
// ErrServiceDBNotInitialized indicates the service database has not been configured.
|
|
var ErrServiceDBNotInitialized = errors.New("service db not initialized")
|
|
|
|
// ErrAdminSettingsVersionConflict is returned when the provided version does not match the stored version.
|
|
var ErrAdminSettingsVersionConflict = errors.New("admin settings version conflict")
|
|
|
|
// AdminSettings holds the permission matrix alongside its version.
|
|
type AdminSettings struct {
|
|
Version uint64
|
|
Matrix map[string]map[string]bool
|
|
}
|
|
|
|
var (
|
|
dbMu sync.RWMutex
|
|
db *gorm.DB
|
|
cache = &adminSettingsCache{}
|
|
)
|
|
|
|
// SetDB configures the backing database used by the admin settings service.
|
|
func SetDB(d *gorm.DB) {
|
|
dbMu.Lock()
|
|
defer dbMu.Unlock()
|
|
db = d
|
|
cache.invalidate()
|
|
}
|
|
|
|
// GetAdminSettings returns the persisted permission matrix and its current version.
|
|
func GetAdminSettings(ctx context.Context) (AdminSettings, error) {
|
|
if cached, ok := cache.get(); ok {
|
|
return cached, nil
|
|
}
|
|
|
|
database := currentDB()
|
|
if database == nil {
|
|
return AdminSettings{}, ErrServiceDBNotInitialized
|
|
}
|
|
|
|
var rows []model.AdminSetting
|
|
if err := database.WithContext(ctx).Order("module_key ASC, role ASC").Find(&rows).Error; err != nil {
|
|
return AdminSettings{}, err
|
|
}
|
|
|
|
matrix := make(map[string]map[string]bool)
|
|
var version uint64
|
|
for _, row := range rows {
|
|
module := row.ModuleKey
|
|
role := row.Role
|
|
if _, ok := matrix[module]; !ok {
|
|
matrix[module] = make(map[string]bool)
|
|
}
|
|
matrix[module][role] = row.Enabled
|
|
if row.Version > version {
|
|
version = row.Version
|
|
}
|
|
}
|
|
|
|
result := AdminSettings{Version: version, Matrix: matrix}
|
|
cache.set(result)
|
|
return result, nil
|
|
}
|
|
|
|
// SaveAdminSettings replaces the permission matrix if the provided version matches the stored version.
|
|
func SaveAdminSettings(ctx context.Context, payload AdminSettings) (AdminSettings, error) {
|
|
sanitized := cloneMatrix(payload.Matrix)
|
|
result := AdminSettings{Matrix: sanitized}
|
|
|
|
database := currentDB()
|
|
if database == nil {
|
|
return result, ErrServiceDBNotInitialized
|
|
}
|
|
|
|
err := database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
var currentVersion uint64
|
|
if err := tx.Model(&model.AdminSetting{}).Select("COALESCE(MAX(version), 0)").Scan(¤tVersion).Error; err != nil {
|
|
return err
|
|
}
|
|
if currentVersion != payload.Version {
|
|
result.Version = currentVersion
|
|
return ErrAdminSettingsVersionConflict
|
|
}
|
|
|
|
nextVersion := currentVersion + 1
|
|
|
|
if err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&model.AdminSetting{}).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(sanitized) > 0 {
|
|
rows := make([]model.AdminSetting, 0)
|
|
for module, roles := range sanitized {
|
|
module = strings.TrimSpace(module)
|
|
for role, enabled := range roles {
|
|
rows = append(rows, model.AdminSetting{
|
|
ModuleKey: module,
|
|
Role: role,
|
|
Enabled: enabled,
|
|
Version: nextVersion,
|
|
OriginNode: "local",
|
|
})
|
|
}
|
|
}
|
|
if len(rows) > 0 {
|
|
if err := tx.Create(&rows).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
result.Version = nextVersion
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, ErrAdminSettingsVersionConflict) {
|
|
cache.invalidate()
|
|
current, getErr := GetAdminSettings(ctx)
|
|
if getErr == nil {
|
|
return current, err
|
|
}
|
|
result.Version = 0
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
cache.set(result)
|
|
return result, nil
|
|
}
|
|
|
|
func cloneMatrix(in map[string]map[string]bool) map[string]map[string]bool {
|
|
if len(in) == 0 {
|
|
return make(map[string]map[string]bool)
|
|
}
|
|
out := make(map[string]map[string]bool, len(in))
|
|
for module, roles := range in {
|
|
if roles == nil {
|
|
out[module] = make(map[string]bool)
|
|
continue
|
|
}
|
|
inner := make(map[string]bool, len(roles))
|
|
for role, enabled := range roles {
|
|
inner[role] = enabled
|
|
}
|
|
out[module] = inner
|
|
}
|
|
return out
|
|
}
|
|
|
|
func currentDB() *gorm.DB {
|
|
dbMu.RLock()
|
|
defer dbMu.RUnlock()
|
|
return db
|
|
}
|
|
|
|
type adminSettingsCache struct {
|
|
mu sync.RWMutex
|
|
version uint64
|
|
matrix map[string]map[string]bool
|
|
loaded bool
|
|
}
|
|
|
|
func (c *adminSettingsCache) get() (AdminSettings, bool) {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
if !c.loaded {
|
|
return AdminSettings{}, false
|
|
}
|
|
return AdminSettings{Version: c.version, Matrix: cloneMatrix(c.matrix)}, true
|
|
}
|
|
|
|
func (c *adminSettingsCache) set(settings AdminSettings) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.version = settings.Version
|
|
c.matrix = cloneMatrix(settings.Matrix)
|
|
c.loaded = true
|
|
}
|
|
|
|
func (c *adminSettingsCache) invalidate() {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.loaded = false
|
|
c.version = 0
|
|
c.matrix = nil
|
|
}
|