mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-27 14:10:30 +01:00
Compare commits
3 Commits
feat/billi
...
platform-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2cab3d825 | ||
|
|
145587f1b0 | ||
|
|
79e3340292 |
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
200
docs/contributing/go/authz.md
Normal file
200
docs/contributing/go/authz.md
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
19
pkg/authz/authzregistry/dashboard.go
Normal file
19
pkg/authz/authzregistry/dashboard.go
Normal 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),
|
||||
},
|
||||
}
|
||||
}
|
||||
94
pkg/authz/authzregistry/registry.go
Normal file
94
pkg/authz/authzregistry/registry.go
Normal 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 ®istry{
|
||||
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
|
||||
}
|
||||
10
pkg/authz/authzregistry/role.go
Normal file
10
pkg/authz/authzregistry/role.go
Normal 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,
|
||||
}
|
||||
}
|
||||
30
pkg/authz/authzregistry/serviceaccount.go
Normal file
30
pkg/authz/authzregistry/serviceaccount.go
Normal 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),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
29
pkg/modules/serviceaccount/option.go
Normal file
29
pkg/modules/serviceaccount/option.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -195,6 +195,7 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewServiceAccountAuthzactory(sqlstore),
|
||||
sqlmigration.NewDropUserDeletedAtFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewMigrateAWSAllRegionsFactory(sqlstore),
|
||||
sqlmigration.NewAddServiceAccountManagedRoleTransactionsFactory(sqlstore),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
155
pkg/sqlmigration/078_add_sa_managed_role_txn.go
Normal file
155
pkg/sqlmigration/078_add_sa_managed_role_txn.go
Normal 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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user