Compare commits

...

8 Commits

Author SHA1 Message Date
swapnil-signoz
2736c82220 style(sqlmigration): drop history comment in org preference tuples migration 2026-11-14 01:59:27 +05:30
swapnil-signoz
d5dc1cbfb7 chore(openapi): regenerate spec for org preference scopes 2026-11-14 01:53:32 +05:30
swapnil-signoz
433e777258 feat(authz): enable FGA for org preference routes
The migration writes tuples only: stored roles already carry the org-preference grant.
2026-11-14 01:53:31 +05:30
swapnil-signoz
ca97c23788 feat(preference): add resource defs to org preference routes
Routes keep the AdminAccess gate. The preference name is recorded as
the resource id with a wildcard selector, since names are not UUIDs.
2026-11-14 01:33:32 +05:30
swapnil-signoz
ea990196dc style(sqlmigration): group imports in the org tuples migration 2026-11-14 01:33:32 +05:30
swapnil-signoz
24aab06a86 chore(openapi): regenerate spec for org scopes 2026-11-14 01:33:32 +05:30
swapnil-signoz
c25fb32935 feat(authz): enable FGA for org routes
Editor and viewer stay blocked, matching the AdminAccess gate.
2026-11-14 01:33:32 +05:30
swapnil-signoz
7e87e1ca04 feat(org): add resource defs to org routes
Routes keep the AdminAccess gate.
2026-11-14 01:33:32 +05:30
8 changed files with 377 additions and 25 deletions

View File

@@ -16319,9 +16319,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- org-preference:list
- tokenizer:
- ADMIN
- org-preference:list
summary: List org preferences
tags:
- preferences
@@ -16384,9 +16384,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- org-preference:read
- tokenizer:
- ADMIN
- org-preference:read
summary: Get org preference
tags:
- preferences
@@ -16441,9 +16441,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- org-preference:update
- tokenizer:
- ADMIN
- org-preference:update
summary: Update org preference
tags:
- preferences
@@ -24702,9 +24702,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- organization:read
- tokenizer:
- ADMIN
- organization:read
summary: Get my organization
tags:
- orgs
@@ -24753,9 +24753,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- organization:update
- tokenizer:
- ADMIN
- organization:update
summary: Update my organization
tags:
- orgs

View File

@@ -5,11 +5,13 @@ import (
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/gorilla/mux"
)
func (provider *provider) addOrgRoutes(router *mux.Router) error {
if err := router.Handle("/api/v2/orgs/me", handler.New(provider.authzMiddleware.AdminAccess(provider.orgHandler.Get), handler.OpenAPIDef{
if err := router.Handle("/api/v2/orgs/me", handler.New(provider.authzMiddleware.CheckResources(provider.orgHandler.Get, authtypes.SigNozAdminRoleName), handler.OpenAPIDef{
ID: "GetMyOrganization",
Tags: []string{"orgs"},
Summary: "Get my organization",
@@ -21,12 +23,18 @@ func (provider *provider) addOrgRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceOrganization.Scope(coretypes.VerbRead)}),
}, handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceOrganization,
Verb: coretypes.VerbRead,
Category: coretypes.ActionCategoryDataAccess,
ID: provider.orgIDExtractor(),
Selector: coretypes.IDSelector,
}))).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/orgs/me", handler.New(provider.authzMiddleware.AdminAccess(provider.orgHandler.Update), handler.OpenAPIDef{
if err := router.Handle("/api/v2/orgs/me", handler.New(provider.authzMiddleware.CheckResources(provider.orgHandler.Update, authtypes.SigNozAdminRoleName), handler.OpenAPIDef{
ID: "UpdateMyOrganization",
Tags: []string{"orgs"},
Summary: "Update my organization",
@@ -38,10 +46,31 @@ func (provider *provider) addOrgRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusConflict, http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceOrganization.Scope(coretypes.VerbUpdate)}),
}, handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceOrganization,
Verb: coretypes.VerbUpdate,
Category: coretypes.ActionCategoryConfigurationChange,
ID: provider.orgIDExtractor(),
Selector: coretypes.IDSelector,
}))).Methods(http.MethodPut).GetError(); err != nil {
return err
}
return nil
}
func (provider *provider) orgIDExtractor() coretypes.ResourceIDExtractor {
return coretypes.NewResourceIDExtractor(coretypes.PhaseRequest, func(ec coretypes.ExtractorContext) (string, error) {
if ec.Request == nil {
return "", nil
}
claims, err := authtypes.ClaimsFromContext(ec.Request.Context())
if err != nil {
return "", err
}
return claims.OrgID, nil
})
}

View File

@@ -5,6 +5,8 @@ import (
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/preferencetypes"
"github.com/gorilla/mux"
)
@@ -61,7 +63,7 @@ func (provider *provider) addPreferenceRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/org/preferences", handler.New(provider.authzMiddleware.AdminAccess(provider.preferenceHandler.ListByOrg), handler.OpenAPIDef{
if err := router.Handle("/api/v1/org/preferences", handler.New(provider.authzMiddleware.CheckResources(provider.preferenceHandler.ListByOrg, authtypes.SigNozAdminRoleName), handler.OpenAPIDef{
ID: "ListOrgPreferences",
Tags: []string{"preferences"},
Summary: "List org preferences",
@@ -73,12 +75,17 @@ func (provider *provider) addPreferenceRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceOrgPreference.Scope(coretypes.VerbList)}),
}, handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceOrgPreference,
Verb: coretypes.VerbList,
Category: coretypes.ActionCategoryDataAccess,
Selector: coretypes.WildcardSelector,
}))).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/org/preferences/{name}", handler.New(provider.authzMiddleware.AdminAccess(provider.preferenceHandler.GetByOrg), handler.OpenAPIDef{
if err := router.Handle("/api/v1/org/preferences/{name}", handler.New(provider.authzMiddleware.CheckResources(provider.preferenceHandler.GetByOrg, authtypes.SigNozAdminRoleName), handler.OpenAPIDef{
ID: "GetOrgPreference",
Tags: []string{"preferences"},
Summary: "Get org preference",
@@ -90,12 +97,18 @@ func (provider *provider) addPreferenceRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceOrgPreference.Scope(coretypes.VerbRead)}),
}, handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceOrgPreference,
Verb: coretypes.VerbRead,
Category: coretypes.ActionCategoryDataAccess,
ID: coretypes.PathParam("name"),
Selector: coretypes.WildcardSelector,
}))).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/org/preferences/{name}", handler.New(provider.authzMiddleware.AdminAccess(provider.preferenceHandler.UpdateByOrg), handler.OpenAPIDef{
if err := router.Handle("/api/v1/org/preferences/{name}", handler.New(provider.authzMiddleware.CheckResources(provider.preferenceHandler.UpdateByOrg, authtypes.SigNozAdminRoleName), handler.OpenAPIDef{
ID: "UpdateOrgPreference",
Tags: []string{"preferences"},
Summary: "Update org preference",
@@ -107,8 +120,14 @@ func (provider *provider) addPreferenceRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceOrgPreference.Scope(coretypes.VerbUpdate)}),
}, handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceOrgPreference,
Verb: coretypes.VerbUpdate,
Category: coretypes.ActionCategoryConfigurationChange,
ID: coretypes.PathParam("name"),
Selector: coretypes.WildcardSelector,
}))).Methods(http.MethodPut).GetError(); err != nil {
return err
}

View File

@@ -275,6 +275,8 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewMigrateRulesV1ToV2Alpha1Factory(sqlstore),
sqlmigration.NewAddLLMPricingRuleTuplesFactory(sqlstore),
sqlmigration.NewRemoveUnsupportedQuickFiltersFactory(),
sqlmigration.NewAddOrganizationTuplesFactory(sqlstore),
sqlmigration.NewAddOrgPreferenceTuplesFactory(sqlstore),
)
}

View File

@@ -0,0 +1,158 @@
package sqlmigration
import (
"context"
"database/sql"
"encoding/json"
"time"
"github.com/oklog/ulid/v2"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
"github.com/uptrace/bun/migrate"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
)
type addOrganizationTuples struct {
sqlstore sqlstore.SQLStore
}
func NewAddOrganizationTuplesFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("add_organization_tuples"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &addOrganizationTuples{sqlstore: sqlstore}, nil
})
}
func (migration *addOrganizationTuples) Register(migrations *migrate.Migrations) error {
return migrations.Register(migration.Up, migration.Down)
}
func (migration *addOrganizationTuples) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
var storeID string
err = tx.QueryRowContext(ctx, `SELECT id FROM store WHERE name = ? LIMIT 1`, "signoz").Scan(&storeID)
if err != nil {
return err
}
var orgIDs []string
err = tx.NewSelect().
Table("organizations").
Column("id").
Scan(ctx, &orgIDs)
if err != nil && err != sql.ErrNoRows {
return err
}
isPG := migration.sqlstore.BunDB().Dialect().Name() == dialect.PG
tuples := []migrationTuple{
{authtypes.SigNozAdminRoleName, "organization", "organization", "read"},
{authtypes.SigNozAdminRoleName, "organization", "organization", "update"},
}
for _, orgID := range orgIDs {
for _, tuple := range tuples {
entropy := ulid.DefaultEntropy()
now := time.Now().UTC()
tupleID := ulid.MustNew(ulid.Timestamp(now), entropy).String()
// The organization object has no org prefix: organization:organization/<selector>.
objectID := tuple.objectName + "/*"
roleSubject := "organization/" + orgID + "/role/" + tuple.roleName
if isPG {
user := "role:" + roleSubject + "#assignee"
result, err := tx.ExecContext(ctx, `
INSERT INTO tuple (store, object_type, object_id, relation, _user, user_type, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, object_type, object_id, relation, _user) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, user, "userset", tupleID, now,
)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
continue
}
_, err = tx.ExecContext(ctx, `
INSERT INTO changelog (store, object_type, object_id, relation, _user, operation, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, user, 0, tupleID, now,
)
if err != nil {
return err
}
} else {
result, err := tx.ExecContext(ctx, `
INSERT INTO tuple (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, user_type, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", "userset", tupleID, now,
)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
continue
}
_, err = tx.ExecContext(ctx, `
INSERT INTO changelog (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, operation, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", 0, tupleID, now,
)
if err != nil {
return err
}
}
}
}
managedRoleGroups := make(map[string]string, len(coretypes.ManagedRoleToTransactions))
for roleName, transactions := range coretypes.ManagedRoleToTransactions {
data, err := json.Marshal(authtypes.NewTransactionGroupsFromTransactions(transactions))
if err != nil {
return err
}
managedRoleGroups[roleName] = string(data)
}
for _, orgID := range orgIDs {
for roleName, data := range managedRoleGroups {
if _, err := tx.NewUpdate().
Model(new(roles)).
Set("transaction_groups = ?", data).
Where("org_id = ?", orgID).
Where("type = ?", authtypes.RoleTypeManaged.StringValue()).
Where("name = ?", roleName).
Exec(ctx); err != nil {
return err
}
}
}
return tx.Commit()
}
func (migration *addOrganizationTuples) Down(context.Context, *bun.DB) error {
return nil
}

View File

@@ -0,0 +1,133 @@
package sqlmigration
import (
"context"
"database/sql"
"time"
"github.com/oklog/ulid/v2"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
"github.com/uptrace/bun/migrate"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
)
type addOrgPreferenceTuples struct {
sqlstore sqlstore.SQLStore
}
func NewAddOrgPreferenceTuplesFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("add_org_preference_tuples"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &addOrgPreferenceTuples{sqlstore: sqlstore}, nil
})
}
func (migration *addOrgPreferenceTuples) Register(migrations *migrate.Migrations) error {
return migrations.Register(migration.Up, migration.Down)
}
func (migration *addOrgPreferenceTuples) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
var storeID string
err = tx.QueryRowContext(ctx, `SELECT id FROM store WHERE name = ? LIMIT 1`, "signoz").Scan(&storeID)
if err != nil {
return err
}
var orgIDs []string
err = tx.NewSelect().
Table("organizations").
Column("id").
Scan(ctx, &orgIDs)
if err != nil && err != sql.ErrNoRows {
return err
}
isPG := migration.sqlstore.BunDB().Dialect().Name() == dialect.PG
tuples := []migrationTuple{
{"signoz-admin", "metaresource", "org-preference", "read"},
{"signoz-admin", "metaresource", "org-preference", "update"},
{"signoz-admin", "metaresource", "org-preference", "delete"},
{"signoz-admin", "metaresource", "org-preference", "create"},
{"signoz-admin", "metaresource", "org-preference", "list"},
}
for _, orgID := range orgIDs {
for _, tuple := range tuples {
entropy := ulid.DefaultEntropy()
now := time.Now().UTC()
tupleID := ulid.MustNew(ulid.Timestamp(now), entropy).String()
objectID := "organization/" + orgID + "/" + tuple.objectName + "/*"
roleSubject := "organization/" + orgID + "/role/" + tuple.roleName
if isPG {
user := "role:" + roleSubject + "#assignee"
result, err := tx.ExecContext(ctx, `
INSERT INTO tuple (store, object_type, object_id, relation, _user, user_type, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, object_type, object_id, relation, _user) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, user, "userset", tupleID, now,
)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
continue
}
_, err = tx.ExecContext(ctx, `
INSERT INTO changelog (store, object_type, object_id, relation, _user, operation, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, user, 0, tupleID, now,
)
if err != nil {
return err
}
} else {
result, err := tx.ExecContext(ctx, `
INSERT INTO tuple (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, user_type, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", "userset", tupleID, now,
)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
continue
}
_, err = tx.ExecContext(ctx, `
INSERT INTO changelog (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, operation, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", 0, tupleID, now,
)
if err != nil {
return err
}
}
}
}
return tx.Commit()
}
func (migration *addOrgPreferenceTuples) Down(context.Context, *bun.DB) error {
return nil
}

View File

@@ -88,6 +88,9 @@ var ManagedRoleToTransactions = map[string][]Transaction{
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindOrgPreference}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindOrgPreference}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindOrgPreference}, WildCardSelectorString)},
// organization: admin only
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeOrganization, Kind: KindOrganization}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeOrganization, Kind: KindOrganization}, WildCardSelectorString)},
// public-dashboard — admin manages, anonymous reads
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPublicDashboard}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPublicDashboard}, WildCardSelectorString)},

View File

@@ -138,6 +138,14 @@
"list"
]
},
{
"type": "organization",
"kind": "organization",
"verbs": [
"read",
"update"
]
},
{
"type": "metaresource",
"kind": "public-dashboard",