Compare commits

...

3 Commits

Author SHA1 Message Date
vikrantgupta25
fe0bb30936 feat(authz): remove unnecessary build checks from the contributing doc 2026-04-24 18:23:16 +05:30
vikrantgupta25
4369aeaf19 feat(authz): update authz permission generated file 2026-04-24 18:17:40 +05:30
vikrantgupta25
79e3340292 refactor(authz): centralize typeable registry and decouple from module DI
- Move typeable vars to authtypes/typeable.go
- Add Registry interface to pkg/authz
- Create pkg/authz/authzregistry implementation
- Add MustNewWildcardTransaction to authtypes
- Remove RegisterTypeable and module implementations
- Authz providers receive Registry via entry points
- Add FGA contributing guide
2026-04-24 18:02:12 +05:30
21 changed files with 445 additions and 147 deletions

View File

@@ -12,6 +12,7 @@ import (
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/authn"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/authz/authzregistry"
"github.com/SigNoz/signoz/pkg/authz/openfgaauthz"
"github.com/SigNoz/signoz/pkg/authz/openfgaschema"
"github.com/SigNoz/signoz/pkg/authz/openfgaserver"
@@ -92,13 +93,14 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error) {
return signoz.NewAuthNs(ctx, providerSettings, store, licensing)
},
func(ctx context.Context, sqlstore sqlstore.SQLStore, _ licensing.Licensing, _ []authz.OnBeforeRoleDelete, _ dashboard.Module) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
func(ctx context.Context, sqlstore sqlstore.SQLStore, _ licensing.Licensing, _ []authz.OnBeforeRoleDelete) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
openfgaDataStore, err := openfgaserver.NewSQLStore(sqlstore)
if err != nil {
return nil, err
}
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore), nil
authzRegistry := authzregistry.NewAuthzRegistry()
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, authzRegistry), nil
},
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing) dashboard.Module {
return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser)

View File

@@ -32,6 +32,7 @@ import (
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/authn"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/authz/authzregistry"
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
@@ -137,12 +138,14 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
return authNs, nil
},
func(ctx context.Context, sqlstore sqlstore.SQLStore, licensing licensing.Licensing, onBeforeRoleDelete []authz.OnBeforeRoleDelete, dashboardModule dashboard.Module) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
func(ctx context.Context, sqlstore sqlstore.SQLStore, licensing licensing.Licensing, onBeforeRoleDelete []authz.OnBeforeRoleDelete) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
openfgaDataStore, err := openfgaserver.NewSQLStore(sqlstore)
if err != nil {
return nil, err
}
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, licensing, onBeforeRoleDelete, dashboardModule), nil
authzRegistry := authzregistry.NewAuthzRegistry()
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, licensing, onBeforeRoleDelete, authzRegistry), nil
},
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, querier, licensing)

View File

@@ -0,0 +1,198 @@
# Authorization (FGA)
SigNoz uses OpenFGA for fine-grained authorization. Resources are modeled as FGA objects — the authz system checks whether a principal (user or service account) has a specific relation (read, update, delete, etc.) on a specific resource.
This guide explains how to enable FGA for a new entity.
## Overview
Enabling FGA for an entity involves four steps:
1. **Define typeables** — Declare the resource type identities in `authtypes`
2. **Register in the authz registry** — Define which managed roles get which permissions
3. **Switch routes to the Check middleware** — Replace role-based middleware with resource-level FGA checks
4. **Add a migration** — Backfill FGA tuples for existing organizations
## Step 1: Define typeables in `authtypes`
Add the typeable vars to the var block in `pkg/types/authtypes/typeable.go`, alongside the existing typeables. Every FGA-managed entity needs two typeables:
- A **collection typeable** (`metaresources`) — for `create` and `list` operations
- An **instance typeable** (`metaresource`) — for `read`, `update`, and `delete` operations
```go
// pkg/types/authtypes/typeable.go — add to the existing var block
var (
// ... existing typeables ...
TypeableMetaResourceMyEntity = MustNewTypeableMetaResource(MustNewName("my-entity"))
TypeableMetaResourcesMyEntities = MustNewTypeableMetaResources(MustNewName("my-entities"))
)
```
These produce FGA objects like:
- `metaresource:organization/{orgID}/my-entity/{entityID}` — individual instance
- `metaresources:organization/{orgID}/my-entities/*` — collection
Use kebab-case for names. The collection name is typically the plural form.
## Step 2: Register in the authz registry
Create a new file `pkg/authz/authzregistry/myentity.go`. Each registry file exports two functions:
- `myEntityTypeables()` — returns the typeables for this entity
- `myEntityTransactions()` — returns the managed role → transaction mapping
Use `serviceaccount.go` or `dashboard.go` as a reference. Here is the pattern:
```go
package authzregistry
import "github.com/SigNoz/signoz/pkg/types/authtypes"
func myEntityTypeables() []authtypes.Typeable {
return []authtypes.Typeable{
authtypes.TypeableMetaResourceMyEntity,
authtypes.TypeableMetaResourcesMyEntities,
}
}
func myEntityTransactions() map[string][]*authtypes.Transaction {
return map[string][]*authtypes.Transaction{
authtypes.SigNozAdminRoleName: {
authtypes.MustNewWildcardTransaction(authtypes.TypeableMetaResourcesMyEntities, authtypes.RelationCreate),
authtypes.MustNewWildcardTransaction(authtypes.TypeableMetaResourcesMyEntities, authtypes.RelationList),
authtypes.MustNewWildcardTransaction(authtypes.TypeableMetaResourceMyEntity, authtypes.RelationRead),
authtypes.MustNewWildcardTransaction(authtypes.TypeableMetaResourceMyEntity, authtypes.RelationUpdate),
authtypes.MustNewWildcardTransaction(authtypes.TypeableMetaResourceMyEntity, authtypes.RelationDelete),
},
authtypes.SigNozEditorRoleName: {
authtypes.MustNewWildcardTransaction(authtypes.TypeableMetaResourcesMyEntities, authtypes.RelationList),
authtypes.MustNewWildcardTransaction(authtypes.TypeableMetaResourceMyEntity, authtypes.RelationRead),
},
authtypes.SigNozViewerRoleName: {
authtypes.MustNewWildcardTransaction(authtypes.TypeableMetaResourcesMyEntities, authtypes.RelationList),
authtypes.MustNewWildcardTransaction(authtypes.TypeableMetaResourceMyEntity, authtypes.RelationRead),
},
}
}
```
`MustNewWildcardTransaction(typeable, relation)` creates a transaction granting the relation on all instances (`*`) of that resource type. It validates that the relation is valid for the type and generates a unique ID.
Then wire it into `pkg/authz/authzregistry/registry.go`:
```go
func collectTypeables() []authtypes.Typeable {
typeables := make([]authtypes.Typeable, 0)
typeables = append(typeables, roleTypeables()...)
typeables = append(typeables, dashboardTypeables()...)
typeables = append(typeables, serviceAccountTypeables()...)
typeables = append(typeables, myEntityTypeables()...) // <-- add this
return typeables
}
func collectTransactions() map[string][]*authtypes.Transaction {
transactions := make(map[string][]*authtypes.Transaction)
sources := []map[string][]*authtypes.Transaction{
dashboardTransactions(),
serviceAccountTransactions(),
myEntityTransactions(), // <-- add this
}
for _, source := range sources {
for roleName, txns := range source {
transactions[roleName] = append(transactions[roleName], txns...)
}
}
return transactions
}
```
## Step 3: Switch routes to the Check middleware
In your route file (e.g., `pkg/apiserver/signozapiserver/myentity.go`), replace `AdminAccess` / `EditAccess` / `ViewAccess` with the `Check` middleware:
```go
provider.authZ.Check(
handler, // the HTTP handler func
authtypes.RelationRead, // the relation to check
authtypes.TypeableMetaResourceMyEntity, // the typeable
selectorCallback, // extracts resource ID from the request
roles, // role names for community edition fallback
)
```
### Selector callbacks
You need two callbacks — one for collection operations, one for instance operations:
```go
// For create/list — wildcard selector on the collection.
func myEntityCollectionSelector(_ *http.Request, _ authtypes.Claims) ([]authtypes.Selector, error) {
return []authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeMetaResources, authtypes.WildCardSelectorString),
}, nil
}
// For read/update/delete — specific instance ID + wildcard.
func myEntityInstanceSelector(req *http.Request, _ authtypes.Claims) ([]authtypes.Selector, error) {
id := mux.Vars(req)["id"]
idSelector, err := authtypes.NewSelector(authtypes.TypeMetaResource, id)
if err != nil {
return nil, err
}
return []authtypes.Selector{
idSelector,
authtypes.MustNewSelector(authtypes.TypeMetaResource, authtypes.WildCardSelectorString),
}, nil
}
```
The instance callback includes a wildcard selector so that roles with wildcard permission (`*`) also match. Use `NewSelector` (not `MustNewSelector`) for user-supplied path parameters to avoid panics on malformed input.
### Role fallback
The `roles` parameter is used by the **community edition**, where `CheckWithTupleCreation` only checks role membership (ignoring resource selectors). Pass the role names that should have access:
```go
var myEntityAdminRoles = []string{authtypes.SigNozAdminRoleName}
var myEntityReadRoles = []string{authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName}
```
### OpenAPI security schemes
Use `newScopedSecuritySchemes` with the exact FGA scope, generated via `Typeable.Scope(relation)`:
```go
SecuritySchemes: newScopedSecuritySchemes([]string{
authtypes.TypeableMetaResourceMyEntity.Scope(authtypes.RelationRead),
}),
// produces: ["my-entity:read"]
```
## Step 4: Add a migration for existing organizations
New organizations get FGA tuples automatically during bootstrap (via `CreateManagedUserRoleTransactions`). Existing organizations need a SQL migration to backfill the tuples.
Create a migration file in `pkg/sqlmigration/` (use the next available number). Follow the pattern in `078_add_sa_managed_role_txn.go`:
1. Select the OpenFGA store ID
2. Iterate all organizations
3. For each org × tuple, insert into the `tuple` and `changelog` tables
4. Use `ON CONFLICT DO NOTHING` for idempotency
5. Handle both PostgreSQL and SQLite dialects
Register the migration in `pkg/signoz/provider.go`.
## Checklist
- [ ] Typeable vars added to `pkg/types/authtypes/typeable.go`
- [ ] Registry file created in `pkg/authz/authzregistry/` with `*Typeables()` and `*Transactions()` functions
- [ ] Functions wired into `collectTypeables()` and `collectTransactions()` in `registry.go`
- [ ] Routes switched from `AdminAccess`/`EditAccess`/`ViewAccess` to `Check` middleware
- [ ] Selector callbacks use `NewSelector` (not `MustNewSelector`) for user-supplied IDs
- [ ] OpenAPI `SecuritySchemes` use `newScopedSecuritySchemes` with exact scope strings
- [ ] Migration backfills FGA tuples for existing organizations

View File

@@ -11,6 +11,7 @@ We adhere to three primary style guides as our foundation:
We **recommend** (almost enforce) reviewing these guides before contributing to the codebase. They provide valuable insights into writing idiomatic Go code and will help you understand our approach to backend development. In addition, we have a few additional rules that make certain areas stricter than the above which can be found in area-specific files in this package:
- [Abstractions](abstractions.md) - When to introduce new types and intermediate representations
- [Authorization](authz.md) - Enabling FGA for new entities
- [Errors](errors.md) - Structured error handling
- [Endpoint](endpoint.md) - HTTP endpoint patterns
- [Flagger](flagger.md) - Feature flag patterns

View File

@@ -25,19 +25,19 @@ type provider struct {
openfgaServer *openfgaserver.Server
licensing licensing.Licensing
store authtypes.RoleStore
registry []authz.RegisterTypeable
registry authz.Registry
settings factory.ScopedProviderSettings
onBeforeRoleDelete []authz.OnBeforeRoleDelete
}
func NewProviderFactory(sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, licensing licensing.Licensing, onBeforeRoleDelete []authz.OnBeforeRoleDelete, registry ...authz.RegisterTypeable) factory.ProviderFactory[authz.AuthZ, authz.Config] {
func NewProviderFactory(sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, licensing licensing.Licensing, onBeforeRoleDelete []authz.OnBeforeRoleDelete, registry authz.Registry) factory.ProviderFactory[authz.AuthZ, authz.Config] {
return factory.NewProviderFactory(factory.MustNewName("openfga"), func(ctx context.Context, ps factory.ProviderSettings, config authz.Config) (authz.AuthZ, error) {
return newOpenfgaProvider(ctx, ps, config, sqlstore, openfgaSchema, openfgaDataStore, licensing, onBeforeRoleDelete, registry)
})
}
func newOpenfgaProvider(ctx context.Context, settings factory.ProviderSettings, config authz.Config, sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, licensing licensing.Licensing, onBeforeRoleDelete []authz.OnBeforeRoleDelete, registry []authz.RegisterTypeable) (authz.AuthZ, error) {
pkgOpenfgaAuthzProvider := pkgopenfgaauthz.NewProviderFactory(sqlstore, openfgaSchema, openfgaDataStore)
func newOpenfgaProvider(ctx context.Context, settings factory.ProviderSettings, config authz.Config, sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, licensing licensing.Licensing, onBeforeRoleDelete []authz.OnBeforeRoleDelete, registry authz.Registry) (authz.AuthZ, error) {
pkgOpenfgaAuthzProvider := pkgopenfgaauthz.NewProviderFactory(sqlstore, openfgaSchema, openfgaDataStore, registry)
pkgAuthzService, err := pkgOpenfgaAuthzProvider.New(ctx, settings, config)
if err != nil {
return nil, err
@@ -210,12 +210,7 @@ func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, ro
func (provider *provider) GetResources(_ context.Context) []*authtypes.Resource {
resources := make([]*authtypes.Resource, 0)
for _, register := range provider.registry {
for _, typeable := range register.MustGetTypeables() {
resources = append(resources, &authtypes.Resource{Name: typeable.Name(), Type: typeable.Type()})
}
}
for _, typeable := range provider.MustGetTypeables() {
for _, typeable := range provider.registry.GetTypeables() {
resources = append(resources, &authtypes.Resource{Name: typeable.Name(), Type: typeable.Type()})
}
@@ -234,7 +229,7 @@ func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id
}
objects := make([]*authtypes.Object, 0)
for _, objectType := range provider.getUniqueTypes() {
for _, objectType := range provider.registry.GetUniqueTypes() {
if !slices.Contains(authtypes.TypeableRelations[objectType], relation) {
continue
}
@@ -318,9 +313,6 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
return provider.store.Delete(ctx, orgID, id)
}
func (provider *provider) MustGetTypeables() []authtypes.Typeable {
return []authtypes.Typeable{authtypes.TypeableRole, authtypes.TypeableResourcesRoles}
}
func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID valuer.UUID) ([]*openfgav1.TupleKey, error) {
tuples := []*openfgav1.TupleKey{}
@@ -359,15 +351,8 @@ func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID va
}
func (provider *provider) getManagedRoleTransactionTuples(orgID valuer.UUID) ([]*openfgav1.TupleKey, error) {
transactionsByRole := make(map[string][]*authtypes.Transaction)
for _, register := range provider.registry {
for roleName, txns := range register.MustGetManagedRoleTransactions() {
transactionsByRole[roleName] = append(transactionsByRole[roleName], txns...)
}
}
tuples := make([]*openfgav1.TupleKey, 0)
for roleName, transactions := range transactionsByRole {
for roleName, transactions := range provider.registry.GetManagedRoleTransactions() {
for _, txn := range transactions {
typeable := authtypes.MustNewTypeableFromType(txn.Object.Resource.Type, txn.Object.Resource.Name)
txnTuples, err := typeable.Tuples(
@@ -395,7 +380,7 @@ func (provider *provider) deleteTuples(ctx context.Context, roleName string, org
subject := authtypes.MustNewSubject(authtypes.TypeableRole, roleName, orgID, &authtypes.RelationAssignee)
tuples := make([]*openfgav1.TupleKey, 0)
for _, objectType := range provider.getUniqueTypes() {
for _, objectType := range provider.registry.GetUniqueTypes() {
typeTuples, err := provider.ReadTuples(ctx, &openfgav1.ReadRequestTupleKey{
User: subject,
Object: objectType.StringValue() + ":",
@@ -425,27 +410,3 @@ func (provider *provider) deleteTuples(ctx context.Context, roleName string, org
return nil
}
func (provider *provider) getUniqueTypes() []authtypes.Type {
seen := make(map[string]struct{})
uniqueTypes := make([]authtypes.Type, 0)
for _, register := range provider.registry {
for _, typeable := range register.MustGetTypeables() {
typeKey := typeable.Type().StringValue()
if _, ok := seen[typeKey]; ok {
continue
}
seen[typeKey] = struct{}{}
uniqueTypes = append(uniqueTypes, typeable.Type())
}
}
for _, typeable := range provider.MustGetTypeables() {
typeKey := typeable.Type().StringValue()
if _, ok := seen[typeKey]; ok {
continue
}
seen[typeKey] = struct{}{}
uniqueTypes = append(uniqueTypes, typeable.Type())
}
return uniqueTypes
}

View File

@@ -217,28 +217,6 @@ func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valu
return module.pkgDashboardModule.LockUnlock(ctx, orgID, id, updatedBy, isAdmin, lock)
}
func (module *module) MustGetTypeables() []authtypes.Typeable {
return module.pkgDashboardModule.MustGetTypeables()
}
func (module *module) MustGetManagedRoleTransactions() map[string][]*authtypes.Transaction {
return map[string][]*authtypes.Transaction{
authtypes.SigNozAnonymousRoleName: {
{
ID: valuer.GenerateUUID(),
Relation: authtypes.RelationRead,
Object: *authtypes.MustNewObject(
authtypes.Resource{
Type: authtypes.TypeMetaResource,
Name: dashboardtypes.TypeableMetaResourcePublicDashboard.Name(),
},
authtypes.MustNewSelector(authtypes.TypeMetaResource, "*"),
),
},
},
}
}
func (module *module) deletePublic(ctx context.Context, _ valuer.UUID, dashboardID valuer.UUID) error {
return module.store.DeletePublic(ctx, dashboardID.StringValue())
}

View File

@@ -3,20 +3,32 @@ export default {
status: 'success',
data: {
resources: [
{
name: 'role',
type: 'role',
},
{
name: 'roles',
type: 'metaresources',
},
{
name: 'dashboard',
type: 'metaresource',
},
{
name: 'public-dashboard',
type: 'metaresource',
},
{
name: 'dashboards',
type: 'metaresources',
},
{
name: 'role',
type: 'role',
name: 'service-account',
type: 'metaresource',
},
{
name: 'roles',
name: 'service-accounts',
type: 'metaresources',
},
],

View File

@@ -84,7 +84,7 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/public/dashboards/{id}", handler.New(provider.authZ.CheckWithoutClaims(
provider.dashboardHandler.GetPublicData,
authtypes.RelationRead,
dashboardtypes.TypeableMetaResourcePublicDashboard,
authtypes.TypeableMetaResourcePublicDashboard,
func(req *http.Request, orgs []*types.Organization) ([]authtypes.Selector, valuer.UUID, error) {
id, err := valuer.NewUUID(mux.Vars(req)["id"])
if err != nil {
@@ -104,7 +104,7 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newAnonymousSecuritySchemes([]string{dashboardtypes.TypeableMetaResourcePublicDashboard.Scope(authtypes.RelationRead)}),
SecuritySchemes: newAnonymousSecuritySchemes([]string{authtypes.TypeableMetaResourcePublicDashboard.Scope(authtypes.RelationRead)}),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -112,7 +112,7 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/public/dashboards/{id}/widgets/{idx}/query_range", handler.New(provider.authZ.CheckWithoutClaims(
provider.dashboardHandler.GetPublicWidgetQueryRange,
authtypes.RelationRead,
dashboardtypes.TypeableMetaResourcePublicDashboard,
authtypes.TypeableMetaResourcePublicDashboard,
func(req *http.Request, orgs []*types.Organization) ([]authtypes.Selector, valuer.UUID, error) {
id, err := valuer.NewUUID(mux.Vars(req)["id"])
if err != nil {
@@ -132,7 +132,7 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newAnonymousSecuritySchemes([]string{dashboardtypes.TypeableMetaResourcePublicDashboard.Scope(authtypes.RelationRead)}),
SecuritySchemes: newAnonymousSecuritySchemes([]string{authtypes.TypeableMetaResourcePublicDashboard.Scope(authtypes.RelationRead)}),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}

View File

@@ -316,10 +316,7 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
}
func newSecuritySchemes(role types.Role) []handler.OpenAPISecurityScheme {
return []handler.OpenAPISecurityScheme{
{Name: authtypes.IdentNProviderAPIKey.StringValue(), Scopes: []string{role.String()}},
{Name: authtypes.IdentNProviderTokenizer.StringValue(), Scopes: []string{role.String()}},
}
return newScopedSecuritySchemes([]string{role.String()})
}
func newAnonymousSecuritySchemes(scopes []string) []handler.OpenAPISecurityScheme {
@@ -327,3 +324,10 @@ func newAnonymousSecuritySchemes(scopes []string) []handler.OpenAPISecuritySchem
{Name: authtypes.IdentNProviderAnonymous.StringValue(), Scopes: scopes},
}
}
func newScopedSecuritySchemes(scopes []string) []handler.OpenAPISecurityScheme {
return []handler.OpenAPISecurityScheme{
{Name: authtypes.IdentNProviderAPIKey.StringValue(), Scopes: scopes},
{Name: authtypes.IdentNProviderTokenizer.StringValue(), Scopes: scopes},
}
}

View File

@@ -90,10 +90,11 @@ type AuthZ interface {
// OnBeforeRoleDelete is a callback invoked before a role is deleted.
type OnBeforeRoleDelete func(context.Context, valuer.UUID, valuer.UUID) error
type RegisterTypeable interface {
MustGetTypeables() []authtypes.Typeable
MustGetManagedRoleTransactions() map[string][]*authtypes.Transaction
type Registry interface {
GetTypeables() []authtypes.Typeable
GetManagedRoleTransactions() map[string][]*authtypes.Transaction
GetUniqueTypes() []authtypes.Type
GetManagedRolesByTransaction() map[string][]string
}
type Handler interface {

View File

@@ -0,0 +1,19 @@
package authzregistry
import "github.com/SigNoz/signoz/pkg/types/authtypes"
func dashboardTypeables() []authtypes.Typeable {
return []authtypes.Typeable{
authtypes.TypeableMetaResourceDashboard,
authtypes.TypeableMetaResourcePublicDashboard,
authtypes.TypeableMetaResourcesDashboards,
}
}
func dashboardTransactions() map[string][]*authtypes.Transaction {
return map[string][]*authtypes.Transaction{
authtypes.SigNozAnonymousRoleName: {
authtypes.MustNewWildcardTransaction(authtypes.TypeableMetaResourcePublicDashboard, authtypes.RelationRead),
},
}
}

View File

@@ -0,0 +1,94 @@
package authzregistry
import (
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/types/authtypes"
)
type registry struct {
typeables []authtypes.Typeable
transactions map[string][]*authtypes.Transaction
uniqueTypes []authtypes.Type
managedRolesByTransaction map[string][]string
}
func NewAuthzRegistry() authz.Registry {
typeables := collectTypeables()
transactions := collectTransactions()
uniqueTypes := buildUniqueTypes(typeables)
managedRolesByTransaction := buildManagedRolesByTransaction(transactions)
return &registry{
typeables: typeables,
transactions: transactions,
uniqueTypes: uniqueTypes,
managedRolesByTransaction: managedRolesByTransaction,
}
}
func (r *registry) GetTypeables() []authtypes.Typeable {
return r.typeables
}
func (r *registry) GetManagedRoleTransactions() map[string][]*authtypes.Transaction {
return r.transactions
}
func (r *registry) GetUniqueTypes() []authtypes.Type {
return r.uniqueTypes
}
func (r *registry) GetManagedRolesByTransaction() map[string][]string {
return r.managedRolesByTransaction
}
func collectTypeables() []authtypes.Typeable {
typeables := make([]authtypes.Typeable, 0)
typeables = append(typeables, roleTypeables()...)
typeables = append(typeables, dashboardTypeables()...)
typeables = append(typeables, serviceAccountTypeables()...)
return typeables
}
func collectTransactions() map[string][]*authtypes.Transaction {
transactions := make(map[string][]*authtypes.Transaction)
sources := []map[string][]*authtypes.Transaction{
dashboardTransactions(),
serviceAccountTransactions(),
}
for _, source := range sources {
for roleName, txns := range source {
transactions[roleName] = append(transactions[roleName], txns...)
}
}
return transactions
}
func buildUniqueTypes(typeables []authtypes.Typeable) []authtypes.Type {
seen := make(map[string]struct{})
uniqueTypes := make([]authtypes.Type, 0)
for _, typeable := range typeables {
typeKey := typeable.Type().StringValue()
if _, ok := seen[typeKey]; ok {
continue
}
seen[typeKey] = struct{}{}
uniqueTypes = append(uniqueTypes, typeable.Type())
}
return uniqueTypes
}
func buildManagedRolesByTransaction(transactions map[string][]*authtypes.Transaction) map[string][]string {
managedRolesByTransaction := make(map[string][]string)
for roleName, txns := range transactions {
for _, txn := range txns {
key := txn.TransactionKey()
managedRolesByTransaction[key] = append(managedRolesByTransaction[key], roleName)
}
}
return managedRolesByTransaction
}

View File

@@ -0,0 +1,10 @@
package authzregistry
import "github.com/SigNoz/signoz/pkg/types/authtypes"
func roleTypeables() []authtypes.Typeable {
return []authtypes.Typeable{
authtypes.TypeableRole,
authtypes.TypeableResourcesRoles,
}
}

View File

@@ -0,0 +1,30 @@
package authzregistry
import "github.com/SigNoz/signoz/pkg/types/authtypes"
func serviceAccountTypeables() []authtypes.Typeable {
return []authtypes.Typeable{
authtypes.TypeableMetaResourceServiceAccount,
authtypes.TypeableMetaResourcesServiceAccounts,
}
}
func serviceAccountTransactions() map[string][]*authtypes.Transaction {
return map[string][]*authtypes.Transaction{
authtypes.SigNozAdminRoleName: {
authtypes.MustNewWildcardTransaction(authtypes.TypeableMetaResourcesServiceAccounts, authtypes.RelationCreate),
authtypes.MustNewWildcardTransaction(authtypes.TypeableMetaResourcesServiceAccounts, authtypes.RelationList),
authtypes.MustNewWildcardTransaction(authtypes.TypeableMetaResourceServiceAccount, authtypes.RelationRead),
authtypes.MustNewWildcardTransaction(authtypes.TypeableMetaResourceServiceAccount, authtypes.RelationUpdate),
authtypes.MustNewWildcardTransaction(authtypes.TypeableMetaResourceServiceAccount, authtypes.RelationDelete),
},
authtypes.SigNozEditorRoleName: {
authtypes.MustNewWildcardTransaction(authtypes.TypeableMetaResourcesServiceAccounts, authtypes.RelationList),
authtypes.MustNewWildcardTransaction(authtypes.TypeableMetaResourceServiceAccount, authtypes.RelationRead),
},
authtypes.SigNozViewerRoleName: {
authtypes.MustNewWildcardTransaction(authtypes.TypeableMetaResourcesServiceAccounts, authtypes.RelationList),
authtypes.MustNewWildcardTransaction(authtypes.TypeableMetaResourceServiceAccount, authtypes.RelationRead),
},
}
}

View File

@@ -18,31 +18,27 @@ import (
)
type provider struct {
server *openfgaserver.Server
store authtypes.RoleStore
registry []authz.RegisterTypeable
managedRolesByTransaction map[string][]string
server *openfgaserver.Server
store authtypes.RoleStore
registry authz.Registry
}
func NewProviderFactory(sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, registry ...authz.RegisterTypeable) factory.ProviderFactory[authz.AuthZ, authz.Config] {
func NewProviderFactory(sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, registry authz.Registry) factory.ProviderFactory[authz.AuthZ, authz.Config] {
return factory.NewProviderFactory(factory.MustNewName("openfga"), func(ctx context.Context, ps factory.ProviderSettings, config authz.Config) (authz.AuthZ, error) {
return newOpenfgaProvider(ctx, ps, config, sqlstore, openfgaSchema, openfgaDataStore, registry)
})
}
func newOpenfgaProvider(ctx context.Context, settings factory.ProviderSettings, config authz.Config, sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, registry []authz.RegisterTypeable) (authz.AuthZ, error) {
func newOpenfgaProvider(ctx context.Context, settings factory.ProviderSettings, config authz.Config, sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, registry authz.Registry) (authz.AuthZ, error) {
server, err := openfgaserver.NewOpenfgaServer(ctx, settings, config, sqlstore, openfgaSchema, openfgaDataStore)
if err != nil {
return nil, err
}
managedRolesByTransaction := buildManagedRolesByTransaction(registry)
return &provider{
server: server,
store: sqlauthzstore.NewSqlAuthzStore(sqlstore),
registry: registry,
managedRolesByTransaction: managedRolesByTransaction,
server: server,
store: sqlauthzstore.NewSqlAuthzStore(sqlstore),
registry: registry,
}, nil
}
@@ -220,7 +216,7 @@ func (provider *provider) CheckTransactions(ctx context.Context, subject string,
return make([]*authtypes.TransactionWithAuthorization, 0), nil
}
tuples, preResolved, roleCorrelations, err := authtypes.NewTuplesFromTransactionsWithManagedRoles(transactions, subject, orgID, provider.managedRolesByTransaction)
tuples, preResolved, roleCorrelations, err := authtypes.NewTuplesFromTransactionsWithManagedRoles(transactions, subject, orgID, provider.registry.GetManagedRolesByTransaction())
if err != nil {
return nil, err
}
@@ -237,20 +233,3 @@ func (provider *provider) CheckTransactions(ctx context.Context, subject string,
return authtypes.NewTransactionWithAuthorizationFromBatchResults(transactions, batchResults, preResolved, roleCorrelations), nil
}
func buildManagedRolesByTransaction(registry []authz.RegisterTypeable) map[string][]string {
managedRolesByTransaction := make(map[string][]string)
for _, register := range registry {
for roleName, transactions := range register.MustGetManagedRoleTransactions() {
for _, txn := range transactions {
key := txn.TransactionKey()
managedRolesByTransaction[key] = append(managedRolesByTransaction[key], roleName)
}
}
}
return managedRolesByTransaction
}
func (provider *provider) MustGetTypeables() []authtypes.Typeable {
return nil
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"net/http"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/statsreporter"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
@@ -50,8 +49,6 @@ type Module interface {
GetByMetricNames(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string][]map[string]string, error)
statsreporter.StatsCollector
authz.RegisterTypeable
}
type Handler interface {

View File

@@ -202,14 +202,6 @@ func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[strin
return dashboardtypes.NewStatsFromStorableDashboards(dashboards), nil
}
func (module *module) MustGetTypeables() []authtypes.Typeable {
return []authtypes.Typeable{dashboardtypes.TypeableMetaResourceDashboard, dashboardtypes.TypeableMetaResourcesDashboards}
}
func (module *module) MustGetManagedRoleTransactions() map[string][]*authtypes.Transaction {
return nil
}
// CreatePublic is not supported.
func (module *module) CreatePublic(ctx context.Context, orgID valuer.UUID, publicDashboard *dashboardtypes.PublicDashboard) error {
return errors.Newf(errors.TypeUnsupported, dashboardtypes.ErrCodePublicDashboardUnsupported, "not implemented")

View File

@@ -100,7 +100,7 @@ func New(
sqlstoreProviderFactories factory.NamedMap[factory.ProviderFactory[sqlstore.SQLStore, sqlstore.Config]],
telemetrystoreProviderFactories factory.NamedMap[factory.ProviderFactory[telemetrystore.TelemetryStore, telemetrystore.Config]],
authNsCallback func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error),
authzCallback func(context.Context, sqlstore.SQLStore, licensing.Licensing, []authz.OnBeforeRoleDelete, dashboard.Module) (factory.ProviderFactory[authz.AuthZ, authz.Config], error),
authzCallback func(context.Context, sqlstore.SQLStore, licensing.Licensing, []authz.OnBeforeRoleDelete) (factory.ProviderFactory[authz.AuthZ, authz.Config], error),
dashboardModuleCallback func(sqlstore.SQLStore, factory.ProviderSettings, analytics.Analytics, organization.Getter, queryparser.QueryParser, querier.Querier, licensing.Licensing) dashboard.Module,
gatewayProviderFactory func(licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config],
auditorProviderFactories func(licensing.Licensing) factory.NamedMap[factory.ProviderFactory[auditor.Auditor, auditor.Config]],
@@ -322,12 +322,9 @@ func New(
return nil, err
}
// Initialize query parser (needed for dashboard module)
// Initialize query parser
queryParser := queryparser.New(providerSettings)
// Initialize dashboard module (needed for authz registry)
dashboard := dashboardModuleCallback(sqlstore, providerSettings, analytics, orgGetter, queryParser, querier, licensing)
// Initialize user getter
userGetter := impluser.NewGetter(userStore, userRoleStore, flagger)
@@ -341,7 +338,7 @@ func New(
}
// Initialize authz
authzProviderFactory, err := authzCallback(ctx, sqlstore, licensing, onBeforeRoleDelete, dashboard)
authzProviderFactory, err := authzCallback(ctx, sqlstore, licensing, onBeforeRoleDelete)
if err != nil {
return nil, err
}
@@ -350,6 +347,9 @@ func New(
return nil, err
}
// Initialize dashboard module (no longer needs to be before authz — registry is independent)
dashboard := dashboardModuleCallback(sqlstore, providerSettings, analytics, orgGetter, queryParser, querier, licensing)
// Initialize notification manager from the available notification manager provider factories
nfManager, err := factory.NewProviderFromNamedMap(
ctx,

View File

@@ -33,6 +33,24 @@ func NewTransaction(relation Relation, object Object) (*Transaction, error) {
return &Transaction{ID: valuer.GenerateUUID(), Relation: relation, Object: object}, nil
}
func MustNewTransaction(relation Relation, object Object) *Transaction {
txn, err := NewTransaction(relation, object)
if err != nil {
panic(err)
}
return txn
}
// MustNewWildcardTransaction creates a Transaction granting the given relation
// on all instances (*) of the given typeable. This is the standard way to
// express managed role permissions in the authz registry.
func MustNewWildcardTransaction(typeable Typeable, relation Relation) *Transaction {
return MustNewTransaction(relation, *MustNewObject(
Resource{Type: typeable.Type(), Name: typeable.Name()},
MustNewSelector(typeable.Type(), "*"),
))
}
func NewGettableTransaction(results []*TransactionWithAuthorization) []*GettableTransaction {
gettableTransactions := make([]*GettableTransaction, len(results))
for i, result := range results {

View File

@@ -25,11 +25,17 @@ var (
)
var (
TypeableUser = &typeableUser{}
TypeableServiceAccount = &typeableServiceAccount{}
TypeableAnonymous = &typeableAnonymous{}
TypeableRole = &typeableRole{}
TypeableOrganization = &typeableOrganization{}
TypeableUser Typeable = new(typeableUser)
TypeableServiceAccount Typeable = new(typeableServiceAccount)
TypeableAnonymous Typeable = new(typeableAnonymous)
TypeableRole Typeable = new(typeableRole)
TypeableOrganization Typeable = new(typeableOrganization)
TypeableMetaResourceDashboard = MustNewTypeableMetaResource(MustNewName("dashboard"))
TypeableMetaResourcePublicDashboard = MustNewTypeableMetaResource(MustNewName("public-dashboard"))
TypeableMetaResourcesDashboards = MustNewTypeableMetaResources(MustNewName("dashboards"))
TypeableMetaResourceServiceAccount = MustNewTypeableMetaResource(MustNewName("service-account"))
TypeableMetaResourcesServiceAccounts = MustNewTypeableMetaResources(MustNewName("service-accounts"))
)
type Typeable interface {

View File

@@ -9,18 +9,11 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/transition"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
var (
TypeableMetaResourceDashboard = authtypes.MustNewTypeableMetaResource(authtypes.MustNewName("dashboard"))
TypeableMetaResourcePublicDashboard = authtypes.MustNewTypeableMetaResource(authtypes.MustNewName("public-dashboard"))
TypeableMetaResourcesDashboards = authtypes.MustNewTypeableMetaResources(authtypes.MustNewName("dashboards"))
)
var (
ErrCodeDashboardInvalidInput = errors.MustNewCode("dashboard_invalid_input")
ErrCodeDashboardNotFound = errors.MustNewCode("dashboard_not_found")