Compare commits

...

3 Commits

Author SHA1 Message Date
vikrantgupta25
f2cab3d825 chore: regenerate openapi spec for SA FGA routes 2026-04-24 18:14:28 +05:30
vikrantgupta25
145587f1b0 feat(authz): add resource-level FGA for service accounts
- Switch SA routes from AdminAccess to Check middleware
- Add privilege escalation guard in setRole with opts
- Add migration for existing org managed role tuples
- Admin can assign any role, editor/viewer downward only
- WithSkipEscalationGuard for cloud integration paths
2026-04-24 18:05:32 +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
29 changed files with 762 additions and 201 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

@@ -8523,9 +8523,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- service-accounts:list
- tokenizer:
- ADMIN
- service-accounts:list
summary: List service accounts
tags:
- serviceaccount
@@ -8585,9 +8585,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- service-accounts:create
- tokenizer:
- ADMIN
- service-accounts:create
summary: Create service account
tags:
- serviceaccount
@@ -8635,9 +8635,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- service-account:delete
- tokenizer:
- ADMIN
- service-account:delete
summary: Deletes a service account
tags:
- serviceaccount
@@ -8692,9 +8692,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- service-account:read
- tokenizer:
- ADMIN
- service-account:read
summary: Gets a service account
tags:
- serviceaccount
@@ -8752,9 +8752,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- service-account:update
- tokenizer:
- ADMIN
- service-account:update
summary: Updates a service account
tags:
- serviceaccount
@@ -8806,9 +8806,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- service-account:read
- tokenizer:
- ADMIN
- service-account:read
summary: List service account keys
tags:
- serviceaccount
@@ -8874,9 +8874,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- service-account:update
- tokenizer:
- ADMIN
- service-account:update
summary: Create a service account key
tags:
- serviceaccount
@@ -8929,9 +8929,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- service-account:update
- tokenizer:
- ADMIN
- service-account:update
summary: Revoke a service account key
tags:
- serviceaccount
@@ -8994,9 +8994,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- service-account:update
- tokenizer:
- ADMIN
- service-account:update
summary: Updates a service account key
tags:
- serviceaccount
@@ -9055,9 +9055,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- service-account:read
- tokenizer:
- ADMIN
- service-account:read
summary: Gets service account roles
tags:
- serviceaccount
@@ -9117,9 +9117,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- service-account:update
- tokenizer:
- ADMIN
- service-account:update
summary: Create service account role
tags:
- serviceaccount
@@ -9166,9 +9166,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- service-account:update
- tokenizer:
- ADMIN
- service-account:update
summary: Delete service account role
tags:
- serviceaccount

View File

@@ -0,0 +1,200 @@
# 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
- [ ] `make go-build-community` and `make go-build-enterprise` compile
- [ ] `make go-test` passes

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

@@ -472,7 +472,7 @@ func (module *module) getOrCreateAPIKey(ctx context.Context, orgID valuer.UUID,
if err != nil {
return "", err
}
err = module.serviceAccount.SetRoleByName(ctx, orgID, serviceAccount.ID, authtypes.SigNozViewerRoleName)
err = module.serviceAccount.SetRoleByName(ctx, orgID, serviceAccount.ID, authtypes.SigNozViewerRoleName, serviceaccount.WithSkipEscalationGuard())
if err != nil {
return "", err
}

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

@@ -14,6 +14,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/serviceaccounttypes"
@@ -141,7 +142,7 @@ func (ah *APIHandler) getOrCreateCloudIntegrationServiceAccount(ctx context.Cont
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration service account: %w", err))
}
err = ah.Signoz.Modules.ServiceAccount.SetRoleByName(ctx, orgId, cloudIntegrationServiceAccount.ID, authtypes.SigNozViewerRoleName)
err = ah.Signoz.Modules.ServiceAccount.SetRoleByName(ctx, orgId, cloudIntegrationServiceAccount.ID, authtypes.SigNozViewerRoleName, serviceaccount.WithSkipEscalationGuard())
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration service account: %w", err))
}

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

@@ -10,8 +10,38 @@ import (
"github.com/gorilla/mux"
)
var (
serviceAccountAdminRoles = []string{
authtypes.SigNozAdminRoleName,
}
serviceAccountReadRoles = []string{
authtypes.SigNozAdminRoleName,
authtypes.SigNozEditorRoleName,
authtypes.SigNozViewerRoleName,
}
)
func serviceAccountCollectionSelectorCallback(_ *http.Request, _ authtypes.Claims) ([]authtypes.Selector, error) {
return []authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeMetaResources, authtypes.WildCardSelectorString),
}, nil
}
func serviceAccountInstanceSelectorCallback(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
}
func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/service_accounts", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.Create), handler.OpenAPIDef{
if err := router.Handle("/api/v1/service_accounts", handler.New(provider.authZ.Check(provider.serviceAccountHandler.Create, authtypes.RelationCreate, authtypes.TypeableMetaResourcesServiceAccounts, serviceAccountCollectionSelectorCallback, serviceAccountAdminRoles), handler.OpenAPIDef{
ID: "CreateServiceAccount",
Tags: []string{"serviceaccount"},
Summary: "Create service account",
@@ -23,12 +53,12 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{authtypes.TypeableMetaResourcesServiceAccounts.Scope(authtypes.RelationCreate)}),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.List), handler.OpenAPIDef{
if err := router.Handle("/api/v1/service_accounts", handler.New(provider.authZ.Check(provider.serviceAccountHandler.List, authtypes.RelationList, authtypes.TypeableMetaResourcesServiceAccounts, serviceAccountCollectionSelectorCallback, serviceAccountReadRoles), handler.OpenAPIDef{
ID: "ListServiceAccounts",
Tags: []string{"serviceaccount"},
Summary: "List service accounts",
@@ -40,7 +70,7 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{authtypes.TypeableMetaResourcesServiceAccounts.Scope(authtypes.RelationList)}),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -62,7 +92,7 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.Get), handler.OpenAPIDef{
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(provider.authZ.Check(provider.serviceAccountHandler.Get, authtypes.RelationRead, authtypes.TypeableMetaResourceServiceAccount, serviceAccountInstanceSelectorCallback, serviceAccountReadRoles), handler.OpenAPIDef{
ID: "GetServiceAccount",
Tags: []string{"serviceaccount"},
Summary: "Gets a service account",
@@ -74,12 +104,12 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{authtypes.TypeableMetaResourceServiceAccount.Scope(authtypes.RelationRead)}),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}/roles", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.GetRoles), handler.OpenAPIDef{
if err := router.Handle("/api/v1/service_accounts/{id}/roles", handler.New(provider.authZ.Check(provider.serviceAccountHandler.GetRoles, authtypes.RelationRead, authtypes.TypeableMetaResourceServiceAccount, serviceAccountInstanceSelectorCallback, serviceAccountReadRoles), handler.OpenAPIDef{
ID: "GetServiceAccountRoles",
Tags: []string{"serviceaccount"},
Summary: "Gets service account roles",
@@ -91,12 +121,12 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{authtypes.TypeableMetaResourceServiceAccount.Scope(authtypes.RelationRead)}),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}/roles", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.SetRole), handler.OpenAPIDef{
if err := router.Handle("/api/v1/service_accounts/{id}/roles", handler.New(provider.authZ.Check(provider.serviceAccountHandler.SetRole, authtypes.RelationUpdate, authtypes.TypeableMetaResourceServiceAccount, serviceAccountInstanceSelectorCallback, serviceAccountAdminRoles), handler.OpenAPIDef{
ID: "CreateServiceAccountRole",
Tags: []string{"serviceaccount"},
Summary: "Create service account role",
@@ -108,12 +138,12 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{authtypes.TypeableMetaResourceServiceAccount.Scope(authtypes.RelationUpdate)}),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}/roles/{rid}", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.DeleteRole), handler.OpenAPIDef{
if err := router.Handle("/api/v1/service_accounts/{id}/roles/{rid}", handler.New(provider.authZ.Check(provider.serviceAccountHandler.DeleteRole, authtypes.RelationUpdate, authtypes.TypeableMetaResourceServiceAccount, serviceAccountInstanceSelectorCallback, serviceAccountAdminRoles), handler.OpenAPIDef{
ID: "DeleteServiceAccountRole",
Tags: []string{"serviceaccount"},
Summary: "Delete service account role",
@@ -125,7 +155,7 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{authtypes.TypeableMetaResourceServiceAccount.Scope(authtypes.RelationUpdate)}),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
@@ -147,7 +177,7 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.Update), handler.OpenAPIDef{
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(provider.authZ.Check(provider.serviceAccountHandler.Update, authtypes.RelationUpdate, authtypes.TypeableMetaResourceServiceAccount, serviceAccountInstanceSelectorCallback, serviceAccountAdminRoles), handler.OpenAPIDef{
ID: "UpdateServiceAccount",
Tags: []string{"serviceaccount"},
Summary: "Updates a service account",
@@ -159,12 +189,12 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{authtypes.TypeableMetaResourceServiceAccount.Scope(authtypes.RelationUpdate)}),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.Delete), handler.OpenAPIDef{
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(provider.authZ.Check(provider.serviceAccountHandler.Delete, authtypes.RelationDelete, authtypes.TypeableMetaResourceServiceAccount, serviceAccountInstanceSelectorCallback, serviceAccountAdminRoles), handler.OpenAPIDef{
ID: "DeleteServiceAccount",
Tags: []string{"serviceaccount"},
Summary: "Deletes a service account",
@@ -176,12 +206,12 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{authtypes.TypeableMetaResourceServiceAccount.Scope(authtypes.RelationDelete)}),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}/keys", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.CreateFactorAPIKey), handler.OpenAPIDef{
if err := router.Handle("/api/v1/service_accounts/{id}/keys", handler.New(provider.authZ.Check(provider.serviceAccountHandler.CreateFactorAPIKey, authtypes.RelationUpdate, authtypes.TypeableMetaResourceServiceAccount, serviceAccountInstanceSelectorCallback, serviceAccountAdminRoles), handler.OpenAPIDef{
ID: "CreateServiceAccountKey",
Tags: []string{"serviceaccount"},
Summary: "Create a service account key",
@@ -193,12 +223,12 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{authtypes.TypeableMetaResourceServiceAccount.Scope(authtypes.RelationUpdate)}),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}/keys", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.ListFactorAPIKey), handler.OpenAPIDef{
if err := router.Handle("/api/v1/service_accounts/{id}/keys", handler.New(provider.authZ.Check(provider.serviceAccountHandler.ListFactorAPIKey, authtypes.RelationRead, authtypes.TypeableMetaResourceServiceAccount, serviceAccountInstanceSelectorCallback, serviceAccountReadRoles), handler.OpenAPIDef{
ID: "ListServiceAccountKeys",
Tags: []string{"serviceaccount"},
Summary: "List service account keys",
@@ -210,12 +240,12 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{authtypes.TypeableMetaResourceServiceAccount.Scope(authtypes.RelationRead)}),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}/keys/{fid}", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.UpdateFactorAPIKey), handler.OpenAPIDef{
if err := router.Handle("/api/v1/service_accounts/{id}/keys/{fid}", handler.New(provider.authZ.Check(provider.serviceAccountHandler.UpdateFactorAPIKey, authtypes.RelationUpdate, authtypes.TypeableMetaResourceServiceAccount, serviceAccountInstanceSelectorCallback, serviceAccountAdminRoles), handler.OpenAPIDef{
ID: "UpdateServiceAccountKey",
Tags: []string{"serviceaccount"},
Summary: "Updates a service account key",
@@ -227,12 +257,12 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{authtypes.TypeableMetaResourceServiceAccount.Scope(authtypes.RelationUpdate)}),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}/keys/{fid}", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.RevokeFactorAPIKey), handler.OpenAPIDef{
if err := router.Handle("/api/v1/service_accounts/{id}/keys/{fid}", handler.New(provider.authZ.Check(provider.serviceAccountHandler.RevokeFactorAPIKey, authtypes.RelationUpdate, authtypes.TypeableMetaResourceServiceAccount, serviceAccountInstanceSelectorCallback, serviceAccountAdminRoles), handler.OpenAPIDef{
ID: "RevokeServiceAccountKey",
Tags: []string{"serviceaccount"},
Summary: "Revoke a service account key",
@@ -244,7 +274,7 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{authtypes.TypeableMetaResourceServiceAccount.Scope(authtypes.RelationUpdate)}),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}

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

@@ -110,22 +110,22 @@ func (module *module) Update(ctx context.Context, orgID valuer.UUID, input *serv
return nil
}
func (module *module) SetRole(ctx context.Context, orgID valuer.UUID, id valuer.UUID, roleID valuer.UUID) error {
func (module *module) SetRole(ctx context.Context, orgID valuer.UUID, id valuer.UUID, roleID valuer.UUID, opts ...serviceaccount.SetRoleOption) error {
role, err := module.authz.Get(ctx, orgID, roleID)
if err != nil {
return err
}
return module.setRole(ctx, orgID, id, role)
return module.setRole(ctx, orgID, id, role, opts...)
}
func (module *module) SetRoleByName(ctx context.Context, orgID valuer.UUID, id valuer.UUID, name string) error {
func (module *module) SetRoleByName(ctx context.Context, orgID valuer.UUID, id valuer.UUID, name string, opts ...serviceaccount.SetRoleOption) error {
role, err := module.authz.GetByOrgIDAndName(ctx, orgID, name)
if err != nil {
return err
}
return module.setRole(ctx, orgID, id, role)
return module.setRole(ctx, orgID, id, role, opts...)
}
func (module *module) DeleteRole(ctx context.Context, orgID valuer.UUID, id valuer.UUID, roleID valuer.UUID) error {
@@ -375,7 +375,63 @@ func (module *module) getOrGetSetIdentity(ctx context.Context, serviceAccountID
return identity, nil
}
func (module *module) setRole(ctx context.Context, orgID valuer.UUID, id valuer.UUID, role *authtypes.Role) error {
// allowedAssignersForRole returns the role selectors that permit assigning the
// given target role. Admin can assign any role. For other managed roles,
// each level can assign itself and below. For custom roles the caller must
// hold admin or the exact role.
func allowedAssignersForRole(targetRole string) []authtypes.Selector {
adminSelector := authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName)
// Admin is always allowed to assign any role.
if targetRole == authtypes.SigNozAdminRoleName {
return []authtypes.Selector{adminSelector}
}
// Managed hierarchy below admin: editor > viewer.
managedBelow := []string{
authtypes.SigNozEditorRoleName,
authtypes.SigNozViewerRoleName,
}
for index, name := range managedBelow {
if name == targetRole {
selectors := []authtypes.Selector{adminSelector}
for _, allowedRole := range managedBelow[:index+1] {
selectors = append(selectors, authtypes.MustNewSelector(authtypes.TypeRole, allowedRole))
}
return selectors
}
}
// Custom role: admin or exact role holder.
return []authtypes.Selector{
adminSelector,
authtypes.MustNewSelector(authtypes.TypeRole, targetRole),
}
}
func (module *module) setRole(ctx context.Context, orgID valuer.UUID, id valuer.UUID, role *authtypes.Role, opts ...serviceaccount.SetRoleOption) error {
options := serviceaccount.NewSetRoleOptions(opts...)
// Verify the caller holds a role that permits assigning the target role.
// For managed roles this follows a downward-assignment model:
// admin can assign admin, editor, viewer
// editor can assign editor, viewer
// viewer can assign viewer
// For custom roles the caller must hold the exact role.
// Skipped for system-internal calls (e.g., cloud integration creating a viewer SA).
if !options.ShouldSkipEscalationGuard() {
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
return err
}
roleSelectors := allowedAssignersForRole(role.Name)
err = module.authz.CheckWithTupleCreation(ctx, claims, orgID, authtypes.RelationAssignee, authtypes.TypeableRole, roleSelectors, roleSelectors)
if err != nil {
return errors.Newf(errors.TypeForbidden, authtypes.ErrCodeAuthZForbidden, "caller does not hold a role that permits assigning %q", role.Name)
}
}
serviceAccount, err := module.GetWithRoles(ctx, orgID, id)
if err != nil {
return err
@@ -430,3 +486,4 @@ func apiKeyCacheKey(apiKey string) string {
func identityCacheKey(serviceAccountID valuer.UUID) string {
return "identity::" + serviceAccountID.String()
}

View File

@@ -0,0 +1,29 @@
package serviceaccount
type setRoleOptions struct {
skipEscalationGuard bool
}
type SetRoleOption func(*setRoleOptions)
func WithSkipEscalationGuard() SetRoleOption {
return func(o *setRoleOptions) {
o.skipEscalationGuard = true
}
}
func NewSetRoleOptions(opts ...SetRoleOption) *setRoleOptions {
o := &setRoleOptions{
skipEscalationGuard: false,
}
for _, opt := range opts {
opt(o)
}
return o
}
func (o *setRoleOptions) ShouldSkipEscalationGuard() bool {
return o.skipEscalationGuard
}

View File

@@ -36,10 +36,10 @@ type Module interface {
Update(context.Context, valuer.UUID, *serviceaccounttypes.ServiceAccount) error
// Assign a role to the service account. this is safe to retry
SetRole(context.Context, valuer.UUID, valuer.UUID, valuer.UUID) error
SetRole(context.Context, valuer.UUID, valuer.UUID, valuer.UUID, ...SetRoleOption) error
// Assigns a role by name to service account, this is safe to retry
SetRoleByName(context.Context, valuer.UUID, valuer.UUID, string) error
SetRoleByName(context.Context, valuer.UUID, valuer.UUID, string, ...SetRoleOption) error
// Revokes a role from service account, this is safe to retry
DeleteRole(context.Context, valuer.UUID, valuer.UUID, valuer.UUID) error

View File

@@ -195,6 +195,7 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewServiceAccountAuthzactory(sqlstore),
sqlmigration.NewDropUserDeletedAtFactory(sqlstore, sqlschema),
sqlmigration.NewMigrateAWSAllRegionsFactory(sqlstore),
sqlmigration.NewAddServiceAccountManagedRoleTransactionsFactory(sqlstore),
)
}

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

@@ -0,0 +1,155 @@
package sqlmigration
import (
"context"
"time"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/oklog/ulid/v2"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
"github.com/uptrace/bun/migrate"
)
type addServiceAccountManagedRoleTransactions struct {
sqlstore sqlstore.SQLStore
}
func NewAddServiceAccountManagedRoleTransactionsFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("add_sa_managed_role_txn"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &addServiceAccountManagedRoleTransactions{sqlstore: sqlstore}, nil
})
}
func (migration *addServiceAccountManagedRoleTransactions) Register(migrations *migrate.Migrations) error {
return migrations.Register(migration.Up, migration.Down)
}
// managedRoleTuple describes a single FGA tuple to insert for a managed role.
type managedRoleTuple struct {
roleName string
objectType string // "metaresources" or "metaresource"
objectName string // "service-accounts" or "service-account"
relation string // "create", "list", "read", "update", "delete"
}
func (migration *addServiceAccountManagedRoleTransactions) Up(ctx context.Context, db *bun.DB) error {
// All tuples that need to be created for service account FGA managed role permissions.
tuples := []managedRoleTuple{
// signoz-admin: full access
{authtypes.SigNozAdminRoleName, "metaresources", "service-accounts", "create"},
{authtypes.SigNozAdminRoleName, "metaresources", "service-accounts", "list"},
{authtypes.SigNozAdminRoleName, "metaresource", "service-account", "read"},
{authtypes.SigNozAdminRoleName, "metaresource", "service-account", "update"},
{authtypes.SigNozAdminRoleName, "metaresource", "service-account", "delete"},
// signoz-editor: list + read
{authtypes.SigNozEditorRoleName, "metaresources", "service-accounts", "list"},
{authtypes.SigNozEditorRoleName, "metaresource", "service-account", "read"},
// signoz-viewer: list + read
{authtypes.SigNozViewerRoleName, "metaresources", "service-accounts", "list"},
{authtypes.SigNozViewerRoleName, "metaresource", "service-account", "read"},
}
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
}
// Fetch all orgs.
var orgIDs []string
rows, err := tx.QueryContext(ctx, `SELECT id FROM organizations`)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var orgID string
if err := rows.Scan(&orgID); err != nil {
return err
}
orgIDs = append(orgIDs, orgID)
}
isPG := migration.sqlstore.BunDB().Dialect().Name() == dialect.PG
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, "TUPLE_OPERATION_WRITE", 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 *addServiceAccountManagedRoleTransactions) Down(context.Context, *bun.DB) error {
return nil
}

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")