Settings store

This commit is contained in:
Daniel Ponte 2025-02-23 12:35:25 -05:00
parent 640fbcaa6f
commit 051bae5dd2
8 changed files with 362 additions and 0 deletions

View file

@ -947,6 +947,53 @@ func (_c *Store_DeleteIncident_Call) RunAndReturn(run func(context.Context, uuid
return _c
}
// DeleteSetting provides a mock function with given fields: ctx, name
func (_m *Store) DeleteSetting(ctx context.Context, name string) error {
ret := _m.Called(ctx, name)
if len(ret) == 0 {
panic("no return value specified for DeleteSetting")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
r0 = rf(ctx, name)
} else {
r0 = ret.Error(0)
}
return r0
}
// Store_DeleteSetting_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteSetting'
type Store_DeleteSetting_Call struct {
*mock.Call
}
// DeleteSetting is a helper method to define mock.On call
// - ctx context.Context
// - name string
func (_e *Store_Expecter) DeleteSetting(ctx interface{}, name interface{}) *Store_DeleteSetting_Call {
return &Store_DeleteSetting_Call{Call: _e.mock.On("DeleteSetting", ctx, name)}
}
func (_c *Store_DeleteSetting_Call) Run(run func(ctx context.Context, name string)) *Store_DeleteSetting_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string))
})
return _c
}
func (_c *Store_DeleteSetting_Call) Return(_a0 error) *Store_DeleteSetting_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *Store_DeleteSetting_Call) RunAndReturn(run func(context.Context, string) error) *Store_DeleteSetting_Call {
_c.Call.Return(run)
return _c
}
// DeleteShare provides a mock function with given fields: ctx, id
func (_m *Store) DeleteShare(ctx context.Context, id string) error {
ret := _m.Called(ctx, id)
@ -1989,6 +2036,65 @@ func (_c *Store_GetIncidentTalkgroups_Call) RunAndReturn(run func(context.Contex
return _c
}
// GetSetting provides a mock function with given fields: ctx, name
func (_m *Store) GetSetting(ctx context.Context, name string) ([]byte, error) {
ret := _m.Called(ctx, name)
if len(ret) == 0 {
panic("no return value specified for GetSetting")
}
var r0 []byte
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) ([]byte, error)); ok {
return rf(ctx, name)
}
if rf, ok := ret.Get(0).(func(context.Context, string) []byte); ok {
r0 = rf(ctx, name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]byte)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Store_GetSetting_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSetting'
type Store_GetSetting_Call struct {
*mock.Call
}
// GetSetting is a helper method to define mock.On call
// - ctx context.Context
// - name string
func (_e *Store_Expecter) GetSetting(ctx interface{}, name interface{}) *Store_GetSetting_Call {
return &Store_GetSetting_Call{Call: _e.mock.On("GetSetting", ctx, name)}
}
func (_c *Store_GetSetting_Call) Run(run func(ctx context.Context, name string)) *Store_GetSetting_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string))
})
return _c
}
func (_c *Store_GetSetting_Call) Return(_a0 []byte, _a1 error) *Store_GetSetting_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *Store_GetSetting_Call) RunAndReturn(run func(context.Context, string) ([]byte, error)) *Store_GetSetting_Call {
_c.Call.Return(run)
return _c
}
// GetShare provides a mock function with given fields: ctx, id
func (_m *Store) GetShare(ctx context.Context, id string) (database.Share, error) {
ret := _m.Called(ctx, id)
@ -3804,6 +3910,55 @@ func (_c *Store_SetCallTranscript_Call) RunAndReturn(run func(context.Context, u
return _c
}
// SetSetting provides a mock function with given fields: ctx, name, updatedBy, value
func (_m *Store) SetSetting(ctx context.Context, name string, updatedBy *int32, value []byte) error {
ret := _m.Called(ctx, name, updatedBy, value)
if len(ret) == 0 {
panic("no return value specified for SetSetting")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, *int32, []byte) error); ok {
r0 = rf(ctx, name, updatedBy, value)
} else {
r0 = ret.Error(0)
}
return r0
}
// Store_SetSetting_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetSetting'
type Store_SetSetting_Call struct {
*mock.Call
}
// SetSetting is a helper method to define mock.On call
// - ctx context.Context
// - name string
// - updatedBy *int32
// - value []byte
func (_e *Store_Expecter) SetSetting(ctx interface{}, name interface{}, updatedBy interface{}, value interface{}) *Store_SetSetting_Call {
return &Store_SetSetting_Call{Call: _e.mock.On("SetSetting", ctx, name, updatedBy, value)}
}
func (_c *Store_SetSetting_Call) Run(run func(ctx context.Context, name string, updatedBy *int32, value []byte)) *Store_SetSetting_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(*int32), args[3].([]byte))
})
return _c
}
func (_c *Store_SetSetting_Call) Return(_a0 error) *Store_SetSetting_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *Store_SetSetting_Call) RunAndReturn(run func(context.Context, string, *int32, []byte) error) *Store_SetSetting_Call {
_c.Call.Return(run)
return _c
}
// SetTalkgroupTags provides a mock function with given fields: ctx, tags, systemID, tGID
func (_m *Store) SetTalkgroupTags(ctx context.Context, tags []string, systemID int32, tGID int32) error {
ret := _m.Called(ctx, tags, systemID, tGID)

View file

@ -26,6 +26,7 @@ type Querier interface {
DeleteAPIKey(ctx context.Context, apiKey string) error
DeleteCall(ctx context.Context, id uuid.UUID) error
DeleteIncident(ctx context.Context, id uuid.UUID) error
DeleteSetting(ctx context.Context, name string) error
DeleteShare(ctx context.Context, id string) error
DeleteSystem(ctx context.Context, id int) error
DeleteTalkgroup(ctx context.Context, systemID int32, tGID int32) error
@ -43,6 +44,7 @@ type Querier interface {
GetIncidentCalls(ctx context.Context, id uuid.UUID) ([]GetIncidentCallsRow, error)
GetIncidentOwner(ctx context.Context, id uuid.UUID) (int, error)
GetIncidentTalkgroups(ctx context.Context, incidentID uuid.UUID) ([]GetIncidentTalkgroupsRow, error)
GetSetting(ctx context.Context, name string) ([]byte, error)
GetShare(ctx context.Context, id string) (Share, error)
GetSharesP(ctx context.Context, arg GetSharesPParams) ([]GetSharesPRow, error)
GetSharesPCount(ctx context.Context, owner *int32) (int64, error)
@ -71,6 +73,7 @@ type Querier interface {
RestoreTalkgroupVersion(ctx context.Context, versionIds int) (Talkgroup, error)
SetAppPrefs(ctx context.Context, appName string, prefs []byte, uid int) error
SetCallTranscript(ctx context.Context, iD uuid.UUID, transcript *string) error
SetSetting(ctx context.Context, name string, updatedBy *int32, value []byte) error
SetTalkgroupTags(ctx context.Context, tags []string, systemID int32, tGID int32) error
StoreDeletedTGVersion(ctx context.Context, systemID *int32, tGID *int32, submitter *int32) error
StoreTGVersion(ctx context.Context, arg []StoreTGVersionParams) *StoreTGVersionBatchResults

View file

@ -0,0 +1,42 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// source: settings.sql
package database
import (
"context"
)
const deleteSetting = `-- name: DeleteSetting :exec
DELETE FROM settings WHERE name = $1
`
func (q *Queries) DeleteSetting(ctx context.Context, name string) error {
_, err := q.db.Exec(ctx, deleteSetting, name)
return err
}
const getSetting = `-- name: GetSetting :one
SELECT value FROM settings WHERE name = $1
`
func (q *Queries) GetSetting(ctx context.Context, name string) ([]byte, error) {
row := q.db.QueryRow(ctx, getSetting, name)
var value []byte
err := row.Scan(&value)
return value, err
}
const setSetting = `-- name: SetSetting :exec
INSERT INTO settings (name, updated_by, value) VALUES ($1, $2, $3)
ON CONFLICT (name) DO UPDATE SET
value = $3,
updated_by = $2
`
func (q *Queries) SetSetting(ctx context.Context, name string, updatedBy *int32, value []byte) error {
_, err := q.db.Exec(ctx, setSetting, name, updatedBy, value)
return err
}

View file

@ -23,6 +23,7 @@ const (
ResourceShare = "Share"
ResourceAPIKey = "APIKey"
ResourceCallStats = "CallStats"
ResourceSetting = "Setting"
ActionRead = "read"
ActionCreate = "create"
@ -97,3 +98,9 @@ func (s *SystemServiceSubject) String() string {
func (s *SystemServiceSubject) GetRoles() []string {
return []string{RoleSystem}
}
func IsSystemService(sub Subject) bool {
_, is := sub.(*SystemServiceSubject)
return is
}

View file

@ -50,6 +50,9 @@ var Policy = &restrict.PolicyDefinition{
entities.ResourceCallStats: {
&restrict.Permission{Action: entities.ActionRead},
},
entities.ResourceSetting: {
&restrict.Permission{Action: entities.ActionRead},
},
},
},
entities.RoleSubmitter: {
@ -108,6 +111,12 @@ var Policy = &restrict.PolicyDefinition{
&restrict.Permission{Action: entities.ActionCreate},
&restrict.Permission{Action: entities.ActionDelete},
},
entities.ResourceSetting: {
&restrict.Permission{Action: entities.ActionRead},
&restrict.Permission{Action: entities.ActionCreate},
&restrict.Permission{Action: entities.ActionUpdate},
&restrict.Permission{Action: entities.ActionDelete},
},
},
},
entities.RoleSystem: {

View file

@ -20,6 +20,7 @@ import (
"dynatron.me/x/stillbox/pkg/rbac/policy"
"dynatron.me/x/stillbox/pkg/rest"
"dynatron.me/x/stillbox/pkg/services"
"dynatron.me/x/stillbox/pkg/settings"
"dynatron.me/x/stillbox/pkg/shares"
"dynatron.me/x/stillbox/pkg/sinks"
"dynatron.me/x/stillbox/pkg/sources"
@ -57,6 +58,7 @@ type Server struct {
share shares.Service
rbac rbac.RBAC
stats stats.Stats
settings settings.Store
}
func New(ctx context.Context, cfg *config.Configuration) (*Server, error) {
@ -110,6 +112,7 @@ func New(ctx context.Context, cfg *config.Configuration) (*Server, error) {
incidents: incstore.NewStore(),
rbac: rbacSvc,
stats: statsSvc,
settings: settings.New(),
}
if cfg.DB.Partition.Enabled {
@ -176,6 +179,7 @@ func (s *Server) fillCtx(ctx context.Context) context.Context {
ctx = shares.CtxWithStore(ctx, s.share)
ctx = rbac.CtxWithRBAC(ctx, s.rbac)
ctx = stats.CtxWithStats(ctx, s.stats)
ctx = settings.CtxWithStore(ctx, s.settings)
return ctx
}

131
pkg/settings/settings.go Normal file
View file

@ -0,0 +1,131 @@
package settings
import (
"context"
"encoding/json"
"errors"
"dynatron.me/x/stillbox/internal/cache"
"dynatron.me/x/stillbox/pkg/database"
"dynatron.me/x/stillbox/pkg/rbac"
"dynatron.me/x/stillbox/pkg/rbac/entities"
"dynatron.me/x/stillbox/pkg/services"
"dynatron.me/x/stillbox/pkg/users"
)
var (
ErrNoSetting = errors.New("no such setting")
)
type Store interface {
// Get gets a setting and unmarshals it into dst.
Get(ctx context.Context, name string, dest interface{}) error
// Set sets a setting.
Set(ctx context.Context, name string, val interface{}) error
// Delete removes a setting.
Delete(ctx context.Context, name string) error
}
type postgresStore struct {
c cache.Cache[string, []byte]
}
type storeCtxKey string
const StoreCtxKey storeCtxKey = "store"
func CtxWithStore(ctx context.Context, s Store) context.Context {
return services.WithValue(ctx, StoreCtxKey, s)
}
func FromCtx(ctx context.Context) Store {
s, ok := services.Value(ctx, StoreCtxKey).(Store)
if !ok {
panic("no settings store in context")
}
return s
}
func New() *postgresStore {
s := &postgresStore{
c: cache.New[string, []byte](),
}
return s
}
func (s *postgresStore) Get(ctx context.Context, name string, dest interface{}) error {
_, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceSetting), rbac.WithActions(entities.ActionRead))
if err != nil {
return err
}
ci, has := s.c.Get(name)
if !has {
db := database.FromCtx(ctx)
ci, err = db.GetSetting(ctx, name)
if err != nil {
if database.IsNoRows(err) {
return ErrNoSetting
}
return err
}
s.c.Set(name, ci)
}
return json.Unmarshal(ci, dest)
}
func (s *postgresStore) Set(ctx context.Context, name string, val interface{}) error {
subj, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceSetting), rbac.WithActions(entities.ActionCreate, entities.ActionUpdate))
if err != nil {
return err
}
b, err := json.Marshal(val)
if err != nil {
return err
}
var uid *int32
switch u := subj.(type) {
case *entities.SystemServiceSubject:
// uid remains null
case *users.User:
u, err := users.FromSubject(subj)
if err != nil {
return err
}
uid = u.ID.Int32Ptr()
default:
return rbac.ErrBadSubject
}
db := database.FromCtx(ctx)
err = db.SetSetting(ctx, name, uid, b)
if err != nil {
return err
}
s.c.Set(name, b)
return nil
}
func (s *postgresStore) Delete(ctx context.Context, name string) error {
_, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceSetting), rbac.WithActions(entities.ActionDelete))
if err != nil {
return err
}
s.c.Delete(name)
return database.FromCtx(ctx).DeleteSetting(ctx, name)
}

View file

@ -0,0 +1,11 @@
-- name: GetSetting :one
SELECT value FROM settings WHERE name = @name;
-- name: SetSetting :exec
INSERT INTO settings (name, updated_by, value) VALUES (@name, @updated_by, @value)
ON CONFLICT (name) DO UPDATE SET
value = @value,
updated_by = @updated_by;
-- name: DeleteSetting :exec
DELETE FROM settings WHERE name = @name;