refactor(authz): split role into domain Role and StorableRole

Replace the Scan/Value/MarshalJSON codecs on TransactionGroups with the
storable pattern: StorableRole is the bun model carrying transaction groups
as raw JSON text, Role is the pure domain/wire type, and
NewStorableRoleFromRole/NewRoleFromStorableRole convert at the store
boundary (nil groups persist as [], reads parse through the validating
constructor). RoleStore and sqlauthzstore speak StorableRole; both
providers convert; handlers and the wire contract are unchanged.
This commit is contained in:
vikrantgupta25
2026-07-08 17:27:43 +05:30
parent 8cbdf311af
commit 73aa7d32b1
6 changed files with 142 additions and 151 deletions

View File

@@ -200,11 +200,21 @@ func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *a
return err
}
return provider.store.Create(ctx, role)
storableRole, err := authtypes.NewStorableRoleFromRole(role)
if err != nil {
return err
}
return provider.store.Create(ctx, storableRole)
}
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.Role, error) {
return provider.store.Get(ctx, orgID, id)
storableRole, err := provider.store.Get(ctx, orgID, id)
if err != nil {
return nil, err
}
return authtypes.NewRoleFromStorableRole(storableRole)
}
func (provider *provider) Update(ctx context.Context, orgID valuer.UUID, updatedRole *authtypes.Role) error {
@@ -240,7 +250,12 @@ func (provider *provider) Update(ctx context.Context, orgID valuer.UUID, updated
return err
}
return provider.store.Update(ctx, orgID, updatedRole)
storableRole, err := authtypes.NewStorableRoleFromRole(updatedRole)
if err != nil {
return err
}
return provider.store.Update(ctx, orgID, storableRole)
}
func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {

View File

@@ -18,22 +18,22 @@ func NewSqlAuthzStore(sqlstore sqlstore.SQLStore) authtypes.RoleStore {
return &store{sqlstore: sqlstore}
}
func (store *store) Create(ctx context.Context, role *authtypes.Role) error {
func (store *store) Create(ctx context.Context, storableRole *authtypes.StorableRole) error {
_, err := store.
sqlstore.
BunDBCtx(ctx).
NewInsert().
Model(role).
Model(storableRole).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "role with name: %s already exists", role.Name)
return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "role with name: %s already exists", storableRole.Name)
}
return nil
}
func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.Role, error) {
role := new(authtypes.Role)
func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.StorableRole, error) {
role := new(authtypes.StorableRole)
err := store.
sqlstore.
BunDBCtx(ctx).
@@ -49,8 +49,8 @@ func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID)
return role, nil
}
func (store *store) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*authtypes.Role, error) {
role := new(authtypes.Role)
func (store *store) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*authtypes.StorableRole, error) {
role := new(authtypes.StorableRole)
err := store.
sqlstore.
BunDBCtx(ctx).
@@ -66,8 +66,8 @@ func (store *store) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, na
return role, nil
}
func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*authtypes.Role, error) {
roles := make([]*authtypes.Role, 0)
func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*authtypes.StorableRole, error) {
roles := make([]*authtypes.StorableRole, 0)
err := store.
sqlstore.
BunDBCtx(ctx).
@@ -82,8 +82,8 @@ func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*authtypes.R
return roles, nil
}
func (store *store) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*authtypes.Role, error) {
roles := make([]*authtypes.Role, 0)
func (store *store) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*authtypes.StorableRole, error) {
roles := make([]*authtypes.StorableRole, 0)
err := store.
sqlstore.
BunDBCtx(ctx).
@@ -103,8 +103,8 @@ func (store *store) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID,
return roles, nil
}
func (store *store) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*authtypes.Role, error) {
roles := make([]*authtypes.Role, 0)
func (store *store) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*authtypes.StorableRole, error) {
roles := make([]*authtypes.StorableRole, 0)
err := store.
sqlstore.
BunDBCtx(ctx).
@@ -124,17 +124,17 @@ func (store *store) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, id
return roles, nil
}
func (store *store) Update(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) error {
func (store *store) Update(ctx context.Context, orgID valuer.UUID, storableRole *authtypes.StorableRole) error {
_, err := store.
sqlstore.
BunDBCtx(ctx).
NewUpdate().
Model(role).
Model(storableRole).
WherePK().
Where("org_id = ?", orgID).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapNotFoundErrf(err, errors.CodeAlreadyExists, "role with id %s doesn't exist", role.ID)
return store.sqlstore.WrapNotFoundErrf(err, errors.CodeAlreadyExists, "role with id %s doesn't exist", storableRole.ID)
}
return nil
@@ -145,7 +145,7 @@ func (store *store) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUI
sqlstore.
BunDBCtx(ctx).
NewDelete().
Model(new(authtypes.Role)).
Model(new(authtypes.StorableRole)).
Where("org_id = ?", orgID).
Where("id = ?", id).
Exec(ctx)

View File

@@ -84,11 +84,21 @@ func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.
}
func (provider *provider) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*authtypes.Role, error) {
return provider.store.GetByOrgIDAndName(ctx, orgID, name)
storableRole, err := provider.store.GetByOrgIDAndName(ctx, orgID, name)
if err != nil {
return nil, err
}
return authtypes.NewRoleFromStorableRole(storableRole)
}
func (provider *provider) List(ctx context.Context, orgID valuer.UUID) ([]*authtypes.Role, error) {
return provider.store.List(ctx, orgID)
storableRoles, err := provider.store.List(ctx, orgID)
if err != nil {
return nil, err
}
return authtypes.NewRolesFromStorableRoles(storableRoles)
}
func (provider *provider) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
@@ -101,11 +111,21 @@ func (provider *provider) Collect(ctx context.Context, orgID valuer.UUID) (map[s
}
func (provider *provider) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*authtypes.Role, error) {
return provider.store.ListByOrgIDAndNames(ctx, orgID, names)
storableRoles, err := provider.store.ListByOrgIDAndNames(ctx, orgID, names)
if err != nil {
return nil, err
}
return authtypes.NewRolesFromStorableRoles(storableRoles)
}
func (provider *provider) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*authtypes.Role, error) {
return provider.store.ListByOrgIDAndIDs(ctx, orgID, ids)
storableRoles, err := provider.store.ListByOrgIDAndIDs(ctx, orgID, ids)
if err != nil {
return nil, err
}
return authtypes.NewRolesFromStorableRoles(storableRoles)
}
func (provider *provider) Grant(ctx context.Context, orgID valuer.UUID, names []string, subject string) error {
@@ -157,10 +177,14 @@ func (provider *provider) Revoke(ctx context.Context, orgID valuer.UUID, names [
func (provider *provider) CreateManagedRoles(ctx context.Context, _ valuer.UUID, managedRoles []*authtypes.Role) error {
err := provider.store.RunInTx(ctx, func(ctx context.Context) error {
for _, role := range managedRoles {
err := provider.store.Create(ctx, role)
storableRole, err := authtypes.NewStorableRoleFromRole(role)
if err != nil {
return err
}
if err := provider.store.Create(ctx, storableRole); err != nil {
return err
}
}
return nil

View File

@@ -62,15 +62,25 @@ var (
)
type Role struct {
types.Identifiable
types.TimeAuditable
Name string `json:"name" required:"true"`
Description string `json:"description" required:"true"`
Type valuer.String `json:"type" required:"true"`
OrgID valuer.UUID `json:"orgId" required:"true"`
TransactionGroups TransactionGroups `json:"transactionGroups" required:"true" nullable:"false"`
}
type StorableRole struct {
bun.BaseModel `bun:"table:role"`
types.Identifiable
types.TimeAuditable
Name string `bun:"name,type:string" json:"name" required:"true"`
Description string `bun:"description,type:string" json:"description" required:"true"`
Type valuer.String `bun:"type,type:string" json:"type" required:"true"`
OrgID valuer.UUID `bun:"org_id,type:string" json:"orgId" required:"true"`
TransactionGroups TransactionGroups `bun:"transaction_groups,type:text" json:"transactionGroups" required:"true" nullable:"false"`
Name string `bun:"name,type:string"`
Description string `bun:"description,type:string"`
Type valuer.String `bun:"type,type:string"`
OrgID valuer.UUID `bun:"org_id,type:string"`
TransactionGroups string `bun:"transaction_groups,type:text,nullzero"`
}
type GettableRole struct {
@@ -110,6 +120,62 @@ func NewRole(name, description string, roleType valuer.String, orgID valuer.UUID
}
}
func NewStorableRoleFromRole(role *Role) (*StorableRole, error) {
transactionGroups := role.TransactionGroups
if transactionGroups == nil {
transactionGroups = make(TransactionGroups, 0)
}
data, err := json.Marshal(transactionGroups)
if err != nil {
return nil, err
}
return &StorableRole{
Identifiable: role.Identifiable,
TimeAuditable: role.TimeAuditable,
Name: role.Name,
Description: role.Description,
Type: role.Type,
OrgID: role.OrgID,
TransactionGroups: string(data),
}, nil
}
func NewRoleFromStorableRole(storableRole *StorableRole) (*Role, error) {
transactionGroups := make(TransactionGroups, 0)
if storableRole.TransactionGroups != "" {
var err error
transactionGroups, err = NewTransactionGroups([]byte(storableRole.TransactionGroups))
if err != nil {
return nil, err
}
}
return &Role{
Identifiable: storableRole.Identifiable,
TimeAuditable: storableRole.TimeAuditable,
Name: storableRole.Name,
Description: storableRole.Description,
Type: storableRole.Type,
OrgID: storableRole.OrgID,
TransactionGroups: transactionGroups,
}, nil
}
func NewRolesFromStorableRoles(storableRoles []*StorableRole) ([]*Role, error) {
roles := make([]*Role, len(storableRoles))
for index, storableRole := range storableRoles {
role, err := NewRoleFromStorableRole(storableRole)
if err != nil {
return nil, err
}
roles[index] = role
}
return roles, nil
}
func NewGettableRolesFromRoles(roles []*Role) []*GettableRole {
gettableRoles := make([]*GettableRole, len(roles))
for index, role := range roles {
@@ -259,13 +325,13 @@ func NormalizeRoleName(role string) string {
}
type RoleStore interface {
Create(context.Context, *Role) error
Get(context.Context, valuer.UUID, valuer.UUID) (*Role, error)
GetByOrgIDAndName(context.Context, valuer.UUID, string) (*Role, error)
List(context.Context, valuer.UUID) ([]*Role, error)
ListByOrgIDAndNames(context.Context, valuer.UUID, []string) ([]*Role, error)
ListByOrgIDAndIDs(context.Context, valuer.UUID, []valuer.UUID) ([]*Role, error)
Update(context.Context, valuer.UUID, *Role) error
Create(context.Context, *StorableRole) error
Get(context.Context, valuer.UUID, valuer.UUID) (*StorableRole, error)
GetByOrgIDAndName(context.Context, valuer.UUID, string) (*StorableRole, error)
List(context.Context, valuer.UUID) ([]*StorableRole, error)
ListByOrgIDAndNames(context.Context, valuer.UUID, []string) ([]*StorableRole, error)
ListByOrgIDAndIDs(context.Context, valuer.UUID, []valuer.UUID) ([]*StorableRole, error)
Update(context.Context, valuer.UUID, *StorableRole) error
Delete(context.Context, valuer.UUID, valuer.UUID) error
RunInTx(context.Context, func(ctx context.Context) error) error
}

View File

@@ -1,7 +1,6 @@
package authtypes
import (
"database/sql/driver"
"encoding/json"
"github.com/SigNoz/signoz/pkg/errors"
@@ -110,48 +109,6 @@ func (groups TransactionGroups) Diff(desired TransactionGroups) (additions, dele
return desired.subtract(groups), groups.subtract(desired)
}
func (groups TransactionGroups) MarshalJSON() ([]byte, error) {
if groups == nil {
return []byte("[]"), nil
}
return json.Marshal([]*TransactionGroup(groups))
}
func (groups TransactionGroups) Value() (driver.Value, error) {
data, err := json.Marshal(groups)
if err != nil {
return nil, err
}
return string(data), nil
}
func (groups *TransactionGroups) Scan(value any) error {
if value == nil {
*groups = nil
return nil
}
var data []byte
switch typed := value.(type) {
case string:
data = []byte(typed)
case []byte:
data = typed
default:
return errors.Newf(errors.TypeInternal, errors.CodeInternal, "unsupported type %T for transaction groups", value)
}
parsed, err := NewTransactionGroups(data)
if err != nil {
return err
}
*groups = parsed
return nil
}
func (transaction *Transaction) UnmarshalJSON(data []byte) error {
var shadow = struct {
Relation Relation

View File

@@ -1,71 +0,0 @@
package authtypes
import (
"testing"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTransactionGroupsValueScanRoundTrip(t *testing.T) {
input := []byte(`[
{"relation":"read","objectGroup":{"resource":{"type":"metaresource","kind":"dashboard"},"selectors":["*"]}},
{"relation":"update","objectGroup":{"resource":{"type":"metaresource","kind":"dashboard"},"selectors":["019823cf-c360-7d0a-9a3f-a37656161ff9"]}}
]`)
groups, err := NewTransactionGroups(input)
require.NoError(t, err)
value, err := groups.Value()
require.NoError(t, err)
var scanned TransactionGroups
require.NoError(t, scanned.Scan(value))
assert.Equal(t, groups, scanned)
}
func TestTransactionGroupsValueNil(t *testing.T) {
var groups TransactionGroups
value, err := groups.Value()
require.NoError(t, err)
assert.Equal(t, "[]", value)
}
func TestTransactionGroupsScan(t *testing.T) {
var groups TransactionGroups
require.NoError(t, groups.Scan(nil))
assert.Nil(t, groups)
require.NoError(t, groups.Scan([]byte(`[]`)))
assert.Empty(t, groups)
assert.Error(t, groups.Scan("not json"))
assert.Error(t, groups.Scan(42))
assert.Error(t, groups.Scan(`[{"relation":"fly","objectGroup":{"resource":{"type":"metaresource","kind":"dashboard"},"selectors":["*"]}}]`))
}
func TestNewTransactionGroupsFromTransactions(t *testing.T) {
dashboard := coretypes.ResourceRef{Type: coretypes.TypeMetaResource, Kind: coretypes.KindDashboard}
rule := coretypes.ResourceRef{Type: coretypes.TypeMetaResource, Kind: coretypes.KindRule}
transactions := []coretypes.Transaction{
{Verb: coretypes.VerbUpdate, Object: *coretypes.MustNewObject(dashboard, coretypes.WildCardSelectorString)},
{Verb: coretypes.VerbRead, Object: *coretypes.MustNewObject(dashboard, coretypes.WildCardSelectorString)},
{Verb: coretypes.VerbRead, Object: *coretypes.MustNewObject(rule, coretypes.WildCardSelectorString)},
}
groups := NewTransactionGroupsFromTransactions(transactions)
require.Len(t, groups, 3)
for _, group := range groups {
require.Len(t, group.ObjectGroup.Selectors, 1)
assert.Equal(t, coretypes.WildCardSelectorString, group.ObjectGroup.Selectors[0].String())
}
assert.Equal(t, coretypes.VerbRead, groups[0].Relation.Verb)
assert.Equal(t, dashboard, groups[0].ObjectGroup.Resource)
assert.Equal(t, coretypes.VerbRead, groups[1].Relation.Verb)
assert.Equal(t, rule, groups[1].ObjectGroup.Resource)
assert.Equal(t, coretypes.VerbUpdate, groups[2].Relation.Verb)
assert.Equal(t, dashboard, groups[2].ObjectGroup.Resource)
}