Compare commits

..

1 Commits

Author SHA1 Message Date
Vinícius Lourenço
ad95dc0eb6 ci(golang): upgrade to v6 & bump go to v1.25 2026-05-05 12:18:42 -03:00
155 changed files with 2303 additions and 3821 deletions

1
.github/CODEOWNERS vendored
View File

@@ -107,7 +107,6 @@ go.mod @therealpandey
/pkg/modules/organization/ @therealpandey
/pkg/modules/authdomain/ @therealpandey
/pkg/modules/role/ @therealpandey
/pkg/types/coretypes/ @therealpandey @vikrantgupta25
# IdentN Owners

View File

@@ -62,7 +62,7 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
GO_VERSION: 1.24
GO_VERSION: 1.25
GO_NAME: signoz-community
GO_INPUT_ARTIFACT_CACHE_KEY: community-jsbuild-${{ github.sha }}
GO_INPUT_ARTIFACT_PATH: frontend/build

View File

@@ -95,7 +95,7 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
GO_VERSION: 1.24
GO_VERSION: 1.25
GO_INPUT_ARTIFACT_CACHE_KEY: enterprise-jsbuild-${{ github.sha }}
GO_INPUT_ARTIFACT_PATH: frontend/build
GO_BUILD_CONTEXT: ./cmd/enterprise

View File

@@ -94,7 +94,7 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
GO_VERSION: 1.24
GO_VERSION: 1.25
GO_INPUT_ARTIFACT_CACHE_KEY: staging-jsbuild-${{ github.sha }}
GO_INPUT_ARTIFACT_PATH: frontend/build
GO_BUILD_CONTEXT: ./cmd/enterprise

View File

@@ -22,7 +22,7 @@ jobs:
with:
PRIMUS_REF: main
GO_TEST_CONTEXT: ./...
GO_VERSION: 1.24
GO_VERSION: 1.25
fmt:
if: |
github.event_name == 'merge_group' ||
@@ -32,7 +32,7 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
GO_VERSION: 1.24
GO_VERSION: 1.25
lint:
if: |
github.event_name == 'merge_group' ||
@@ -42,7 +42,7 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
GO_VERSION: 1.24
GO_VERSION: 1.25
deps:
if: |
github.event_name == 'merge_group' ||
@@ -52,7 +52,7 @@ jobs:
secrets: inherit
with:
PRIMUS_REF: main
GO_VERSION: 1.24
GO_VERSION: 1.25
build:
if: |
github.event_name == 'merge_group' ||
@@ -63,9 +63,9 @@ jobs:
- name: self-checkout
uses: actions/checkout@v4
- name: go-install
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: "1.24"
go-version: "1.25"
- name: qemu-install
uses: docker/setup-qemu-action@v3
- name: aarch64-install
@@ -95,27 +95,10 @@ jobs:
- name: self-checkout
uses: actions/checkout@v4
- name: go-install
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: "1.24"
go-version: "1.25"
- name: generate-openapi
run: |
go run cmd/enterprise/*.go generate openapi
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in openapi spec. Run go run cmd/enterprise/*.go generate openapi locally and commit."; exit 1)
authz:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
steps:
- name: self-checkout
uses: actions/checkout@v4
- name: go-install
uses: actions/setup-go@v5
with:
go-version: "1.24"
- name: generate-authz
run: |
go run cmd/enterprise/*.go generate authz
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in authz permissions. Run go run cmd/enterprise/*.go generate authz locally and commit."; exit 1)

View File

@@ -21,7 +21,7 @@ jobs:
with:
fetch-depth: 0
- name: set-up-go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
- name: run-goreleaser
uses: goreleaser/goreleaser-action@v6
with:

View File

@@ -60,9 +60,9 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: setup-go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: "1.24"
go-version: "1.25"
- name: cross-compilation-tools
if: matrix.os == 'ubuntu-latest'
run: |
@@ -124,9 +124,9 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: setup-go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: "1.24"
go-version: "1.25"
# copy the caches from build
- name: get-sha

View File

@@ -76,9 +76,9 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: setup-go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: "1.24"
go-version: "1.25"
- name: cross-compilation-tools
if: matrix.os == 'ubuntu-latest'
run: |
@@ -139,9 +139,9 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: setup-go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: "1.24"
go-version: "1.25"
# copy the caches from build
- name: get-sha

View File

@@ -63,6 +63,46 @@ jobs:
uses: actions/checkout@v4
- name: run
run: bash frontend/scripts/validate-md-languages.sh
authz:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
steps:
- name: self-checkout
uses: actions/checkout@v5
- name: node-install
uses: actions/setup-node@v5
with:
node-version: "22"
- name: deps-install
working-directory: ./frontend
run: |
yarn install
- name: uv-install
uses: astral-sh/setup-uv@v5
- name: uv-deps
working-directory: ./tests/integration
run: |
uv sync
- name: setup-test
run: |
make py-test-setup
- name: generate
working-directory: ./frontend
run: |
yarn generate:permissions-type
- name: teardown-test
if: always()
run: |
make py-test-teardown
- name: validate
run: |
if ! git diff --exit-code frontend/src/hooks/useAuthZ/permissions.type.ts; then
echo "::error::frontend/src/hooks/useAuthZ/permissions.type.ts is out of date. Please run the generator locally and commit the changes: npm run generate:permissions-type (from the frontend directory)"
exit 1
fi
openapi:
if: |
github.event_name == 'merge_group' ||

View File

@@ -1,117 +0,0 @@
package cmd
import (
"bytes"
"context"
"os"
"sort"
"text/template"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/spf13/cobra"
)
const permissionsTypePath = "frontend/src/hooks/useAuthZ/permissions.type.ts"
var permissionsTypeTemplate = template.Must(template.New("permissions").Parse(
`// AUTO GENERATED FILE - DO NOT EDIT - GENERATED BY cmd/enterprise/*.go generate authz
export default {
status: 'success',
data: {
resources: [
{{- range .Resources }}
{
kind: '{{ .Kind }}',
type: '{{ .Type }}',
},
{{- end }}
],
relations: {
{{- range .Relations }}
{{ .Verb }}: [{{ range $i, $t := .Types }}{{ if $i }}, {{ end }}'{{ $t }}'{{ end }}],
{{- end }}
},
},
} as const;
`))
type permissionsTypeRelation struct {
Verb string
Types []string
}
type permissionsTypeResource struct {
Kind string
Type string
}
type permissionsTypeData struct {
Resources []permissionsTypeResource
Relations []permissionsTypeRelation
}
func registerGenerateAuthz(parentCmd *cobra.Command) {
authzCmd := &cobra.Command{
Use: "authz",
Short: "Generate authz permissions for the frontend",
RunE: func(currCmd *cobra.Command, args []string) error {
return runGenerateAuthz(currCmd.Context())
},
}
parentCmd.AddCommand(authzCmd)
}
func runGenerateAuthz(_ context.Context) error {
registry := coretypes.NewRegistry()
allowedResources := map[string]bool{
coretypes.NewResourceRef(coretypes.ResourceServiceAccount).String(): true,
coretypes.NewResourceRef(coretypes.ResourceRole).String(): true,
coretypes.NewResourceRef(coretypes.ResourceMetaResourcesRole).String(): true,
}
allowedTypes := map[string]bool{}
refs := registry.ResourceRefs()
resources := make([]permissionsTypeResource, 0, len(refs))
for _, ref := range refs {
if !allowedResources[ref.String()] {
continue
}
allowedTypes[ref.Type.StringValue()] = true
resources = append(resources, permissionsTypeResource{
Kind: ref.Kind.String(),
Type: ref.Type.StringValue(),
})
}
typesByVerb := registry.TypesByVerb()
verbs := make([]coretypes.Verb, 0, len(typesByVerb))
for verb := range typesByVerb {
verbs = append(verbs, verb)
}
sort.Slice(verbs, func(i, j int) bool { return verbs[i].StringValue() < verbs[j].StringValue() })
relations := make([]permissionsTypeRelation, 0, len(verbs))
for _, verb := range verbs {
types := make([]string, 0, len(typesByVerb[verb]))
for _, t := range typesByVerb[verb] {
if !allowedTypes[t.StringValue()] {
continue
}
types = append(types, t.StringValue())
}
relations = append(relations, permissionsTypeRelation{
Verb: verb.StringValue(),
Types: types,
})
}
var buf bytes.Buffer
if err := permissionsTypeTemplate.Execute(&buf, permissionsTypeData{Resources: resources, Relations: relations}); err != nil {
return err
}
return os.WriteFile(permissionsTypePath, buf.Bytes(), 0o600)
}

View File

@@ -92,13 +92,13 @@ 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, config authz.Config, _ licensing.Licensing, _ []authz.OnBeforeRoleDelete) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
openfgaDataStore, err := openfgaserver.NewSQLStore(sqlstore, config)
func(ctx context.Context, sqlstore sqlstore.SQLStore, _ licensing.Licensing, _ []authz.OnBeforeRoleDelete, _ dashboard.Module) (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, authtypes.NewRegistry()), nil
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore), 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

@@ -137,12 +137,12 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
return authNs, nil
},
func(ctx context.Context, sqlstore sqlstore.SQLStore, config authz.Config, licensing licensing.Licensing, onBeforeRoleDelete []authz.OnBeforeRoleDelete) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
openfgaDataStore, err := openfgaserver.NewSQLStore(sqlstore, config)
func(ctx context.Context, sqlstore sqlstore.SQLStore, licensing licensing.Licensing, onBeforeRoleDelete []authz.OnBeforeRoleDelete, dashboardModule dashboard.Module) (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, authtypes.NewRegistry()), nil
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, licensing, onBeforeRoleDelete, dashboardModule), 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

@@ -16,7 +16,6 @@ func RegisterGenerate(parentCmd *cobra.Command, logger *slog.Logger) {
}
registerGenerateOpenAPI(generateCmd)
registerGenerateAuthz(generateCmd)
parentCmd.AddCommand(generateCmd)
}

View File

@@ -428,4 +428,4 @@ authz:
provider: openfga
openfga:
# maximum tuples allowed per openfga write operation.
max_tuples_per_write: 300
max_tuples_per_write: 100

View File

@@ -190,7 +190,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.122.0
image: signoz/signoz:v0.121.1
ports:
- "8080:8080" # signoz port
# - "6060:6060" # pprof port

View File

@@ -117,7 +117,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.122.0
image: signoz/signoz:v0.121.1
ports:
- "8080:8080" # signoz port
volumes:

View File

@@ -181,7 +181,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.122.0}
image: signoz/signoz:${VERSION:-v0.121.1}
container_name: signoz
ports:
- "8080:8080" # signoz port

View File

@@ -109,7 +109,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.122.0}
image: signoz/signoz:${VERSION:-v0.121.1}
container_name: signoz
ports:
- "8080:8080" # signoz port

View File

@@ -321,6 +321,35 @@ components:
required:
- id
type: object
AuthtypesGettableObjects:
properties:
resource:
$ref: '#/components/schemas/AuthtypesResource'
selectors:
items:
type: string
type: array
required:
- resource
- selectors
type: object
AuthtypesGettableResources:
properties:
relations:
additionalProperties:
items:
type: string
type: array
nullable: true
type: object
resources:
items:
$ref: '#/components/schemas/AuthtypesResource'
type: array
required:
- resources
- relations
type: object
AuthtypesGettableToken:
properties:
accessToken:
@@ -337,9 +366,9 @@ components:
authorized:
type: boolean
object:
$ref: '#/components/schemas/CoretypesObject'
$ref: '#/components/schemas/AuthtypesObject'
relation:
$ref: '#/components/schemas/AuthtypesRelation'
type: string
required:
- relation
- object
@@ -387,6 +416,16 @@ components:
issuerAlias:
type: string
type: object
AuthtypesObject:
properties:
resource:
$ref: '#/components/schemas/AuthtypesResource'
selector:
type: string
required:
- resource
- selector
type: object
AuthtypesOrgSessionContext:
properties:
authNSupport:
@@ -403,6 +442,22 @@ components:
provider:
$ref: '#/components/schemas/AuthtypesAuthNProvider'
type: object
AuthtypesPatchableObjects:
properties:
additions:
items:
$ref: '#/components/schemas/AuthtypesGettableObjects'
nullable: true
type: array
deletions:
items:
$ref: '#/components/schemas/AuthtypesGettableObjects'
nullable: true
type: array
required:
- additions
- deletions
type: object
AuthtypesPatchableRole:
properties:
description:
@@ -440,15 +495,16 @@ components:
refreshToken:
type: string
type: object
AuthtypesRelation:
enum:
- create
- read
- update
- delete
- list
- assignee
type: string
AuthtypesResource:
properties:
name:
type: string
type:
type: string
required:
- name
- type
type: object
AuthtypesRole:
properties:
createdAt:
@@ -512,9 +568,9 @@ components:
AuthtypesTransaction:
properties:
object:
$ref: '#/components/schemas/CoretypesObject'
$ref: '#/components/schemas/AuthtypesObject'
relation:
$ref: '#/components/schemas/AuthtypesRelation'
type: string
required:
- relation
- object
@@ -2149,64 +2205,6 @@ components:
to_user:
type: string
type: object
CoretypesObject:
properties:
resource:
$ref: '#/components/schemas/CoretypesResourceRef'
selector:
type: string
required:
- resource
- selector
type: object
CoretypesObjectGroup:
properties:
resource:
$ref: '#/components/schemas/CoretypesResourceRef'
selectors:
items:
type: string
type: array
required:
- resource
- selectors
type: object
CoretypesPatchableObjects:
properties:
additions:
items:
$ref: '#/components/schemas/CoretypesObjectGroup'
nullable: true
type: array
deletions:
items:
$ref: '#/components/schemas/CoretypesObjectGroup'
nullable: true
type: array
required:
- additions
- deletions
type: object
CoretypesResourceRef:
properties:
kind:
type: string
type:
$ref: '#/components/schemas/CoretypesType'
required:
- type
- kind
type: object
CoretypesType:
enum:
- user
- serviceaccount
- anonymous
- role
- organization
- metaresource
- metaresources
type: string
DashboardtypesDashboard:
properties:
createdAt:
@@ -5706,6 +5704,35 @@ paths:
summary: Check permissions
tags:
- authz
/api/v1/authz/resources:
get:
deprecated: false
description: Gets all the available resources
operationId: AuthzResources
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/AuthtypesGettableResources'
status:
type: string
required:
- status
- data
type: object
description: OK
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
summary: Get resources
tags:
- authz
/api/v1/channels:
get:
deprecated: false
@@ -8929,7 +8956,7 @@ paths:
properties:
data:
items:
$ref: '#/components/schemas/CoretypesObjectGroup'
$ref: '#/components/schemas/AuthtypesGettableObjects'
type: array
status:
type: string
@@ -9002,7 +9029,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/CoretypesPatchableObjects'
$ref: '#/components/schemas/AuthtypesPatchableObjects'
responses:
"204":
content:

View File

@@ -2,6 +2,7 @@ package openfgaauthz
import (
"context"
"slices"
"github.com/SigNoz/signoz/ee/authz/openfgaserver"
"github.com/SigNoz/signoz/pkg/authz"
@@ -12,7 +13,6 @@ import (
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/valuer"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
openfgapkgtransformer "github.com/openfga/language/pkg/go/transformer"
@@ -25,19 +25,19 @@ type provider struct {
openfgaServer *openfgaserver.Server
licensing licensing.Licensing
store authtypes.RoleStore
registry *authtypes.Registry
registry []authz.RegisterTypeable
settings factory.ScopedProviderSettings
onBeforeRoleDelete []authz.OnBeforeRoleDelete
}
func NewProviderFactory(sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, licensing licensing.Licensing, onBeforeRoleDelete []authz.OnBeforeRoleDelete, registry *authtypes.Registry) 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.RegisterTypeable) 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 *authtypes.Registry) (authz.AuthZ, error) {
pkgOpenfgaAuthzProvider := pkgopenfgaauthz.NewProviderFactory(sqlstore, openfgaSchema, openfgaDataStore, 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)
pkgAuthzService, err := pkgOpenfgaAuthzProvider.New(ctx, settings, config)
if err != nil {
return nil, err
@@ -74,11 +74,11 @@ func (provider *provider) Stop(ctx context.Context) error {
return provider.openfgaServer.Stop(ctx)
}
func (provider *provider) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, relation authtypes.Relation, typeable coretypes.Resource, selectors []coretypes.Selector, roleSelectors []coretypes.Selector) error {
func (provider *provider) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, relation authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector, roleSelectors []authtypes.Selector) error {
return provider.openfgaServer.CheckWithTupleCreation(ctx, claims, orgID, relation, typeable, selectors, roleSelectors)
}
func (provider *provider) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, relation authtypes.Relation, typeable coretypes.Resource, selectors []coretypes.Selector, roleSelectors []coretypes.Selector) error {
func (provider *provider) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, relation authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector, roleSelectors []authtypes.Selector) error {
return provider.openfgaServer.CheckWithTupleCreationWithoutClaims(ctx, orgID, relation, typeable, selectors, roleSelectors)
}
@@ -108,7 +108,7 @@ func (provider *provider) CheckTransactions(ctx context.Context, subject string,
return results, nil
}
func (provider *provider) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType coretypes.Type) ([]*coretypes.Object, error) {
func (provider *provider) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType authtypes.Type) ([]*authtypes.Object, error) {
return provider.openfgaServer.ListObjects(ctx, subject, relation, objectType)
}
@@ -159,10 +159,16 @@ func (provider *provider) CreateManagedRoles(ctx context.Context, orgID valuer.U
func (provider *provider) CreateManagedUserRoleTransactions(ctx context.Context, orgID valuer.UUID, userID valuer.UUID) error {
tuples := make([]*openfgav1.TupleKey, 0)
grantTuples := provider.getManagedRoleGrantTuples(orgID, userID)
grantTuples, err := provider.getManagedRoleGrantTuples(orgID, userID)
if err != nil {
return err
}
tuples = append(tuples, grantTuples...)
managedRoleTuples := provider.getManagedRoleTransactionTuples(orgID)
managedRoleTuples, err := provider.getManagedRoleTransactionTuples(orgID)
if err != nil {
return err
}
tuples = append(tuples, managedRoleTuples...)
return provider.Write(ctx, tuples, nil)
@@ -202,7 +208,21 @@ func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, ro
return role, nil
}
func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation) ([]*coretypes.Object, error) {
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() {
resources = append(resources, &authtypes.Resource{Name: typeable.Name(), Type: typeable.Type()})
}
return resources
}
func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation) ([]*authtypes.Object, error) {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
@@ -213,16 +233,16 @@ func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id
return nil, err
}
objects := make([]*coretypes.Object, 0)
for _, objectType := range provider.registry.Types() {
if coretypes.ErrIfVerbNotValidForType(relation.Verb, objectType) != nil {
objects := make([]*authtypes.Object, 0)
for _, objectType := range provider.getUniqueTypes() {
if !slices.Contains(authtypes.TypeableRelations[objectType], relation) {
continue
}
resourceObjects, err := provider.
ListObjects(
ctx,
authtypes.MustNewSubject(coretypes.NewResourceRole(), storableRole.Name, orgID, &coretypes.VerbAssignee),
authtypes.MustNewSubject(authtypes.TypeableRole, storableRole.Name, orgID, &authtypes.RelationAssignee),
relation,
objectType,
)
@@ -245,7 +265,7 @@ func (provider *provider) Patch(ctx context.Context, orgID valuer.UUID, role *au
return provider.store.Update(ctx, orgID, role)
}
func (provider *provider) PatchObjects(ctx context.Context, orgID valuer.UUID, name string, relation authtypes.Relation, additions, deletions []*coretypes.Object) error {
func (provider *provider) PatchObjects(ctx context.Context, orgID valuer.UUID, name string, relation authtypes.Relation, additions, deletions []*authtypes.Object) error {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
@@ -298,63 +318,84 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
return provider.store.Delete(ctx, orgID, id)
}
func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID valuer.UUID) []*openfgav1.TupleKey {
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{}
// Grant the admin role to the user
adminSubject := authtypes.MustNewSubject(coretypes.NewResourceUser(), userID.String(), orgID, nil)
adminTuple := authtypes.NewTuples(
coretypes.NewResourceRole(),
adminSubject := authtypes.MustNewSubject(authtypes.TypeableUser, userID.String(), orgID, nil)
adminTuple, err := authtypes.TypeableRole.Tuples(
adminSubject,
authtypes.Relation{Verb: coretypes.VerbAssignee},
[]coretypes.Selector{coretypes.TypeRole.MustSelector(authtypes.SigNozAdminRoleName)},
authtypes.RelationAssignee,
[]authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName),
},
orgID,
)
if err != nil {
return nil, err
}
tuples = append(tuples, adminTuple...)
// Grant the admin role to the anonymous user
anonymousSubject := authtypes.MustNewSubject(coretypes.NewResourceAnonymous(), coretypes.AnonymousUser.String(), orgID, nil)
anonymousTuple := authtypes.NewTuples(
coretypes.NewResourceRole(),
anonymousSubject := authtypes.MustNewSubject(authtypes.TypeableAnonymous, authtypes.AnonymousUser.String(), orgID, nil)
anonymousTuple, err := authtypes.TypeableRole.Tuples(
anonymousSubject,
authtypes.Relation{Verb: coretypes.VerbAssignee},
[]coretypes.Selector{coretypes.TypeRole.MustSelector(authtypes.SigNozAnonymousRoleName)},
authtypes.RelationAssignee,
[]authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAnonymousRoleName),
},
orgID,
)
if err != nil {
return nil, err
}
tuples = append(tuples, anonymousTuple...)
return tuples
return tuples, nil
}
func (provider *provider) getManagedRoleTransactionTuples(orgID valuer.UUID) []*openfgav1.TupleKey {
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 provider.registry.ManagedRoleTransactions() {
for roleName, transactions := range transactionsByRole {
for _, txn := range transactions {
resource := coretypes.MustNewResourceFromTypeAndKind(txn.Object.Resource.Type, txn.Object.Resource.Kind)
txnTuples := authtypes.NewTuples(
resource,
typeable := authtypes.MustNewTypeableFromType(txn.Object.Resource.Type, txn.Object.Resource.Name)
txnTuples, err := typeable.Tuples(
authtypes.MustNewSubject(
coretypes.NewResourceRole(),
authtypes.TypeableRole,
roleName,
orgID,
&coretypes.VerbAssignee,
&authtypes.RelationAssignee,
),
txn.Relation,
[]coretypes.Selector{txn.Object.Selector},
[]authtypes.Selector{txn.Object.Selector},
orgID,
)
if err != nil {
return nil, err
}
tuples = append(tuples, txnTuples...)
}
}
return tuples
return tuples, nil
}
func (provider *provider) deleteTuples(ctx context.Context, roleName string, orgID valuer.UUID) error {
subject := authtypes.MustNewSubject(coretypes.NewResourceRole(), roleName, orgID, &coretypes.VerbAssignee)
subject := authtypes.MustNewSubject(authtypes.TypeableRole, roleName, orgID, &authtypes.RelationAssignee)
tuples := make([]*openfgav1.TupleKey, 0)
for _, objectType := range provider.registry.Types() {
for _, objectType := range provider.getUniqueTypes() {
typeTuples, err := provider.ReadTuples(ctx, &openfgav1.ReadRequestTupleKey{
User: subject,
Object: objectType.StringValue() + ":",
@@ -383,3 +424,28 @@ 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

@@ -1,6 +1,6 @@
module base
type organization
type organisation
relations
define read: [user, serviceaccount, role#assignee]
define update: [user, serviceaccount, role#assignee]
@@ -10,14 +10,12 @@ type user
define read: [user, serviceaccount, role#assignee]
define update: [user, serviceaccount, role#assignee]
define delete: [user, serviceaccount, role#assignee]
define attach: [user, serviceaccount, role#assignee]
type serviceaccount
type serviceaccount
relations
define read: [user, serviceaccount, role#assignee]
define update: [user, serviceaccount, role#assignee]
define delete: [user, serviceaccount, role#assignee]
define attach: [user, serviceaccount, role#assignee]
define delete: [user, serviceaccount, role#assignee]
type anonymous
@@ -28,7 +26,6 @@ type role
define read: [user, serviceaccount, role#assignee]
define update: [user, serviceaccount, role#assignee]
define delete: [user, serviceaccount, role#assignee]
define attach: [user, serviceaccount, role#assignee]
type metaresources
relations

View File

@@ -7,7 +7,6 @@ import (
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/valuer"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
)
@@ -34,18 +33,18 @@ func (server *Server) Stop(ctx context.Context) error {
return server.pkgAuthzService.Stop(ctx)
}
func (server *Server) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, relation authtypes.Relation, typeable coretypes.Resource, selectors []coretypes.Selector, _ []coretypes.Selector) error {
func (server *Server) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, relation authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector, _ []authtypes.Selector) error {
subject := ""
switch claims.Principal {
case authtypes.PrincipalUser:
user, err := authtypes.NewSubject(coretypes.NewResourceUser(), claims.UserID, orgID, nil)
user, err := authtypes.NewSubject(authtypes.TypeableUser, claims.UserID, orgID, nil)
if err != nil {
return err
}
subject = user
case authtypes.PrincipalServiceAccount:
serviceAccount, err := authtypes.NewSubject(coretypes.NewResourceServiceAccount(), claims.ServiceAccountID, orgID, nil)
serviceAccount, err := authtypes.NewSubject(authtypes.TypeableServiceAccount, claims.ServiceAccountID, orgID, nil)
if err != nil {
return err
}
@@ -53,7 +52,10 @@ func (server *Server) CheckWithTupleCreation(ctx context.Context, claims authtyp
subject = serviceAccount
}
tupleSlice := authtypes.NewTuples(typeable, subject, relation, selectors, orgID)
tupleSlice, err := typeable.Tuples(subject, relation, selectors, orgID)
if err != nil {
return err
}
tuples := make(map[string]*openfgav1.TupleKey, len(tupleSlice))
for idx, tuple := range tupleSlice {
@@ -74,13 +76,16 @@ func (server *Server) CheckWithTupleCreation(ctx context.Context, claims authtyp
return errors.Newf(errors.TypeForbidden, authtypes.ErrCodeAuthZForbidden, "subjects are not authorized for requested access")
}
func (server *Server) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, relation authtypes.Relation, typeable coretypes.Resource, selectors []coretypes.Selector, _ []coretypes.Selector) error {
subject, err := authtypes.NewSubject(coretypes.NewResourceAnonymous(), coretypes.AnonymousUser.String(), orgID, nil)
func (server *Server) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, relation authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector, _ []authtypes.Selector) error {
subject, err := authtypes.NewSubject(authtypes.TypeableAnonymous, authtypes.AnonymousUser.String(), orgID, nil)
if err != nil {
return err
}
tupleSlice := authtypes.NewTuples(typeable, subject, relation, selectors, orgID)
tupleSlice, err := typeable.Tuples(subject, relation, selectors, orgID)
if err != nil {
return err
}
tuples := make(map[string]*openfgav1.TupleKey, len(tupleSlice))
for idx, tuple := range tupleSlice {
@@ -105,7 +110,7 @@ func (server *Server) BatchCheck(ctx context.Context, tupleReq map[string]*openf
return server.pkgAuthzService.BatchCheck(ctx, tupleReq)
}
func (server *Server) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType coretypes.Type) ([]*coretypes.Object, error) {
func (server *Server) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType authtypes.Type) ([]*authtypes.Object, error) {
return server.pkgAuthzService.ListObjects(ctx, subject, relation, objectType)
}

View File

@@ -2,7 +2,6 @@ package openfgaserver
import (
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/openfga/openfga/pkg/storage"
@@ -11,11 +10,11 @@ import (
"github.com/openfga/openfga/pkg/storage/sqlite"
)
func NewSQLStore(store sqlstore.SQLStore, config authz.Config) (storage.OpenFGADatastore, error) {
func NewSQLStore(store sqlstore.SQLStore) (storage.OpenFGADatastore, error) {
switch store.BunDB().Dialect().Name().String() {
case "sqlite":
return sqlite.NewWithDB(store.SQLDB(), &sqlcommon.Config{
MaxTuplesPerWriteField: config.OpenFGA.MaxTuplesPerWrite,
MaxTuplesPerWriteField: 100,
MaxTypesPerModelField: 100,
})
case "pg":
@@ -25,7 +24,7 @@ func NewSQLStore(store sqlstore.SQLStore, config authz.Config) (storage.OpenFGAD
}
return postgres.NewWithDB(pgStore.Pool(), nil, &sqlcommon.Config{
MaxTuplesPerWriteField: config.OpenFGA.MaxTuplesPerWrite,
MaxTuplesPerWriteField: 100,
MaxTypesPerModelField: 100,
})
}

View File

@@ -14,7 +14,7 @@ import (
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
@@ -88,7 +88,7 @@ func (module *module) GetDashboardByPublicID(ctx context.Context, id valuer.UUID
return dashboardtypes.NewDashboardFromStorableDashboard(storableDashboard), nil
}
func (module *module) GetPublicDashboardSelectorsAndOrg(ctx context.Context, id valuer.UUID, orgs []*types.Organization) ([]coretypes.Selector, valuer.UUID, error) {
func (module *module) GetPublicDashboardSelectorsAndOrg(ctx context.Context, id valuer.UUID, orgs []*types.Organization) ([]authtypes.Selector, valuer.UUID, error) {
orgIDs := make([]string, len(orgs))
for idx, org := range orgs {
orgIDs[idx] = org.ID.StringValue()
@@ -99,9 +99,9 @@ func (module *module) GetPublicDashboardSelectorsAndOrg(ctx context.Context, id
return nil, valuer.UUID{}, err
}
return []coretypes.Selector{
coretypes.TypeMetaResource.MustSelector(id.StringValue()),
coretypes.TypeMetaResource.MustSelector(coretypes.WildCardSelectorString),
return []authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeMetaResource, id.StringValue()),
authtypes.MustNewSelector(authtypes.TypeMetaResource, authtypes.WildCardSelectorString),
}, storableDashboard.OrgID, nil
}
@@ -217,6 +217,28 @@ 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

@@ -6,8 +6,6 @@ import (
"io"
"net/http"
"log/slog"
"github.com/SigNoz/signoz/ee/query-service/anomaly"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
@@ -17,6 +15,7 @@ import (
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"log/slog"
)
func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
@@ -138,3 +137,4 @@ func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
aH.QueryRangeV4(w, r)
}
}

View File

@@ -42,8 +42,8 @@ import (
// Server runs HTTP, Mux and a grpc server
type Server struct {
config signoz.Config
signoz *signoz.SigNoz
config signoz.Config
signoz *signoz.SigNoz
// public http router
httpConn net.Listener
@@ -148,7 +148,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
s := &Server{
config: config,
signoz: signoz,
signoz: signoz,
httpHostPort: baseconst.HTTPHostPort,
unavailableChannel: make(chan healthcheck.Status),
usageManager: usageManager,
@@ -317,3 +317,4 @@ func (s *Server) Stop(ctx context.Context) error {
return nil
}

View File

@@ -23,11 +23,6 @@
"**/*.md",
"**/*.json",
"src/parser/**",
"src/TraceOperator/parser/**",
".claude",
".opencode",
"dist",
"playwright-report",
".temp_cache"
"src/TraceOperator/parser/**"
]
}

View File

@@ -23,7 +23,8 @@
"commitlint": "commitlint --edit $1",
"test": "jest",
"test:changedsince": "jest --changedSince=main --coverage --silent",
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh"
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh",
"generate:permissions-type": "node scripts/generate-permissions-type.cjs"
},
"engines": {
"node": ">=22.0.0"

View File

@@ -0,0 +1,199 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const axios = require('axios');
const PERMISSIONS_TYPE_FILE = path.join(
__dirname,
'../src/hooks/useAuthZ/permissions.type.ts',
);
const SIGNOZ_INTEGRATION_IMAGE = 'signoz:integration';
const LOCAL_BACKEND_URL = 'http://localhost:8080';
function log(message) {
console.log(`[generate-permissions-type] ${message}`);
}
function getBackendUrlFromDocker() {
try {
const output = execSync(
`docker ps --filter "ancestor=${SIGNOZ_INTEGRATION_IMAGE}" --format "{{.Ports}}"`,
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] },
).trim();
if (!output) {
return null;
}
const portMatch = output.match(/0\.0\.0\.0:(\d+)->8080\/tcp/);
if (portMatch) {
return `http://localhost:${portMatch[1]}`;
}
const ipv6Match = output.match(/:::(\d+)->8080\/tcp/);
if (ipv6Match) {
return `http://localhost:${ipv6Match[1]}`;
}
} catch (err) {
log(`Warning: Could not get port from docker: ${err.message}`);
}
return null;
}
async function checkBackendHealth(url, maxAttempts = 3, delayMs = 1000) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
await axios.get(`${url}/api/v1/health`, {
timeout: 5000,
validateStatus: (status) => status === 200,
});
return true;
} catch (err) {
if (attempt < maxAttempts) {
await new Promise((r) => setTimeout(r, delayMs));
}
}
}
return false;
}
async function discoverBackendUrl() {
const dockerUrl = getBackendUrlFromDocker();
if (dockerUrl) {
log(`Found ${SIGNOZ_INTEGRATION_IMAGE} container, trying ${dockerUrl}...`);
if (await checkBackendHealth(dockerUrl)) {
log(`Backend found at ${dockerUrl} (from py-test-setup)`);
return dockerUrl;
}
log(`Backend at ${dockerUrl} is not responding`);
}
log(`Trying local backend at ${LOCAL_BACKEND_URL}...`);
if (await checkBackendHealth(LOCAL_BACKEND_URL)) {
log(`Backend found at ${LOCAL_BACKEND_URL}`);
return LOCAL_BACKEND_URL;
}
return null;
}
async function fetchResources(backendUrl) {
log('Fetching resources from API...');
const resourcesUrl = `${backendUrl}/api/v1/authz/resources`;
const { data: response } = await axios.get(resourcesUrl);
return response;
}
function transformResponse(apiResponse) {
if (!apiResponse.data) {
throw new Error('Invalid API response: missing data field');
}
const { resources, relations } = apiResponse.data;
return {
status: apiResponse.status || 'success',
data: {
resources: resources,
relations: relations,
},
};
}
function generateTypeScriptFile(data) {
const resourcesStr = data.data.resources
.map(
(r) =>
`\t\t\t{\n\t\t\t\tname: '${r.name}',\n\t\t\t\ttype: '${r.type}',\n\t\t\t},`,
)
.join('\n');
const relationsStr = Object.entries(data.data.relations)
.map(
([type, relations]) =>
`\t\t\t${type}: [${relations.map((r) => `'${r}'`).join(', ')}],`,
)
.join('\n');
return `// AUTO GENERATED FILE - DO NOT EDIT - GENERATED BY scripts/generate-permissions-type
export default {
\tstatus: '${data.status}',
\tdata: {
\t\tresources: [
${resourcesStr}
\t\t],
\t\trelations: {
${relationsStr}
\t\t},
\t},
} as const;
`;
}
async function main() {
try {
log('Starting permissions type generation...');
const backendUrl = await discoverBackendUrl();
if (!backendUrl) {
console.error('\n' + '='.repeat(80));
console.error('ERROR: No running SigNoz backend found!');
console.error('='.repeat(80));
console.error(
'\nThe permissions type generator requires a running SigNoz backend.',
);
console.error('\nFor local development, start the backend with:');
console.error(' make go-run-enterprise');
console.error(
'\nFor CI or integration testing, start the test environment with:',
);
console.error(' make py-test-setup');
console.error(
'\nIf running in CI and seeing this error, check that the py-test-setup',
);
console.error('step completed successfully before this step runs.');
console.error('='.repeat(80) + '\n');
process.exit(1);
}
log('Fetching resources...');
const apiResponse = await fetchResources(backendUrl);
log('Transforming response...');
const transformed = transformResponse(apiResponse);
log('Generating TypeScript file...');
const content = generateTypeScriptFile(transformed);
log(`Writing to ${PERMISSIONS_TYPE_FILE}...`);
fs.writeFileSync(PERMISSIONS_TYPE_FILE, content, 'utf8');
const rootDir = path.join(__dirname, '../..');
const relativePath = path.relative(
path.join(rootDir, 'frontend'),
PERMISSIONS_TYPE_FILE,
);
log('Linting generated file...');
execSync(`cd frontend && yarn oxlint ${relativePath}`, {
cwd: rootDir,
stdio: 'inherit',
});
log('Successfully generated permissions.type.ts');
} catch (error) {
log(`Error: ${error.message}`);
process.exit(1);
}
}
if (require.main === module) {
main();
}
module.exports = { main };

View File

@@ -4,16 +4,23 @@
* * regenerate with 'yarn generate:api'
* SigNoz
*/
import { useMutation } from 'react-query';
import { useMutation, useQuery } from 'react-query';
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import type {
AuthtypesTransactionDTO,
AuthzCheck200,
AuthzResources200,
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
@@ -103,3 +110,88 @@ export const useAuthzCheck = <
return useMutation(mutationOptions);
};
/**
* Gets all the available resources
* @summary Get resources
*/
export const authzResources = (signal?: AbortSignal) => {
return GeneratedAPIInstance<AuthzResources200>({
url: `/api/v1/authz/resources`,
method: 'GET',
signal,
});
};
export const getAuthzResourcesQueryKey = () => {
return [`/api/v1/authz/resources`] as const;
};
export const getAuthzResourcesQueryOptions = <
TData = Awaited<ReturnType<typeof authzResources>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof authzResources>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getAuthzResourcesQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof authzResources>>> = ({
signal,
}) => authzResources(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof authzResources>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type AuthzResourcesQueryResult = NonNullable<
Awaited<ReturnType<typeof authzResources>>
>;
export type AuthzResourcesQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get resources
*/
export function useAuthzResources<
TData = Awaited<ReturnType<typeof authzResources>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof authzResources>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getAuthzResourcesQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get resources
*/
export const invalidateAuthzResources = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getAuthzResourcesQueryKey() },
options,
);
return queryClient;
};

View File

@@ -18,9 +18,9 @@ import type {
} from 'react-query';
import type {
AuthtypesPatchableObjectsDTO,
AuthtypesPatchableRoleDTO,
AuthtypesPostableRoleDTO,
CoretypesPatchableObjectsDTO,
CreateRole201,
DeleteRolePathParameters,
GetObjects200,
@@ -571,13 +571,13 @@ export const invalidateGetObjects = async (
*/
export const patchObjects = (
{ id, relation }: PatchObjectsPathParameters,
coretypesPatchableObjectsDTO: BodyType<CoretypesPatchableObjectsDTO>,
authtypesPatchableObjectsDTO: BodyType<AuthtypesPatchableObjectsDTO>,
) => {
return GeneratedAPIInstance<string>({
url: `/api/v1/roles/${id}/relations/${relation}/objects`,
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
data: coretypesPatchableObjectsDTO,
data: authtypesPatchableObjectsDTO,
});
};
@@ -590,7 +590,7 @@ export const getPatchObjectsMutationOptions = <
TError,
{
pathParams: PatchObjectsPathParameters;
data: BodyType<CoretypesPatchableObjectsDTO>;
data: BodyType<AuthtypesPatchableObjectsDTO>;
},
TContext
>;
@@ -599,7 +599,7 @@ export const getPatchObjectsMutationOptions = <
TError,
{
pathParams: PatchObjectsPathParameters;
data: BodyType<CoretypesPatchableObjectsDTO>;
data: BodyType<AuthtypesPatchableObjectsDTO>;
},
TContext
> => {
@@ -616,7 +616,7 @@ export const getPatchObjectsMutationOptions = <
Awaited<ReturnType<typeof patchObjects>>,
{
pathParams: PatchObjectsPathParameters;
data: BodyType<CoretypesPatchableObjectsDTO>;
data: BodyType<AuthtypesPatchableObjectsDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
@@ -630,7 +630,7 @@ export const getPatchObjectsMutationOptions = <
export type PatchObjectsMutationResult = NonNullable<
Awaited<ReturnType<typeof patchObjects>>
>;
export type PatchObjectsMutationBody = BodyType<CoretypesPatchableObjectsDTO>;
export type PatchObjectsMutationBody = BodyType<AuthtypesPatchableObjectsDTO>;
export type PatchObjectsMutationError = ErrorType<RenderErrorResponseDTO>;
/**
@@ -645,7 +645,7 @@ export const usePatchObjects = <
TError,
{
pathParams: PatchObjectsPathParameters;
data: BodyType<CoretypesPatchableObjectsDTO>;
data: BodyType<AuthtypesPatchableObjectsDTO>;
},
TContext
>;
@@ -654,7 +654,7 @@ export const usePatchObjects = <
TError,
{
pathParams: PatchObjectsPathParameters;
data: BodyType<CoretypesPatchableObjectsDTO>;
data: BodyType<AuthtypesPatchableObjectsDTO>;
},
TContext
> => {

View File

@@ -1668,6 +1668,33 @@ export interface AuthtypesGettableAuthDomainDTO {
updatedAt?: Date;
}
export interface AuthtypesGettableObjectsDTO {
resource: AuthtypesResourceDTO;
/**
* @type array
*/
selectors: string[];
}
/**
* @nullable
*/
export type AuthtypesGettableResourcesDTORelations = {
[key: string]: string[];
} | null;
export interface AuthtypesGettableResourcesDTO {
/**
* @type object
* @nullable true
*/
relations: AuthtypesGettableResourcesDTORelations;
/**
* @type array
*/
resources: AuthtypesResourceDTO[];
}
export interface AuthtypesGettableTokenDTO {
/**
* @type string
@@ -1692,8 +1719,11 @@ export interface AuthtypesGettableTransactionDTO {
* @type boolean
*/
authorized: boolean;
object: CoretypesObjectDTO;
relation: AuthtypesRelationDTO;
object: AuthtypesObjectDTO;
/**
* @type string
*/
relation: string;
}
export type AuthtypesGoogleConfigDTODomainToAdminEmail = {
@@ -1767,6 +1797,14 @@ export interface AuthtypesOIDCConfigDTO {
issuerAlias?: string;
}
export interface AuthtypesObjectDTO {
resource: AuthtypesResourceDTO;
/**
* @type string
*/
selector: string;
}
export interface AuthtypesOrgSessionContextDTO {
authNSupport?: AuthtypesAuthNSupportDTO;
/**
@@ -1784,6 +1822,19 @@ export interface AuthtypesPasswordAuthNSupportDTO {
provider?: AuthtypesAuthNProviderDTO;
}
export interface AuthtypesPatchableObjectsDTO {
/**
* @type array
* @nullable true
*/
additions: AuthtypesGettableObjectsDTO[] | null;
/**
* @type array
* @nullable true
*/
deletions: AuthtypesGettableObjectsDTO[] | null;
}
export interface AuthtypesPatchableRoleDTO {
/**
* @type string
@@ -1832,14 +1883,17 @@ export interface AuthtypesPostableRotateTokenDTO {
refreshToken?: string;
}
export enum AuthtypesRelationDTO {
create = 'create',
read = 'read',
update = 'update',
delete = 'delete',
list = 'list',
assignee = 'assignee',
export interface AuthtypesResourceDTO {
/**
* @type string
*/
name: string;
/**
* @type string
*/
type: string;
}
export interface AuthtypesRoleDTO {
/**
* @type string
@@ -1929,8 +1983,11 @@ export interface AuthtypesSessionContextDTO {
}
export interface AuthtypesTransactionDTO {
object: CoretypesObjectDTO;
relation: AuthtypesRelationDTO;
object: AuthtypesObjectDTO;
/**
* @type string
*/
relation: string;
}
export interface AuthtypesUpdatableAuthDomainDTO {
@@ -4116,52 +4173,6 @@ export interface ConfigWechatConfigDTO {
to_user?: string;
}
export interface CoretypesObjectDTO {
resource: CoretypesResourceRefDTO;
/**
* @type string
*/
selector: string;
}
export interface CoretypesObjectGroupDTO {
resource: CoretypesResourceRefDTO;
/**
* @type array
*/
selectors: string[];
}
export interface CoretypesPatchableObjectsDTO {
/**
* @type array
* @nullable true
*/
additions: CoretypesObjectGroupDTO[] | null;
/**
* @type array
* @nullable true
*/
deletions: CoretypesObjectGroupDTO[] | null;
}
export interface CoretypesResourceRefDTO {
/**
* @type string
*/
kind: string;
type: CoretypesTypeDTO;
}
export enum CoretypesTypeDTO {
user = 'user',
serviceaccount = 'serviceaccount',
anonymous = 'anonymous',
role = 'role',
organization = 'organization',
metaresource = 'metaresource',
metaresources = 'metaresources',
}
export interface DashboardtypesDashboardDTO {
/**
* @type string
@@ -8099,6 +8110,14 @@ export type AuthzCheck200 = {
status: string;
};
export type AuthzResources200 = {
data: AuthtypesGettableResourcesDTO;
/**
* @type string
*/
status: string;
};
export type ListChannels200 = {
/**
* @type array
@@ -8724,7 +8743,7 @@ export type GetObjects200 = {
/**
* @type array
*/
data: CoretypesObjectGroupDTO[];
data: AuthtypesGettableObjectsDTO[];
/**
* @type string
*/

View File

@@ -55,7 +55,7 @@ describe('GuardAuthZ', () => {
);
render(
<GuardAuthZ relation="read" object="role:*">
<GuardAuthZ relation="read" object="dashboard:*">
<TestChild />
</GuardAuthZ>,
);
@@ -79,7 +79,7 @@ describe('GuardAuthZ', () => {
render(
<GuardAuthZ
relation="read"
object="role:*"
object="dashboard:*"
fallbackOnLoading={<LoadingFallback />}
>
<TestChild />
@@ -102,7 +102,7 @@ describe('GuardAuthZ', () => {
);
const { container } = render(
<GuardAuthZ relation="read" object="role:*">
<GuardAuthZ relation="read" object="dashboard:*">
<TestChild />
</GuardAuthZ>,
);
@@ -121,7 +121,11 @@ describe('GuardAuthZ', () => {
);
render(
<GuardAuthZ relation="read" object="role:*" fallbackOnError={ErrorFallback}>
<GuardAuthZ
relation="read"
object="dashboard:*"
fallbackOnError={ErrorFallback}
>
<TestChild />
</GuardAuthZ>,
);
@@ -151,7 +155,7 @@ describe('GuardAuthZ', () => {
render(
<GuardAuthZ
relation="read"
object="role:*"
object="dashboard:*"
fallbackOnError={errorFallbackWithCapture}
>
<TestChild />
@@ -174,7 +178,7 @@ describe('GuardAuthZ', () => {
);
const { container } = render(
<GuardAuthZ relation="read" object="role:*">
<GuardAuthZ relation="read" object="dashboard:*">
<TestChild />
</GuardAuthZ>,
);
@@ -197,7 +201,7 @@ describe('GuardAuthZ', () => {
render(
<GuardAuthZ
relation="update"
object="role:123"
object="dashboard:123"
fallbackOnNoPermissions={NoPermissionFallback}
>
<TestChild />
@@ -220,7 +224,7 @@ describe('GuardAuthZ', () => {
);
const { container } = render(
<GuardAuthZ relation="update" object="role:123">
<GuardAuthZ relation="update" object="dashboard:123">
<TestChild />
</GuardAuthZ>,
);
@@ -240,7 +244,7 @@ describe('GuardAuthZ', () => {
);
const { container } = render(
<GuardAuthZ relation="read" object="role:*">
<GuardAuthZ relation="read" object="dashboard:*">
<TestChild />
</GuardAuthZ>,
);
@@ -253,7 +257,7 @@ describe('GuardAuthZ', () => {
});
it('should pass requiredPermissionName to fallbackOnNoPermissions', async () => {
const permission = buildPermission('update', 'role:123');
const permission = buildPermission('update', 'dashboard:123');
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
@@ -265,7 +269,7 @@ describe('GuardAuthZ', () => {
render(
<GuardAuthZ
relation="update"
object="role:123"
object="dashboard:123"
fallbackOnNoPermissions={NoPermissionFallbackWithSuggestions}
>
<TestChild />
@@ -295,7 +299,7 @@ describe('GuardAuthZ', () => {
);
const { rerender } = render(
<GuardAuthZ relation="read" object="role:*">
<GuardAuthZ relation="read" object="dashboard:*">
<TestChild />
</GuardAuthZ>,
);
@@ -305,7 +309,7 @@ describe('GuardAuthZ', () => {
});
rerender(
<GuardAuthZ relation="delete" object="role:456">
<GuardAuthZ relation="delete" object="dashboard:456">
<TestChild />
</GuardAuthZ>,
);

View File

@@ -41,7 +41,11 @@ describe('createGuardedRoute', () => {
}),
);
const GuardedComponent = createGuardedRoute(TestComponent, 'read', 'role:*');
const GuardedComponent = createGuardedRoute(
TestComponent,
'read',
'dashboard:*',
);
const mockMatch = {
params: {},
@@ -75,7 +79,7 @@ describe('createGuardedRoute', () => {
const GuardedComponent = createGuardedRoute(
TestComponent,
'read',
'role:{id}',
'dashboard:{id}',
);
const mockMatch = {
@@ -109,7 +113,7 @@ describe('createGuardedRoute', () => {
relation: txn.relation,
object: {
resource: {
kind: txn.object.resource.kind,
name: txn.object.resource.name,
type: txn.object.resource.type,
},
selector: '123:456',
@@ -127,7 +131,7 @@ describe('createGuardedRoute', () => {
const GuardedComponent = createGuardedRoute(
TestComponent,
'update',
'role:{id}:{version}',
'dashboard:{id}:{version}',
);
const mockMatch = {
@@ -162,7 +166,7 @@ describe('createGuardedRoute', () => {
const GuardedComponent = createGuardedRoute(
TestComponent,
'read',
'role:{id}',
'dashboard:{id}',
);
const mockMatch = {
@@ -197,7 +201,11 @@ describe('createGuardedRoute', () => {
}),
);
const GuardedComponent = createGuardedRoute(TestComponent, 'read', 'role:*');
const GuardedComponent = createGuardedRoute(
TestComponent,
'read',
'dashboard:*',
);
const mockMatch = {
params: {},
@@ -228,7 +236,11 @@ describe('createGuardedRoute', () => {
}),
);
const GuardedComponent = createGuardedRoute(TestComponent, 'read', 'role:*');
const GuardedComponent = createGuardedRoute(
TestComponent,
'read',
'dashboard:*',
);
const mockMatch = {
params: {},
@@ -266,7 +278,7 @@ describe('createGuardedRoute', () => {
const GuardedComponent = createGuardedRoute(
TestComponent,
'update',
'role:{id}',
'dashboard:{id}',
);
const mockMatch = {
@@ -292,7 +304,7 @@ describe('createGuardedRoute', () => {
});
expect(screen.getByText('update')).toBeInTheDocument();
expect(screen.getByText('role:123')).toBeInTheDocument();
expect(screen.getByText('dashboard:123')).toBeInTheDocument();
expect(
screen.queryByText('Test Component: test-value'),
).not.toBeInTheDocument();
@@ -323,7 +335,7 @@ describe('createGuardedRoute', () => {
const GuardedComponent = createGuardedRoute(
ComponentWithMultipleProps,
'read',
'role:*',
'dashboard:*',
);
const mockMatch = {
@@ -358,10 +370,10 @@ describe('createGuardedRoute', () => {
requestCount++;
const payload = (await req.json()) as AuthtypesTransactionDTO[];
const obj = payload[0]?.object;
const kind = obj?.resource?.kind;
const name = obj?.resource?.name;
const selector = obj?.selector ?? '*';
const objectStr =
obj?.resource?.type === 'metaresources' ? kind : `${kind}:${selector}`;
obj?.resource?.type === 'metaresources' ? name : `${name}:${selector}`;
requestedObjects.push(objectStr ?? '');
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
@@ -371,7 +383,7 @@ describe('createGuardedRoute', () => {
const GuardedComponent = createGuardedRoute(
TestComponent,
'read',
'role:{id}',
'dashboard:{id}',
);
const mockMatch1 = {
@@ -395,7 +407,7 @@ describe('createGuardedRoute', () => {
});
expect(requestCount).toBe(1);
expect(requestedObjects).toContain('role:123');
expect(requestedObjects).toContain('dashboard:123');
unmount();
@@ -420,7 +432,7 @@ describe('createGuardedRoute', () => {
});
expect(requestCount).toBe(2);
expect(requestedObjects).toContain('role:456');
expect(requestedObjects).toContain('dashboard:456');
});
it('should handle different relation types', async () => {
@@ -434,7 +446,7 @@ describe('createGuardedRoute', () => {
const GuardedComponent = createGuardedRoute(
TestComponent,
'delete',
'role:{id}',
'dashboard:{id}',
);
const mockMatch = {

View File

@@ -24,43 +24,6 @@
line-height: 20px;
}
.crossPanelSyncSectionHeader {
display: flex;
align-items: center;
gap: 6px;
align-self: flex-start;
}
.crossPanelSyncInfoIcon {
cursor: help;
color: var(--l3-foreground);
}
.crossPanelSyncTooltipContent {
display: flex;
flex-direction: column;
gap: 8px;
max-width: 300px;
}
.crossPanelSyncTooltipTitle {
font-size: 14px;
}
.crossPanelSyncTooltipDescription {
font-size: 12px;
line-height: 1.5;
}
.crossPanelSyncTooltipDocLink {
display: flex;
align-items: center;
gap: 4px;
color: var(--primary-background);
font-size: 12px;
margin-top: 4px;
}
.crossPanelSyncRow {
display: flex;
flex-direction: row;

View File

@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Col, Input, Radio, Select, Space, Tooltip, Typography } from 'antd';
import { Col, Input, Radio, Select, Space, Typography } from 'antd';
import AddTags from 'container/DashboardContainer/DashboardSettings/General/AddTags';
import { useDashboardCursorSyncMode } from 'hooks/dashboard/useDashboardCursorSyncMode';
import { useSyncTooltipFilterMode } from 'hooks/dashboard/useSyncTooltipFilterMode';
@@ -10,7 +10,7 @@ import {
SyncTooltipFilterMode,
} from 'lib/uPlotV2/plugins/TooltipPlugin/types';
import { isEqual } from 'lodash-es';
import { Check, ExternalLink, Info, X } from '@signozhq/icons';
import { Check, X } from 'lucide-react';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import styles from './GeneralSettings.module.scss';
@@ -173,36 +173,9 @@ function GeneralDashboardSettings(): JSX.Element {
</Space>
</Col>
<Col className={`${styles.overviewSettings} ${styles.crossPanelSyncGroup}`}>
<div className={styles.crossPanelSyncSectionHeader}>
<Typography.Text className={styles.crossPanelSyncSectionTitle}>
Cross-Panel Sync
</Typography.Text>
<Tooltip
title={
<div className={styles.crossPanelSyncTooltipContent}>
<strong className={styles.crossPanelSyncTooltipTitle}>
Cross-Panel Sync
</strong>
<span className={styles.crossPanelSyncTooltipDescription}>
Sync crosshair and tooltip across all the dashboard panels
</span>
<a
href="https://signoz.io/docs/dashboards/interactivity/#cross-panel-sync"
target="_blank"
rel="noopener noreferrer"
className={styles.crossPanelSyncTooltipDocLink}
>
Learn more
<ExternalLink size={12} />
</a>
</div>
}
placement="top"
mouseEnterDelay={0.5}
>
<Info size={14} className={styles.crossPanelSyncInfoIcon} />
</Tooltip>
</div>
<Typography.Text className={styles.crossPanelSyncSectionTitle}>
Cross-Panel Sync
</Typography.Text>
<div className={styles.crossPanelSyncRow}>
<div className={styles.crossPanelSyncInfo}>
<Typography.Text className={styles.crossPanelSyncTitle}>

View File

@@ -4,6 +4,7 @@ import { useHistory, useLocation } from 'react-router-dom';
import { Table2, Trash2, Users } from '@signozhq/icons';
import { Button, toast, ToggleGroup, ToggleGroupItem } from '@signozhq/ui';
import { Skeleton } from 'antd';
import { useAuthzResources } from 'api/generated/services/authz';
import {
getGetObjectsQueryKey,
useDeleteRole,
@@ -11,9 +12,6 @@ import {
useGetRole,
usePatchObjects,
} from 'api/generated/services/role';
import permissionsType from 'hooks/useAuthZ/permissions.type';
import type { AuthzResources } from '../utils';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import ROUTES from 'constants/routes';
import { capitalize } from 'lodash-es';
@@ -54,7 +52,10 @@ function RoleDetailsPage(): JSX.Element {
const queryClient = useQueryClient();
const { showErrorModal } = useErrorModal();
const authzResources = permissionsType.data as unknown as AuthzResources;
const { data: authzResourcesResponse } = useAuthzResources({
query: { enabled: true },
});
const authzResources = authzResourcesResponse?.data ?? null;
// Extract channelId from URL pathname since useParams doesn't work in nested routing
const roleIdMatch = pathname.match(ROLE_ID_REGEX);
@@ -93,7 +94,7 @@ function RoleDetailsPage(): JSX.Element {
const initialConfig = useMemo(() => {
if (!objectsData?.data || !activePermission) {
return;
return undefined;
}
return objectsToPermissionConfig(
objectsData.data,

View File

@@ -15,6 +15,15 @@ const CUSTOM_ROLE_ID = '019c24aa-3333-0001-aaaa-111111111111';
const MANAGED_ROLE_ID = '019c24aa-2248-756f-9833-984f1ab63819';
const rolesApiBase = 'http://localhost/api/v1/roles';
const authzResourcesUrl = 'http://localhost/api/v1/authz/resources';
const authzResourcesResponse = {
status: 'success',
data: {
relations: { create: ['dashboard'], read: ['dashboard'] },
resources: [{ name: 'dashboard', type: 'dashboard' }],
},
};
const emptyObjectsResponse = { status: 'success', data: [] };
@@ -36,6 +45,9 @@ function setupDefaultHandlers(roleId = CUSTOM_ROLE_ID): void {
rest.get(`${rolesApiBase}/:id`, (_req, res, ctx) =>
res(ctx.status(200), ctx.json(roleResponse)),
),
rest.get(authzResourcesUrl, (_req, res, ctx) =>
res(ctx.status(200), ctx.json(authzResourcesResponse)),
),
);
}

View File

@@ -1,18 +1,12 @@
import type {
CoretypesResourceRefDTO,
CoretypesObjectGroupDTO,
CoretypesTypeDTO,
AuthtypesGettableObjectsDTO,
AuthtypesGettableResourcesDTO,
} from 'api/generated/services/sigNoz.schemas';
import type {
PermissionConfig,
ResourceDefinition,
} from '../PermissionSidePanel/PermissionSidePanel.types';
type AuthzResources = {
resources: CoretypesResourceRefDTO[];
relations: Record<string, string[]>;
};
import { PermissionScope } from '../PermissionSidePanel/PermissionSidePanel.types';
import {
buildConfig,
@@ -39,17 +33,17 @@ jest.mock('../RoleDetails/constants', () => {
};
});
const dashboardResource: AuthzResources['resources'][number] = {
kind: 'dashboard',
type: 'metaresource' as CoretypesTypeDTO,
const dashboardResource: AuthtypesGettableResourcesDTO['resources'][number] = {
name: 'dashboard',
type: 'metaresource',
};
const alertResource: AuthzResources['resources'][number] = {
kind: 'alert',
type: 'metaresource' as CoretypesTypeDTO,
const alertResource: AuthtypesGettableResourcesDTO['resources'][number] = {
name: 'alert',
type: 'metaresource',
};
const baseAuthzResources: AuthzResources = {
const baseAuthzResources: AuthtypesGettableResourcesDTO = {
resources: [dashboardResource, alertResource],
relations: {
create: ['metaresource'],
@@ -226,7 +220,7 @@ describe('buildPatchPayload', () => {
describe('objectsToPermissionConfig', () => {
it('maps a wildcard selector to ALL scope', () => {
const objects: CoretypesObjectGroupDTO[] = [
const objects: AuthtypesGettableObjectsDTO[] = [
{ resource: dashboardResource, selectors: ['*'] },
];
@@ -239,7 +233,7 @@ describe('objectsToPermissionConfig', () => {
});
it('maps specific selectors to ONLY_SELECTED scope with the IDs', () => {
const objects: CoretypesObjectGroupDTO[] = [
const objects: AuthtypesGettableObjectsDTO[] = [
{ resource: dashboardResource, selectors: [ID_A, ID_B] },
];
@@ -344,7 +338,7 @@ describe('buildConfig', () => {
describe('derivePermissionTypes', () => {
it('derives one PermissionType per relation key with correct key and capitalised label', () => {
const relations: AuthzResources['relations'] = {
const relations: AuthtypesGettableResourcesDTO['relations'] = {
create: ['metaresource'],
read: ['metaresource'],
delete: ['metaresource'],

View File

@@ -1,8 +1,8 @@
import React from 'react';
import { Badge } from '@signozhq/ui';
import type {
CoretypesResourceRefDTO,
CoretypesObjectGroupDTO,
AuthtypesGettableObjectsDTO,
AuthtypesGettableResourcesDTO,
} from 'api/generated/services/sigNoz.schemas';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { capitalize } from 'lodash-es';
@@ -19,11 +19,6 @@ import {
PERMISSION_ICON_MAP,
} from './RoleDetails/constants';
export type AuthzResources = {
resources: ReadonlyArray<CoretypesResourceRefDTO>;
relations: Readonly<Record<string, ReadonlyArray<string>>>;
};
export interface PermissionType {
key: string;
label: string;
@@ -34,11 +29,11 @@ export interface PatchPayloadOptions {
newConfig: PermissionConfig;
initialConfig: PermissionConfig;
resources: ResourceDefinition[];
authzRes: AuthzResources;
authzRes: AuthtypesGettableResourcesDTO;
}
export function derivePermissionTypes(
relations: AuthzResources['relations'] | null,
relations: AuthtypesGettableResourcesDTO['relations'] | null,
): PermissionType[] {
const iconSize = { size: 14 };
@@ -60,7 +55,7 @@ export function derivePermissionTypes(
}
export function deriveResourcesForRelation(
authzResources: AuthzResources | null,
authzResources: AuthtypesGettableResourcesDTO | null,
relation: string,
): ResourceDefinition[] {
if (!authzResources?.relations) {
@@ -70,19 +65,19 @@ export function deriveResourcesForRelation(
return authzResources.resources
.filter((r) => supportedTypes.includes(r.type))
.map((r) => ({
id: r.kind,
label: capitalize(r.kind).replaceAll('_', ' '),
id: r.name,
label: capitalize(r.name).replace(/_/g, ' '),
options: [],
}));
}
export function objectsToPermissionConfig(
objects: CoretypesObjectGroupDTO[],
objects: AuthtypesGettableObjectsDTO[],
resources: ResourceDefinition[],
): PermissionConfig {
const config: PermissionConfig = {};
for (const res of resources) {
const obj = objects.find((o) => o.resource.kind === res.id);
const obj = objects.find((o) => o.resource.name === res.id);
if (!obj) {
config[res.id] = {
scope: PermissionScope.ONLY_SELECTED,
@@ -106,19 +101,19 @@ export function buildPatchPayload({
resources,
authzRes,
}: PatchPayloadOptions): {
additions: CoretypesObjectGroupDTO[] | null;
deletions: CoretypesObjectGroupDTO[] | null;
additions: AuthtypesGettableObjectsDTO[] | null;
deletions: AuthtypesGettableObjectsDTO[] | null;
} {
if (!authzRes) {
return { additions: null, deletions: null };
}
const additions: CoretypesObjectGroupDTO[] = [];
const deletions: CoretypesObjectGroupDTO[] = [];
const additions: AuthtypesGettableObjectsDTO[] = [];
const deletions: AuthtypesGettableObjectsDTO[] = [];
for (const res of resources) {
const initial = initialConfig[res.id];
const current = newConfig[res.id];
const resourceDef = authzRes.resources.find((r) => r.kind === res.id);
const resourceDef = authzRes.resources.find((r) => r.name === res.id);
if (!resourceDef) {
continue;
}

View File

@@ -1,158 +0,0 @@
import { act, render } from '@testing-library/react';
import { Modal } from 'antd';
import { useDashboardBootstrap } from 'hooks/dashboard/useDashboardBootstrap';
import { useTransformDashboardVariables } from 'hooks/dashboard/useTransformDashboardVariables';
import useTabVisibility from 'hooks/useTabFocus';
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { useDashboardQuery } from './useDashboardQuery';
const mockDispatch = jest.fn();
const mockSetDashboardData = jest.fn();
const mockSetLayouts = jest.fn();
const mockSetPanelMap = jest.fn();
const mockResetDashboardStore = jest.fn();
const mockGetUrlVariables = jest.fn();
const mockUpdateUrlVariable = jest.fn();
const mockRefetch = jest.fn();
let mockGlobalTime = {
selectedTime: 'custom',
minTime: 1710000000000000000,
maxTime: 1710000300000000000,
isAutoRefreshDisabled: true,
};
let currentQueryData: unknown;
jest.mock('react-i18next', () => ({
useTranslation: (): { t: (key: string) => string } => ({
t: (key: string): string => key,
}),
}));
jest.mock('react-redux', () => ({
useDispatch: jest.fn(() => mockDispatch),
useSelector: jest.fn(
(
selectorFn: (state: { globalTime: typeof mockGlobalTime }) => unknown,
): unknown => selectorFn({ globalTime: mockGlobalTime }),
),
}));
jest.mock('hooks/useTabFocus', () => jest.fn(() => true));
jest.mock('hooks/dashboard/useDashboardVariablesSync', () => ({
useDashboardVariablesSync: jest.fn(),
}));
jest.mock('./useDashboardQuery', () => ({
useDashboardQuery: jest.fn(),
}));
jest.mock('hooks/dashboard/useTransformDashboardVariables', () => ({
useTransformDashboardVariables: jest.fn(),
}));
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: jest.fn(),
}));
jest.mock('providers/Dashboard/initializeDefaultVariables', () => ({
initializeDefaultVariables: jest.fn(),
}));
jest.mock('lib/dashboard/getUpdatedLayout', () => ({
getUpdatedLayout: jest.fn(() => []),
}));
jest.mock('providers/Dashboard/util', () => ({
sortLayout: jest.fn((layout) => layout),
}));
jest.mock('lib/getMinMax', () => ({
getMinMaxForSelectedTime: jest.fn(),
}));
function TestComponent({ confirm }: { confirm: typeof Modal.confirm }): null {
useDashboardBootstrap('dashboard-1', { confirm });
return null;
}
describe('useDashboardBootstrap', () => {
beforeEach(() => {
jest.clearAllMocks();
mockGlobalTime = {
selectedTime: 'custom',
minTime: 1710000000000000000,
maxTime: 1710000300000000000,
isAutoRefreshDisabled: true,
};
jest.mocked(useDashboardStore as unknown as jest.Mock).mockReturnValue({
setDashboardData: mockSetDashboardData,
setLayouts: mockSetLayouts,
setPanelMap: mockSetPanelMap,
resetDashboardStore: mockResetDashboardStore,
});
jest
.mocked(useTransformDashboardVariables as unknown as jest.Mock)
.mockReturnValue({
getUrlVariables: mockGetUrlVariables,
updateUrlVariable: mockUpdateUrlVariable,
transformDashboardVariables: <T,>(data: T): T => data,
});
jest.mocked(useTabVisibility as unknown as jest.Mock).mockReturnValue(true);
jest
.mocked(useDashboardQuery as unknown as jest.Mock)
.mockImplementation(() => ({
data: currentQueryData,
isLoading: false,
isError: false,
isFetching: false,
error: null,
refetch: mockRefetch,
}));
});
it('keeps minTime and maxTime unchanged for custom range on refresh confirm', () => {
const initialDashboard = {
id: 'dashboard-1',
updatedAt: '2024-01-01T00:00:00.000Z',
data: { layout: [], panelMap: {}, variables: {} },
};
const updatedDashboard = {
id: 'dashboard-1',
updatedAt: '2024-01-01T01:00:00.000Z',
data: { layout: [], panelMap: {}, variables: {} },
};
const mockConfirm = jest.fn<
ReturnType<typeof Modal.confirm>,
Parameters<typeof Modal.confirm>
>(() => ({ destroy: jest.fn(), update: jest.fn() }));
currentQueryData = { data: initialDashboard };
const { rerender } = render(<TestComponent confirm={mockConfirm} />);
expect(mockConfirm).not.toHaveBeenCalled();
currentQueryData = { data: updatedDashboard };
rerender(<TestComponent confirm={mockConfirm} />);
expect(mockConfirm).toHaveBeenCalledTimes(1);
const firstCall = mockConfirm.mock.calls[0];
expect(firstCall).toBeDefined();
const [confirmProps] = firstCall as Parameters<typeof Modal.confirm>;
act(() => {
confirmProps.onOk?.();
});
expect(getMinMaxForSelectedTime).not.toHaveBeenCalled();
expect(mockDispatch).toHaveBeenCalledWith({
type: 'UPDATE_TIME_INTERVAL',
payload: {
selectedTime: 'custom',
minTime: mockGlobalTime.minTime,
maxTime: mockGlobalTime.maxTime,
},
});
});
});

View File

@@ -102,19 +102,11 @@ export function useDashboardBootstrap(
onOk() {
setDashboardData(updatedDashboardData);
const { maxTime, minTime } =
globalTime.selectedTime === 'custom'
? {
// For custom ranges, min/max are already stored in nanoseconds.
// Recomputing via getMinMaxForSelectedTime would multiply them again.
maxTime: globalTime.maxTime,
minTime: globalTime.minTime,
}
: getMinMaxForSelectedTime(
globalTime.selectedTime,
globalTime.minTime,
globalTime.maxTime,
);
const { maxTime, minTime } = getMinMaxForSelectedTime(
globalTime.selectedTime,
globalTime.minTime,
globalTime.maxTime,
);
dispatch({
type: UPDATE_TIME_INTERVAL,
payload: { maxTime, minTime, selectedTime: globalTime.selectedTime },

View File

@@ -1,29 +1,32 @@
// AUTO GENERATED FILE - DO NOT EDIT - GENERATED BY cmd/enterprise/*.go generate authz
// AUTO GENERATED FILE - DO NOT EDIT - GENERATED BY scripts/generate-permissions-type
export default {
status: 'success',
data: {
resources: [
{
kind: 'role',
name: 'dashboard',
type: 'metaresource',
},
{
name: 'dashboards',
type: 'metaresources',
},
{
kind: 'role',
name: 'role',
type: 'role',
},
{
kind: 'serviceaccount',
type: 'serviceaccount',
name: 'roles',
type: 'metaresources',
},
],
relations: {
assignee: ['role'],
attach: ['role', 'serviceaccount'],
create: ['metaresources'],
delete: ['role', 'serviceaccount'],
delete: ['user', 'serviceaccount', 'role', 'organization', 'metaresource'],
list: ['metaresources'],
read: ['role', 'serviceaccount'],
update: ['role', 'serviceaccount'],
read: ['user', 'serviceaccount', 'role', 'organization', 'metaresource'],
update: ['user', 'serviceaccount', 'role', 'organization', 'metaresource'],
},
},
} as const;

View File

@@ -1,16 +1,15 @@
import permissionsType from './permissions.type';
const ObjectSeparator = ':';
import { ObjectSeparator } from './utils';
type PermissionsData = typeof permissionsType.data;
export type Resource = PermissionsData['resources'][number];
export type ResourceName = Resource['kind'];
export type ResourceName = Resource['name'];
export type ResourceType = Resource['type'];
type RelationsByType = PermissionsData['relations'];
type ResourceTypeMap = {
[K in ResourceName]: Extract<Resource, { kind: K }>['type'];
[K in ResourceName]: Extract<Resource, { name: K }>['type'];
};
type RelationName = keyof RelationsByType;
@@ -18,7 +17,7 @@ type RelationName = keyof RelationsByType;
export type ResourcesForRelation<R extends RelationName> = Extract<
Resource,
{ type: RelationsByType[R][number] }
>['kind'];
>['name'];
type IsPluralResource<R extends ResourceName> =
ResourceTypeMap[R] extends 'metaresources' ? true : false;

View File

@@ -36,8 +36,8 @@ const wrapper = ({ children }: { children: ReactElement }): ReactElement => (
describe('useAuthZ', () => {
it('should fetch and return permissions successfully', async () => {
const permission1 = buildPermission('read', 'role:*');
const permission2 = buildPermission('update', 'role:123');
const permission1 = buildPermission('read', 'dashboard:*');
const permission2 = buildPermission('update', 'dashboard:123');
const expectedResponse = {
[permission1]: {
@@ -74,7 +74,7 @@ describe('useAuthZ', () => {
});
it('should handle API errors', async () => {
const permission = buildPermission('read', 'role:*');
const permission = buildPermission('read', 'dashboard:*');
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
@@ -95,9 +95,9 @@ describe('useAuthZ', () => {
});
it('should refetch when permissions array changes', async () => {
const permission1 = buildPermission('read', 'role:*');
const permission2 = buildPermission('update', 'role:123');
const permission3 = buildPermission('delete', 'role:456');
const permission1 = buildPermission('read', 'dashboard:*');
const permission2 = buildPermission('update', 'dashboard:123');
const permission3 = buildPermission('delete', 'dashboard:456');
let requestCount = 0;
@@ -161,8 +161,8 @@ describe('useAuthZ', () => {
});
it('should not refetch when permissions array order changes but content is the same', async () => {
const permission1 = buildPermission('read', 'role:*');
const permission2 = buildPermission('update', 'role:123');
const permission1 = buildPermission('read', 'dashboard:*');
const permission2 = buildPermission('update', 'dashboard:123');
let requestCount = 0;
@@ -217,8 +217,8 @@ describe('useAuthZ', () => {
});
it('should send correct payload format to API', async () => {
const permission1 = buildPermission('read', 'role:*');
const permission2 = buildPermission('update', 'role:123');
const permission1 = buildPermission('read', 'dashboard:*');
const permission2 = buildPermission('update', 'dashboard:123');
let receivedPayload: any = null;
@@ -244,23 +244,23 @@ describe('useAuthZ', () => {
expect(receivedPayload[0]).toMatchObject({
relation: 'read',
object: {
resource: { kind: 'role', type: 'role' },
resource: { name: 'dashboard', type: 'metaresource' },
selector: '*',
},
});
expect(receivedPayload[1]).toMatchObject({
relation: 'update',
object: {
resource: { kind: 'role', type: 'role' },
resource: { name: 'dashboard', type: 'metaresource' },
selector: '123',
},
});
});
it('should batch multiple hooks into single flight request', async () => {
const permission1 = buildPermission('read', 'role:*');
const permission2 = buildPermission('update', 'role:123');
const permission3 = buildPermission('delete', 'role:456');
const permission1 = buildPermission('read', 'dashboard:*');
const permission2 = buildPermission('update', 'dashboard:123');
const permission3 = buildPermission('delete', 'dashboard:456');
let requestCount = 0;
const receivedPayloads: any[] = [];
@@ -304,17 +304,17 @@ describe('useAuthZ', () => {
expect(receivedPayloads[0][0]).toMatchObject({
relation: 'read',
object: {
resource: { kind: 'role', type: 'role' },
resource: { name: 'dashboard', type: 'metaresource' },
selector: '*',
},
});
expect(receivedPayloads[0][1]).toMatchObject({
relation: 'update',
object: { resource: { kind: 'role' }, selector: '123' },
object: { resource: { name: 'dashboard' }, selector: '123' },
});
expect(receivedPayloads[0][2]).toMatchObject({
relation: 'delete',
object: { resource: { kind: 'role' }, selector: '456' },
object: { resource: { name: 'dashboard' }, selector: '456' },
});
expect(result1.current.permissions).toStrictEqual({
@@ -329,9 +329,9 @@ describe('useAuthZ', () => {
});
it('should create separate batches for calls after single flight window', async () => {
const permission1 = buildPermission('read', 'role:*');
const permission2 = buildPermission('update', 'role:123');
const permission3 = buildPermission('delete', 'role:456');
const permission1 = buildPermission('read', 'dashboard:*');
const permission2 = buildPermission('update', 'dashboard:123');
const permission3 = buildPermission('delete', 'dashboard:456');
let requestCount = 0;
const receivedPayloads: any[] = [];
@@ -386,18 +386,18 @@ describe('useAuthZ', () => {
expect(receivedPayloads[1]).toHaveLength(2);
expect(receivedPayloads[1][0]).toMatchObject({
relation: 'update',
object: { resource: { kind: 'role' }, selector: '123' },
object: { resource: { name: 'dashboard' }, selector: '123' },
});
expect(receivedPayloads[1][1]).toMatchObject({
relation: 'delete',
object: { resource: { kind: 'role' }, selector: '456' },
object: { resource: { name: 'dashboard' }, selector: '456' },
});
});
it('should map permissions correctly when API returns response out of order', async () => {
const permission1 = buildPermission('read', 'role:*');
const permission2 = buildPermission('update', 'role:123');
const permission3 = buildPermission('delete', 'role:456');
const permission1 = buildPermission('read', 'dashboard:*');
const permission2 = buildPermission('update', 'dashboard:123');
const permission3 = buildPermission('delete', 'dashboard:456');
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
@@ -435,8 +435,8 @@ describe('useAuthZ', () => {
});
it('should not leak state between separate batches', async () => {
const permission1 = buildPermission('read', 'role:*');
const permission2 = buildPermission('update', 'role:123');
const permission1 = buildPermission('read', 'dashboard:*');
const permission2 = buildPermission('update', 'dashboard:123');
let requestCount = 0;

View File

@@ -2,7 +2,7 @@ import { useCallback, useMemo } from 'react';
import { useQueries } from 'react-query';
import { authzCheck } from 'api/generated/services/authz';
import type {
CoretypesObjectDTO,
AuthtypesObjectDTO,
AuthtypesTransactionDTO,
} from 'api/generated/services/sigNoz.schemas';
@@ -34,7 +34,7 @@ function dispatchPermission(
});
setTimeout(() => {
const copiedPermissions = [...pendingPermissions];
const copiedPermissions = pendingPermissions.slice();
pendingPermissions = [];
ctx = null;
@@ -50,9 +50,9 @@ async function fetchManyPermissions(
): Promise<AuthZCheckResponse> {
const payload: AuthtypesTransactionDTO[] = permissions.map((permission) => {
const dto = permissionToTransactionDto(permission);
const object: CoretypesObjectDTO = {
const object: AuthtypesObjectDTO = {
resource: {
kind: dto.object.resource.kind,
name: dto.object.resource.name,
type: dto.object.resource.type,
},
selector: dto.object.selector,

View File

@@ -1,8 +1,4 @@
import {
AuthtypesTransactionDTO,
CoretypesTypeDTO,
AuthtypesRelationDTO,
} from '../../api/generated/services/sigNoz.schemas';
import { AuthtypesTransactionDTO } from '../../api/generated/services/sigNoz.schemas';
import permissionsType from './permissions.type';
import {
AuthZObject,
@@ -37,72 +33,36 @@ export function parsePermission(permission: BrandedPermission): {
return { relation: relation as AuthZRelation, object };
}
const kindsByType = permissionsType.data.resources.reduce(
const resourceNameToType = permissionsType.data.resources.reduce(
(acc, r) => {
if (!acc[r.type]) {
acc[r.type] = new Set();
}
acc[r.type].add(r.kind);
acc[r.name] = r.type;
return acc;
},
{} as Record<string, Set<string>>,
{} as Record<ResourceName, ResourceType>,
);
function resolveType(
relation: AuthZRelation,
kind: string,
): ResourceType | undefined {
const candidates: readonly string[] =
permissionsType.data.relations[relation] ?? [];
for (const t of candidates) {
if (kindsByType[t]?.has(kind)) {
return t as ResourceType;
}
}
return undefined;
}
function splitObjectString(objectStr: string): {
resourceName: string;
selector: string;
} {
const idx = objectStr.indexOf(ObjectSeparator);
if (idx === -1) {
return { resourceName: objectStr, selector: '' };
}
return {
resourceName: objectStr.slice(0, idx),
selector: objectStr.slice(idx + 1),
};
}
export function permissionToTransactionDto(
permission: BrandedPermission,
): AuthtypesTransactionDTO {
const { relation, object: objectStr } = parsePermission(permission);
const directType = resolveType(relation, objectStr);
const directType = resourceNameToType[objectStr as ResourceName];
if (directType === 'metaresources') {
return {
relation: relation as AuthtypesRelationDTO,
relation,
object: {
resource: {
kind: objectStr as ResourceName,
type: directType as CoretypesTypeDTO,
},
resource: { name: objectStr, type: directType },
selector: '*',
},
};
}
const { resourceName, selector } = splitObjectString(objectStr);
const type = resolveType(relation, resourceName) ?? 'metaresource';
const [resourceName, selector] = objectStr.split(ObjectSeparator);
const type =
resourceNameToType[resourceName as ResourceName] ?? 'metaresource';
return {
relation: relation as AuthtypesRelationDTO,
relation,
object: {
resource: {
kind: resourceName as ResourceName,
type: type as CoretypesTypeDTO,
},
resource: { name: resourceName, type },
selector: selector || '*',
},
};
@@ -115,7 +75,7 @@ export function gettableTransactionToPermission(
relation,
object: { resource, selector },
} = item;
const resourceName = String(resource.kind);
const resourceName = String(resource.name);
const selectorStr = typeof selector === 'string' ? selector : '*';
const objectStr =
resource.type === 'metaresources'

View File

@@ -24,15 +24,10 @@ export default function Tooltip({
);
const showHeader = showTooltipHeader || activeItem != null;
// A single row collapses into the header when it's the active item, but
// must stay in the list when there's no active item (e.g. sync-driven
// tooltips with no focused series) — otherwise the row would vanish.
const showList =
tooltipContent.length > 1 ||
(tooltipContent.length === 1 && activeItem == null);
// The divider separates the active row in the header from the list; with
// no active item it has nothing to separate.
const showDivider = showList && showHeader && activeItem != null;
// With a single series the active item is fully represented in the header —
// hide the divider and list to avoid showing a duplicate row.
const showList = tooltipContent.length > 1;
const showDivider = showList && showHeader;
return (
<div

View File

@@ -137,7 +137,7 @@ function applyReceiverSync({
if (commonKeys.length === 0) {
uPlotInstance.setSeries(null, { focus: false });
return noMatchResult;
return [];
}
if ((uPlotInstance.cursor.left ?? -1) < 0) {

View File

@@ -8,7 +8,7 @@ import afterLogin from 'AppRoutes/utils';
import AuthError from 'components/AuthError/AuthError';
import AuthPageContainer from 'components/AuthPageContainer';
import { useNotifications } from 'hooks/useNotifications';
import { ArrowRight } from 'lucide-react';
import { ArrowRight, CircleAlert } from 'lucide-react';
import APIError from 'types/api/error';
import tvUrl from '@/assets/svgs/tv.svg';
@@ -28,8 +28,9 @@ type FormValues = {
function SignUp(): JSX.Element {
const [loading, setLoading] = useState(false);
const [confirmPasswordTouched, setConfirmPasswordTouched] = useState(false);
const [confirmPasswordError, setConfirmPasswordError] =
useState<boolean>(false);
const [formError, setFormError] = useState<APIError | null>();
const { notifications } = useNotifications();
@@ -83,10 +84,35 @@ function SignUp(): JSX.Element {
})();
};
const isPasswordMismatch =
Boolean(confirmPassword) && password !== confirmPassword;
const handleValuesChange: (changedValues: Partial<FormValues>) => void = (
changedValues,
) => {
// Clear error if passwords match while typing (but don't set error until blur)
if ('password' in changedValues || 'confirmPassword' in changedValues) {
const { password, confirmPassword } = form.getFieldsValue();
const showPasswordMismatchError = confirmPasswordTouched && isPasswordMismatch;
if (password && confirmPassword && password === confirmPassword) {
setConfirmPasswordError(false);
}
}
};
const handlePasswordBlur = (): void => {
const { password, confirmPassword } = form.getFieldsValue();
// Only validate if confirm password has a value
if (confirmPassword) {
const isSamePassword = password === confirmPassword;
setConfirmPasswordError(!isSamePassword);
}
};
const handleConfirmPasswordBlur = (): void => {
const { password, confirmPassword } = form.getFieldsValue();
if (password && confirmPassword) {
const isSamePassword = password === confirmPassword;
setConfirmPasswordError(!isSamePassword);
}
};
const isValidForm = useMemo(
(): boolean =>
@@ -94,8 +120,8 @@ function SignUp(): JSX.Element {
Boolean(email?.trim()) &&
Boolean(password?.trim()) &&
Boolean(confirmPassword?.trim()) &&
password === confirmPassword,
[loading, email, password, confirmPassword],
!confirmPasswordError,
[loading, email, password, confirmPassword, confirmPasswordError],
);
return (
@@ -114,7 +140,12 @@ function SignUp(): JSX.Element {
</Typography.Paragraph>
</div>
<FormContainer onFinish={handleSubmit} form={form} className="signup-form">
<FormContainer
onFinish={handleSubmit}
onValuesChange={handleValuesChange}
form={form}
className="signup-form"
>
<div className="signup-form-container">
<div className="signup-form-fields">
<div className="signup-field-container">
@@ -144,6 +175,7 @@ function SignUp(): JSX.Element {
placeholder="Enter new password"
disabled={loading}
className="signup-antd-input"
onBlur={handlePasswordBlur}
/>
</FormContainer.Item>
</div>
@@ -153,12 +185,6 @@ function SignUp(): JSX.Element {
<FormContainer.Item
name="confirmPassword"
validateTrigger="onBlur"
validateStatus={showPasswordMismatchError ? 'error' : undefined}
help={
showPasswordMismatchError
? "Passwords don't match. Please try again."
: undefined
}
rules={[{ required: true, message: 'Please enter confirm password!' }]}
>
<AntdInput.Password
@@ -167,7 +193,7 @@ function SignUp(): JSX.Element {
placeholder="Confirm your new password"
disabled={loading}
className="signup-antd-input"
onBlur={() => setConfirmPasswordTouched(true)}
onBlur={handleConfirmPasswordBlur}
/>
</FormContainer.Item>
</div>
@@ -179,7 +205,19 @@ function SignUp(): JSX.Element {
your admin for an invite link
</Callout>
{formError && <AuthError error={formError} />}
{confirmPasswordError && (
<Callout
type="error"
size="small"
showIcon
icon={<CircleAlert size={12} />}
className="signup-error-callout"
>
Passwords don&apos;t match. Please try again.
</Callout>
)}
{formError && !confirmPasswordError && <AuthError error={formError} />}
<div className="signup-form-actions">
<Button

View File

@@ -7,12 +7,7 @@ export const topTracesTableColumns = [
dataIndex: 'trace_id',
key: 'trace_id',
render: (traceId: string): JSX.Element => (
<Link
to={`/trace/${traceId}`}
className="trace-id-cell"
target="_blank"
rel="noopener noreferrer"
>
<Link to={`/trace/${traceId}`} className="trace-id-cell">
{traceId}
</Link>
),

View File

@@ -26,5 +26,22 @@ func (provider *provider) addAuthzRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/authz/resources", handler.New(provider.authZ.OpenAccess(provider.authzHandler.GetResources), handler.OpenAPIDef{
ID: "AuthzResources",
Tags: []string{"authz"},
Summary: "Get resources",
Description: "Gets all the available resources",
Request: nil,
RequestContentType: "",
Response: new(authtypes.GettableResources),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: nil,
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -6,7 +6,6 @@ import (
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -84,9 +83,9 @@ 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.Relation{Verb: coretypes.VerbRead},
coretypes.ResourceMetaResourcePublicDashboard,
func(req *http.Request, orgs []*types.Organization) ([]coretypes.Selector, valuer.UUID, error) {
authtypes.RelationRead,
dashboardtypes.TypeableMetaResourcePublicDashboard,
func(req *http.Request, orgs []*types.Organization) ([]authtypes.Selector, valuer.UUID, error) {
id, err := valuer.NewUUID(mux.Vars(req)["id"])
if err != nil {
return nil, valuer.UUID{}, err
@@ -105,16 +104,16 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newAnonymousSecuritySchemes([]string{coretypes.ResourceMetaResourcePublicDashboard.Scope(coretypes.VerbRead)}),
SecuritySchemes: newAnonymousSecuritySchemes([]string{dashboardtypes.TypeableMetaResourcePublicDashboard.Scope(authtypes.RelationRead)}),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/public/dashboards/{id}/widgets/{idx}/query_range", handler.New(provider.authZ.CheckWithoutClaims(
provider.dashboardHandler.GetPublicWidgetQueryRange,
authtypes.Relation{Verb: coretypes.VerbRead},
coretypes.ResourceMetaResourcePublicDashboard,
func(req *http.Request, orgs []*types.Organization) ([]coretypes.Selector, valuer.UUID, error) {
authtypes.RelationRead,
dashboardtypes.TypeableMetaResourcePublicDashboard,
func(req *http.Request, orgs []*types.Organization) ([]authtypes.Selector, valuer.UUID, error) {
id, err := valuer.NewUUID(mux.Vars(req)["id"])
if err != nil {
return nil, valuer.UUID{}, err
@@ -133,7 +132,7 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newAnonymousSecuritySchemes([]string{coretypes.ResourceMetaResourcePublicDashboard.Scope(coretypes.VerbRead)}),
SecuritySchemes: newAnonymousSecuritySchemes([]string{dashboardtypes.TypeableMetaResourcePublicDashboard.Scope(authtypes.RelationRead)}),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}

View File

@@ -6,7 +6,6 @@ import (
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/gorilla/mux"
)
@@ -69,7 +68,7 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
Description: "Gets all objects connected to the specified role via a given relation type",
Request: nil,
RequestContentType: "",
Response: make([]*coretypes.ObjectGroup, 0),
Response: make([]*authtypes.GettableObjects, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
@@ -101,7 +100,7 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
Tags: []string{"role"},
Summary: "Patch objects for a role by relation",
Description: "Patches the objects connected to the specified role via a given relation type",
Request: new(coretypes.PatchableObjects),
Request: new(authtypes.PatchableObjects),
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",

View File

@@ -11,7 +11,6 @@ import (
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/types/audittypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -20,16 +19,16 @@ func newTestSettings() factory.ScopedProviderSettings {
return factory.NewScopedProviderSettings(instrumentationtest.New().ToProviderSettings(), "auditorserver_test")
}
func newTestEvent(resource string, action coretypes.Verb) audittypes.AuditEvent {
func newTestEvent(resource string, action audittypes.Action) audittypes.AuditEvent {
return audittypes.AuditEvent{
Timestamp: time.Now(),
EventName: audittypes.NewEventName(coretypes.MustNewKind(resource), action),
EventName: audittypes.NewEventName(resource, action),
AuditAttributes: audittypes.AuditAttributes{
Action: action,
Outcome: audittypes.OutcomeSuccess,
},
ResourceAttributes: audittypes.ResourceAttributes{
ResourceKind: coretypes.MustNewKind(resource),
ResourceKind: resource,
},
}
}
@@ -84,7 +83,7 @@ func TestAdd_FlushesOnBatchSize(t *testing.T) {
go func() { _ = server.Start(ctx) }()
for i := 0; i < 3; i++ {
server.Add(ctx, newTestEvent("dashboard", coretypes.VerbCreate))
server.Add(ctx, newTestEvent("dashboard", audittypes.ActionCreate))
}
assert.Eventually(t, func() bool {
@@ -113,7 +112,7 @@ func TestAdd_FlushesOnInterval(t *testing.T) {
go func() { _ = server.Start(ctx) }()
server.Add(ctx, newTestEvent("user", coretypes.VerbUpdate))
server.Add(ctx, newTestEvent("user", audittypes.ActionUpdate))
assert.Eventually(t, func() bool {
return exported.Load() == 1
@@ -131,9 +130,9 @@ func TestAdd_DropsWhenBufferFull(t *testing.T) {
ctx := context.Background()
server.Add(ctx, newTestEvent("dashboard", coretypes.VerbCreate))
server.Add(ctx, newTestEvent("dashboard", coretypes.VerbUpdate))
server.Add(ctx, newTestEvent("dashboard", coretypes.VerbDelete))
server.Add(ctx, newTestEvent("dashboard", audittypes.ActionCreate))
server.Add(ctx, newTestEvent("dashboard", audittypes.ActionUpdate))
server.Add(ctx, newTestEvent("dashboard", audittypes.ActionDelete))
assert.Equal(t, 2, server.queueLen())
}
@@ -156,7 +155,7 @@ func TestStop_DrainsRemainingEvents(t *testing.T) {
go func() { _ = server.Start(ctx) }()
for i := 0; i < 5; i++ {
server.Add(ctx, newTestEvent("alert-rule", coretypes.VerbCreate))
server.Add(ctx, newTestEvent("alert-rule", audittypes.ActionCreate))
}
require.NoError(t, server.Stop(ctx))
@@ -181,8 +180,8 @@ func TestAdd_ContinuesAfterExportFailure(t *testing.T) {
go func() { _ = server.Start(ctx) }()
server.Add(ctx, newTestEvent("user", coretypes.VerbDelete))
server.Add(ctx, newTestEvent("user", coretypes.VerbDelete))
server.Add(ctx, newTestEvent("user", audittypes.ActionDelete))
server.Add(ctx, newTestEvent("user", audittypes.ActionDelete))
assert.Eventually(t, func() bool {
return calls.Load() >= 1
@@ -213,7 +212,7 @@ func TestAdd_ConcurrentSafety(t *testing.T) {
wg.Add(1)
go func() {
defer wg.Done()
server.Add(ctx, newTestEvent("dashboard", coretypes.VerbCreate))
server.Add(ctx, newTestEvent("dashboard", audittypes.ActionCreate))
}()
}
wg.Wait()

View File

@@ -6,7 +6,6 @@ import (
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/valuer"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
)
@@ -15,10 +14,10 @@ type AuthZ interface {
factory.ServiceWithHealthy
// CheckWithTupleCreation takes upon the responsibility for generating the tuples alongside everything Check does.
CheckWithTupleCreation(context.Context, authtypes.Claims, valuer.UUID, authtypes.Relation, coretypes.Resource, []coretypes.Selector, []coretypes.Selector) error
CheckWithTupleCreation(context.Context, authtypes.Claims, valuer.UUID, authtypes.Relation, authtypes.Typeable, []authtypes.Selector, []authtypes.Selector) error
// CheckWithTupleCreationWithoutClaims checks permissions for anonymous users.
CheckWithTupleCreationWithoutClaims(context.Context, valuer.UUID, authtypes.Relation, coretypes.Resource, []coretypes.Selector, []coretypes.Selector) error
CheckWithTupleCreationWithoutClaims(context.Context, valuer.UUID, authtypes.Relation, authtypes.Typeable, []authtypes.Selector, []authtypes.Selector) error
// BatchCheck accepts a map of ID → tuple and returns a map of ID → authorization result.
BatchCheck(context.Context, map[string]*openfgav1.TupleKey) (map[string]*authtypes.TupleKeyAuthorization, error)
@@ -31,7 +30,7 @@ type AuthZ interface {
Write(context.Context, []*openfgav1.TupleKey, []*openfgav1.TupleKey) error
// Lists the selectors for objects assigned to subject (s) with relation (r) on resource (s)
ListObjects(context.Context, string, authtypes.Relation, coretypes.Type) ([]*coretypes.Object, error)
ListObjects(context.Context, string, authtypes.Relation, authtypes.Type) ([]*authtypes.Object, error)
// Creates the role.
Create(context.Context, valuer.UUID, *authtypes.Role) error
@@ -40,13 +39,16 @@ type AuthZ interface {
GetOrCreate(context.Context, valuer.UUID, *authtypes.Role) (*authtypes.Role, error)
// Gets the objects associated with the given role and relation.
GetObjects(context.Context, valuer.UUID, valuer.UUID, authtypes.Relation) ([]*coretypes.Object, error)
GetObjects(context.Context, valuer.UUID, valuer.UUID, authtypes.Relation) ([]*authtypes.Object, error)
// Gets all the typeable resources registered from role registry.
GetResources(context.Context) []*authtypes.Resource
// Patches the role.
Patch(context.Context, valuer.UUID, *authtypes.Role) error
// Patches the objects in authorization server associated with the given role and relation
PatchObjects(context.Context, valuer.UUID, string, authtypes.Relation, []*coretypes.Object, []*coretypes.Object) error
PatchObjects(context.Context, valuer.UUID, string, authtypes.Relation, []*authtypes.Object, []*authtypes.Object) error
// Deletes the role and tuples in authorization server.
Delete(context.Context, valuer.UUID, valuer.UUID) error
@@ -88,6 +90,12 @@ 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 Handler interface {
Create(http.ResponseWriter, *http.Request)
@@ -95,6 +103,8 @@ type Handler interface {
GetObjects(http.ResponseWriter, *http.Request)
GetResources(http.ResponseWriter, *http.Request)
List(http.ResponseWriter, *http.Request)
Patch(http.ResponseWriter, *http.Request)

View File

@@ -25,7 +25,7 @@ func newConfig() factory.Config {
return &Config{
Provider: "openfga",
OpenFGA: OpenFGAConfig{
MaxTuplesPerWrite: 300,
MaxTuplesPerWrite: 100,
},
}
}

View File

@@ -8,7 +8,6 @@ import (
"github.com/SigNoz/signoz/pkg/authz/openfgaserver"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/SigNoz/signoz/pkg/factory"
@@ -19,27 +18,31 @@ import (
)
type provider struct {
server *openfgaserver.Server
store authtypes.RoleStore
registry *authtypes.Registry
server *openfgaserver.Server
store authtypes.RoleStore
registry []authz.RegisterTypeable
managedRolesByTransaction map[string][]string
}
func NewProviderFactory(sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, registry *authtypes.Registry) factory.ProviderFactory[authz.AuthZ, authz.Config] {
func NewProviderFactory(sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, registry ...authz.RegisterTypeable) 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 *authtypes.Registry) (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.RegisterTypeable) (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,
server: server,
store: sqlauthzstore.NewSqlAuthzStore(sqlstore),
registry: registry,
managedRolesByTransaction: managedRolesByTransaction,
}, nil
}
@@ -59,11 +62,11 @@ func (provider *provider) BatchCheck(ctx context.Context, tupleReq map[string]*o
return provider.server.BatchCheck(ctx, tupleReq)
}
func (provider *provider) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, relation authtypes.Relation, typeable coretypes.Resource, selectors []coretypes.Selector, roleSelectors []coretypes.Selector) error {
func (provider *provider) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, relation authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector, roleSelectors []authtypes.Selector) error {
return provider.server.CheckWithTupleCreation(ctx, claims, orgID, relation, typeable, selectors, roleSelectors)
}
func (provider *provider) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, relation authtypes.Relation, typeable coretypes.Resource, selectors []coretypes.Selector, roleSelectors []coretypes.Selector) error {
func (provider *provider) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, relation authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector, roleSelectors []authtypes.Selector) error {
return provider.server.CheckWithTupleCreationWithoutClaims(ctx, orgID, relation, typeable, selectors, roleSelectors)
}
@@ -75,7 +78,7 @@ func (provider *provider) ReadTuples(ctx context.Context, tupleKey *openfgav1.Re
return provider.server.ReadTuples(ctx, tupleKey)
}
func (provider *provider) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType coretypes.Type) ([]*coretypes.Object, error) {
func (provider *provider) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType authtypes.Type) ([]*authtypes.Object, error) {
return provider.server.ListObjects(ctx, subject, relation, objectType)
}
@@ -100,14 +103,22 @@ func (provider *provider) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UU
}
func (provider *provider) Grant(ctx context.Context, orgID valuer.UUID, names []string, subject string) error {
selectors := make([]coretypes.Selector, len(names))
selectors := make([]authtypes.Selector, len(names))
for idx, name := range names {
selectors[idx] = coretypes.TypeRole.MustSelector(name)
selectors[idx] = authtypes.MustNewSelector(authtypes.TypeRole, name)
}
tuples := authtypes.NewTuples(coretypes.NewResourceRole(), subject, authtypes.Relation{Verb: coretypes.VerbAssignee}, selectors, orgID)
tuples, err := authtypes.TypeableRole.Tuples(
subject,
authtypes.RelationAssignee,
selectors,
orgID,
)
if err != nil {
return err
}
err := provider.Write(ctx, tuples, nil)
err = provider.Write(ctx, tuples, nil)
if err != nil {
return errors.WithAdditionalf(err, "failed to grant roles: %v to subject: %s", names, subject)
}
@@ -130,14 +141,22 @@ func (provider *provider) ModifyGrant(ctx context.Context, orgID valuer.UUID, ex
}
func (provider *provider) Revoke(ctx context.Context, orgID valuer.UUID, names []string, subject string) error {
selectors := make([]coretypes.Selector, len(names))
selectors := make([]authtypes.Selector, len(names))
for idx, name := range names {
selectors[idx] = coretypes.TypeRole.MustSelector(name)
selectors[idx] = authtypes.MustNewSelector(authtypes.TypeRole, name)
}
tuples := authtypes.NewTuples(coretypes.NewResourceRole(), subject, authtypes.Relation{Verb: coretypes.VerbAssignee}, selectors, orgID)
tuples, err := authtypes.TypeableRole.Tuples(
subject,
authtypes.RelationAssignee,
selectors,
orgID,
)
if err != nil {
return err
}
err := provider.Write(ctx, nil, tuples)
err = provider.Write(ctx, nil, tuples)
if err != nil {
return errors.WithAdditionalf(err, "failed to revoke roles: %v to subject: %s", names, subject)
}
@@ -165,7 +184,7 @@ func (provider *provider) CreateManagedRoles(ctx context.Context, _ valuer.UUID,
}
func (provider *provider) CreateManagedUserRoleTransactions(ctx context.Context, orgID valuer.UUID, userID valuer.UUID) error {
return provider.Grant(ctx, orgID, []string{authtypes.SigNozAdminRoleName}, authtypes.MustNewSubject(coretypes.NewResourceUser(), userID.String(), orgID, nil))
return provider.Grant(ctx, orgID, []string{authtypes.SigNozAdminRoleName}, authtypes.MustNewSubject(authtypes.TypeableUser, userID.String(), orgID, nil))
}
func (setter *provider) Create(_ context.Context, _ valuer.UUID, _ *authtypes.Role) error {
@@ -176,7 +195,11 @@ func (provider *provider) GetOrCreate(_ context.Context, _ valuer.UUID, _ *autht
return nil, errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
}
func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation) ([]*coretypes.Object, error) {
func (provider *provider) GetResources(_ context.Context) []*authtypes.Resource {
return []*authtypes.Resource{}
}
func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation) ([]*authtypes.Object, error) {
return nil, errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
}
@@ -184,7 +207,7 @@ func (provider *provider) Patch(_ context.Context, _ valuer.UUID, _ *authtypes.R
return errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
}
func (provider *provider) PatchObjects(_ context.Context, _ valuer.UUID, _ string, _ authtypes.Relation, _, _ []*coretypes.Object) error {
func (provider *provider) PatchObjects(_ context.Context, _ valuer.UUID, _ string, _ authtypes.Relation, _, _ []*authtypes.Object) error {
return errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
}
@@ -197,7 +220,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.registry.ManagedRolesByTransaction())
tuples, preResolved, roleCorrelations, err := authtypes.NewTuplesFromTransactionsWithManagedRoles(transactions, subject, orgID, provider.managedRolesByTransaction)
if err != nil {
return nil, err
}
@@ -213,3 +236,21 @@ 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

@@ -8,7 +8,7 @@ type role
relations
define assignee: [user, serviceaccount]
type organization
type organisation
relations
define create: [role#assignee]
define read: [role#assignee]

View File

@@ -8,7 +8,6 @@ import (
authz "github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/SigNoz/signoz/pkg/factory"
@@ -142,18 +141,18 @@ func (server *Server) BatchCheck(ctx context.Context, tupleReq map[string]*openf
return response, nil
}
func (server *Server) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, _ authtypes.Relation, _ coretypes.Resource, _ []coretypes.Selector, roleSelectors []coretypes.Selector) error {
func (server *Server) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, _ authtypes.Relation, _ authtypes.Typeable, _ []authtypes.Selector, roleSelectors []authtypes.Selector) error {
subject := ""
switch claims.Principal {
case authtypes.PrincipalUser:
user, err := authtypes.NewSubject(coretypes.NewResourceUser(), claims.UserID, orgID, nil)
user, err := authtypes.NewSubject(authtypes.TypeableUser, claims.UserID, orgID, nil)
if err != nil {
return err
}
subject = user
case authtypes.PrincipalServiceAccount:
serviceAccount, err := authtypes.NewSubject(coretypes.NewResourceServiceAccount(), claims.ServiceAccountID, orgID, nil)
serviceAccount, err := authtypes.NewSubject(authtypes.TypeableServiceAccount, claims.ServiceAccountID, orgID, nil)
if err != nil {
return err
}
@@ -161,7 +160,10 @@ func (server *Server) CheckWithTupleCreation(ctx context.Context, claims authtyp
subject = serviceAccount
}
tupleSlice := authtypes.NewTuples(coretypes.NewResourceRole(), subject, authtypes.Relation{Verb: coretypes.VerbAssignee}, roleSelectors, orgID)
tupleSlice, err := authtypes.TypeableRole.Tuples(subject, authtypes.RelationAssignee, roleSelectors, orgID)
if err != nil {
return err
}
// Convert slice to map with generated IDs for internal use
tuples := make(map[string]*openfgav1.TupleKey, len(tupleSlice))
@@ -183,13 +185,16 @@ func (server *Server) CheckWithTupleCreation(ctx context.Context, claims authtyp
return errors.Newf(errors.TypeForbidden, authtypes.ErrCodeAuthZForbidden, "subjects are not authorized for requested access")
}
func (server *Server) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, _ authtypes.Relation, _ coretypes.Resource, _ []coretypes.Selector, roleSelectors []coretypes.Selector) error {
subject, err := authtypes.NewSubject(coretypes.NewResourceAnonymous(), coretypes.AnonymousUser.String(), orgID, nil)
func (server *Server) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, _ authtypes.Relation, _ authtypes.Typeable, _ []authtypes.Selector, roleSelectors []authtypes.Selector) error {
subject, err := authtypes.NewSubject(authtypes.TypeableAnonymous, authtypes.AnonymousUser.String(), orgID, nil)
if err != nil {
return err
}
tupleSlice := authtypes.NewTuples(coretypes.NewResourceRole(), subject, authtypes.Relation{Verb: coretypes.VerbAssignee}, roleSelectors, orgID)
tupleSlice, err := authtypes.TypeableRole.Tuples(subject, authtypes.RelationAssignee, roleSelectors, orgID)
if err != nil {
return err
}
// Convert slice to map with generated IDs for internal use
tuples := make(map[string]*openfgav1.TupleKey, len(tupleSlice))
@@ -288,7 +293,7 @@ func (server *Server) ReadTuples(ctx context.Context, tupleKey *openfgav1.ReadRe
return tuples, nil
}
func (server *Server) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType coretypes.Type) ([]*coretypes.Object, error) {
func (server *Server) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType authtypes.Type) ([]*authtypes.Object, error) {
storeID, modelID := server.getStoreIDandModelID()
response, err := server.openfgaServer.ListObjects(ctx, &openfgav1.ListObjectsRequest{
StoreId: storeID,
@@ -301,7 +306,7 @@ func (server *Server) ListObjects(ctx context.Context, subject string, relation
return nil, errors.Wrapf(err, errors.TypeInternal, authtypes.ErrCodeAuthZUnavailable, "cannot list objects for subject %s with relation %s for type %s", subject, relation.StringValue(), objectType.StringValue())
}
return coretypes.MustNewObjectsFromStringSlice(response.Objects), nil
return authtypes.MustNewObjectsFromStringSlice(response.Objects), nil
}
func (server *Server) getOrCreateStore(ctx context.Context, name string) (string, error) {

View File

@@ -18,7 +18,7 @@ func TestProviderStartStop(t *testing.T) {
providerSettings := instrumentationtest.New().ToProviderSettings()
sqlstore := sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherRegexp)
openfgaDataStore, err := NewSQLStore(sqlstore, authz.Config{OpenFGA: authz.OpenFGAConfig{MaxTuplesPerWrite: 100}})
openfgaDataStore, err := NewSQLStore(sqlstore)
require.NoError(t, err)
expectedModel := `module base

View File

@@ -1,7 +1,6 @@
package openfgaserver
import (
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/openfga/openfga/pkg/storage"
@@ -9,11 +8,11 @@ import (
"github.com/openfga/openfga/pkg/storage/sqlite"
)
func NewSQLStore(store sqlstore.SQLStore, config authz.Config) (storage.OpenFGADatastore, error) {
func NewSQLStore(store sqlstore.SQLStore) (storage.OpenFGADatastore, error) {
switch store.BunDB().Dialect().Name().String() {
case "sqlite":
return sqlite.NewWithDB(store.SQLDB(), &sqlcommon.Config{
MaxTuplesPerWriteField: config.OpenFGA.MaxTuplesPerWrite,
MaxTuplesPerWriteField: 100,
MaxTypesPerModelField: 100,
})

View File

@@ -9,7 +9,6 @@ import (
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
@@ -98,20 +97,25 @@ func (handler *handler) GetObjects(rw http.ResponseWriter, r *http.Request) {
render.Error(rw, errors.New(errors.TypeInvalidInput, authtypes.ErrCodeRoleInvalidInput, "relation is missing from the request"))
return
}
relation, err := coretypes.NewVerb(relationStr)
relation, err := authtypes.NewRelation(relationStr)
if err != nil {
render.Error(rw, err)
return
}
objects, err := handler.authz.GetObjects(ctx, valuer.MustNewUUID(claims.OrgID), roleID, authtypes.Relation{Verb: relation})
objects, err := handler.authz.GetObjects(ctx, valuer.MustNewUUID(claims.OrgID), roleID, relation)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, coretypes.NewObjectGroupsFromObjects(objects))
render.Success(rw, http.StatusOK, authtypes.NewGettableObjects(objects))
}
func (handler *handler) GetResources(rw http.ResponseWriter, r *http.Request) {
resources := handler.authz.GetResources(r.Context())
render.Success(rw, http.StatusOK, authtypes.NewGettableResources(resources))
}
func (handler *handler) List(rw http.ResponseWriter, r *http.Request) {
@@ -186,7 +190,7 @@ func (handler *handler) PatchObjects(rw http.ResponseWriter, r *http.Request) {
return
}
relation, err := coretypes.NewVerb(mux.Vars(r)["relation"])
relation, err := authtypes.NewRelation(mux.Vars(r)["relation"])
if err != nil {
render.Error(rw, err)
return
@@ -203,19 +207,19 @@ func (handler *handler) PatchObjects(rw http.ResponseWriter, r *http.Request) {
return
}
req := new(coretypes.PatchableObjects)
req := new(authtypes.PatchableObjects)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return
}
additions, deletions, err := coretypes.NewPatchableObjects(req.Additions, req.Deletions, relation)
additions, deletions, err := authtypes.NewPatchableObjects(req.Additions, req.Deletions, relation)
if err != nil {
render.Error(rw, err)
return
}
err = handler.authz.PatchObjects(ctx, valuer.MustNewUUID(claims.OrgID), role.Name, authtypes.Relation{Verb: relation}, additions, deletions)
err = handler.authz.PatchObjects(ctx, valuer.MustNewUUID(claims.OrgID), role.Name, relation, additions, deletions)
if err != nil {
render.Error(rw, err)
return
@@ -262,7 +266,7 @@ func (handler *handler) Check(rw http.ResponseWriter, r *http.Request) {
}
orgID := valuer.MustNewUUID(claims.OrgID)
subject, err := authtypes.NewSubject(coretypes.NewResourceUser(), claims.UserID, orgID, nil)
subject, err := authtypes.NewSubject(authtypes.TypeableUser, claims.UserID, orgID, nil)
if err != nil {
render.Error(rw, err)
return

View File

@@ -1,99 +0,0 @@
package v2
import (
"regexp"
"sync"
"time"
)
// Code is a dotted, hierarchical identifier registered at process start. It
// encodes domain (subsystem), op (verb), optional sub (qualifier), and a
// terminal reason. Codes are values; two Codes with the same string are equal
// by value and safe to compare with ==.
type Code struct{ s string }
// String returns the dotted code as it appears on the wire. Empty for the
// zero value.
func (c Code) String() string { return c.s }
// codePattern allows 2-4 dotted segments, each starting with a lowercase
// letter and continuing with [a-z0-9_]. One segment is too broad (use a
// domain prefix); five or more means the domain should be split.
var codePattern = regexp.MustCompile(`^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*){1,3}$`)
// Meta is the per-code default envelope applied by constructors before
// per-call options. Every field has a natural per-code default — an auth
// code always wants Reauthenticate, every documented code wants its docs
// URL — so the registry is the right place to declare them once.
type Meta struct {
Category Category
Fault Fault
Retry Retry
Remediation Remediation
Refs map[RefKind]string
}
// Retry tells the caller how and when to retry. After is meaningful only
// when Policy == RetryAfter.
type Retry struct {
Policy RetryPolicy
After time.Duration
}
var (
registryMu sync.RWMutex
registry = map[string]Meta{}
)
// Register installs a code with its default Meta and returns the Code value.
// It panics on a malformed code string or a duplicate registration — both
// indicate a programming error that must be caught at boot, not at first
// failure.
//
// Call from the owning domain's package init or top-level var block:
//
// var CodeUnknownFunction = errors.Register("query.parse.unknown_function", errors.Meta{
// Category: errors.CategoryInvalidInput,
// Fault: errors.FaultCaller,
// Retry: errors.Retry{Policy: errors.RetryAfterFix},
// })
func Register(s string, meta Meta) Code {
if !codePattern.MatchString(s) {
panic("errors/v2: malformed code: " + s)
}
registryMu.Lock()
defer registryMu.Unlock()
if _, ok := registry[s]; ok {
panic("errors/v2: duplicate code: " + s)
}
registry[s] = meta
return Code{s: s}
}
// MetaOf returns the Meta a code was registered with. Returns the zero Meta
// and false for unregistered or zero codes.
func MetaOf(c Code) (Meta, bool) {
if c.s == "" {
return Meta{}, false
}
registryMu.RLock()
defer registryMu.RUnlock()
m, ok := registry[c.s]
return m, ok
}
// registerOrGet is the internal idempotent register used by adapters that
// may see the same code (e.g. legacy.<v1>) more than once across the process
// lifetime. It panics on malformed codes — duplicate codes silently keep the
// existing Meta.
func registerOrGet(s string, meta Meta) Code {
if !codePattern.MatchString(s) {
panic("errors/v2: malformed code: " + s)
}
registryMu.Lock()
defer registryMu.Unlock()
if _, ok := registry[s]; !ok {
registry[s] = meta
}
return Code{s: s}
}

View File

@@ -1,93 +0,0 @@
package v2
// The enums in this file are closed sets. Each value is a package-level var of
// an unexported-field struct, so external code cannot synthesize new values —
// it must reference one of the defined ones. String() returns the stable
// snake_case wire name; once shipped, those names are append-only.
// Category groups errors by what kind of failure occurred. It is the coarsest
// branch-worthy axis and is intended to be a superset of gRPC status codes
// extended with cases SigNoz cares about (e.g. license issues land under
// FailedDependency or ResourceExhausted depending on context).
type Category struct{ s string }
func (c Category) String() string { return c.s }
var (
CategoryInvalidInput = Category{"invalid_input"} // request was malformed or violated a documented constraint.
CategoryNotFound = Category{"not_found"} // referenced resource does not exist.
CategoryAlreadyExists = Category{"already_exists"} // resource creation conflicts with an existing one.
CategoryConflict = Category{"conflict"} // concurrent modification or state mismatch (e.g. stale revision).
CategoryPrecondition = Category{"precondition"} // a required precondition (system or caller-asserted) was not met.
CategoryUnauthenticated = Category{"unauthenticated"} // credentials are missing or invalid.
CategoryForbidden = Category{"forbidden"} // authenticated but not authorized for this action.
CategoryResourceExhausted = Category{"resource_exhausted"} // quota, rate limit, or other budget exceeded.
CategoryFailedDependency = Category{"failed_dependency"} // an upstream service we depend on failed (db, license, etc.).
CategoryUnavailable = Category{"unavailable"} // service is temporarily down; retry with backoff.
CategoryTimeout = Category{"timeout"} // deadline exceeded before the operation completed.
CategoryCanceled = Category{"canceled"} // caller or context canceled the operation.
CategoryUnimplemented = Category{"unimplemented"} // operation is not supported (or not yet) by this server.
CategoryDataLoss = Category{"data_loss"} // unrecoverable data corruption or loss detected.
CategoryInternal = Category{"internal"} // bug — invariant broken; should not occur in normal operation.
)
// Fault attributes responsibility. An agent uses this to decide whether to
// fix the request (Caller), retry/escalate (Server, Upstream), or page a
// human (Operator).
type Fault struct{ s string }
func (f Fault) String() string { return f.s }
var (
FaultCaller = Fault{"caller"}
FaultServer = Fault{"server"}
FaultUpstream = Fault{"upstream"}
FaultOperator = Fault{"operator"}
)
// RetryPolicy tells the caller how to behave on retry. Backoff implies the
// caller should use its own backoff schedule; After means honor Retry.After
// exactly; AfterFix and AfterAuth signal that retry is pointless until the
// caller fixes the request or re-authenticates.
type RetryPolicy struct{ s string }
func (r RetryPolicy) String() string { return r.s }
var (
RetryNever = RetryPolicy{"never"}
RetryImmediate = RetryPolicy{"immediate"}
RetryBackoff = RetryPolicy{"backoff"}
RetryAfter = RetryPolicy{"after"}
RetryAfterFix = RetryPolicy{"after_fix"}
RetryAfterAuth = RetryPolicy{"after_auth"}
)
// Remediation names the single recommended next action. It does not execute.
type Remediation struct{ s string }
func (r Remediation) String() string { return r.s }
var (
RemediationNone = Remediation{"none"}
RemediationFixInput = Remediation{"fix_input"}
RemediationReauthenticate = Remediation{"reauthenticate"}
RemediationWaitAndRetry = Remediation{"wait_and_retry"}
RemediationFailover = Remediation{"failover"}
RemediationContactOperator = Remediation{"contact_operator"}
RemediationFileBug = Remediation{"file_bug"}
RemediationUpgradeLicense = Remediation{"upgrade_license"}
)
// RefKind classifies a reference URL attached to the error.
type RefKind struct{ s string }
func (r RefKind) String() string { return r.s }
var (
RefDocs = RefKind{"docs"}
RefRunbook = RefKind{"runbook"}
RefDashboard = RefKind{"dashboard"}
RefTrace = RefKind{"trace"}
RefSource = RefKind{"source"}
RefIssue = RefKind{"issue"}
)

View File

@@ -1,248 +0,0 @@
// Package v2 is the redesigned pkg/errors.
//
// Every branch-worthy field on the Error struct is a closed enum and every
// variable part is a typed key/value. The intent is to make errors first-class
// data for programmatic consumers — SDK clients, UI surfaces, alerting, and
// LLM agents — without sacrificing human readability.
//
// Domain and op are encoded into Code (e.g. "query.parse.unknown_function")
// rather than carried as separate struct fields. Frames[0] is the
// authoritative call-site location, captured at construction time.
package v2
import (
stderrors "errors"
"fmt"
"io"
"sort"
"strconv"
"strings"
)
// Error is the redesigned error value. *Error is the canonical form passed
// around — the zero value is unused, construct via New / Newf / Wrap / Wrapf.
//
// Frames are intentionally not a struct field: resolving captured PCs into
// func/file/line is the dominant construction cost, so we capture PCs eagerly
// at construction time (so the snapshot is faithful to the call site) and
// resolve them lazily via Frames() only when something actually inspects them.
type Error struct {
// WHAT
Category Category
Code Code
Title string
Detail string
// WHY / WHO
Cause error
Fault Fault
// WHAT NEXT
Retry Retry
Remediation Remediation
Refs map[RefKind]string
// CONTEXT
Attrs map[string]any
TraceID string
SpanID string
// stack is the captured PCs plus a memoized []Frame; never read directly,
// always go through Frames().
stack *frameStack
}
// Frames returns the captured stack, resolved to func/file/line on first
// access. Frames[0] is the constructor's caller. Safe for concurrent use.
func (e *Error) Frames() []Frame {
if e == nil {
return nil
}
return e.stack.frames()
}
// New creates an Error for a registered Code. Defaults from the registered
// Meta are applied first; opts override per call site.
func New(code Code, title string, opts ...Option) *Error {
e := &Error{Code: code, Title: title, stack: captureStack(3)}
applyMeta(e)
for _, opt := range opts {
opt(e)
}
return e
}
// Newf is New with fmt.Sprintf-style formatting for the title.
func Newf(code Code, format string, args ...any) *Error {
e := &Error{Code: code, Title: fmt.Sprintf(format, args...), stack: captureStack(3)}
applyMeta(e)
return e
}
// Wrap creates an Error that wraps cause. The new error's Title is the
// caller-supplied title (not the cause's message), so Error() reports what
// went wrong at this layer — the cause is reachable via Unwrap.
func Wrap(cause error, code Code, title string, opts ...Option) *Error {
e := &Error{Code: code, Title: title, Cause: cause, stack: captureStack(3)}
applyMeta(e)
for _, opt := range opts {
opt(e)
}
return e
}
// Wrapf is Wrap with fmt.Sprintf-style formatting for the title.
func Wrapf(cause error, code Code, format string, args ...any) *Error {
e := &Error{Code: code, Title: fmt.Sprintf(format, args...), Cause: cause, stack: captureStack(3)}
applyMeta(e)
return e
}
// applyMeta copies default values from the registered Meta into a fresh
// Error. It runs before per-call options so options win.
func applyMeta(e *Error) {
meta, ok := MetaOf(e.Code)
if !ok {
return
}
if (e.Category == Category{}) {
e.Category = meta.Category
}
if (e.Fault == Fault{}) {
e.Fault = meta.Fault
}
if (e.Retry == Retry{}) {
e.Retry = meta.Retry
}
if (e.Remediation == Remediation{}) {
e.Remediation = meta.Remediation
}
if len(meta.Refs) > 0 {
if e.Refs == nil {
e.Refs = make(map[RefKind]string, len(meta.Refs))
}
for k, v := range meta.Refs {
if _, exists := e.Refs[k]; !exists {
e.Refs[k] = v
}
}
}
}
// Error returns the Title (the message specifically attached at this wrap
// site), not the cause's message. This fixes the v1 surprise where Error()
// returned the wrapped cause's text.
func (e *Error) Error() string {
if e == nil {
return "<nil>"
}
return e.Title
}
// Unwrap returns the wrapped cause, enabling errors.Is / errors.As.
func (e *Error) Unwrap() error {
if e == nil {
return nil
}
return e.Cause
}
// Format implements fmt.Formatter.
//
// %s, %v → Title only
// %+v → full chain: code, title, frames, attrs, recursive cause
func (e *Error) Format(f fmt.State, verb rune) {
switch verb {
case 's':
_, _ = io.WriteString(f, e.Title)
case 'v':
if f.Flag('+') {
_, _ = io.WriteString(f, e.fullString())
return
}
_, _ = io.WriteString(f, e.Title)
case 'q':
fmt.Fprintf(f, "%q", e.Title)
default:
fmt.Fprintf(f, "%%!%c(*errors/v2.Error)", verb)
}
}
// fullString produces the %+v rendering. Format is intentionally
// human-readable rather than machine-parseable; consumers that want structure
// should marshal to JSON.
func (e *Error) fullString() string {
var b strings.Builder
e.appendFull(&b, 0)
return b.String()
}
func (e *Error) appendFull(b *strings.Builder, depth int) {
indent := strings.Repeat(" ", depth)
fmt.Fprintf(b, "%s[%s] %s\n", indent, e.Code.s, e.Title)
if e.Detail != "" {
fmt.Fprintf(b, "%s detail: %s\n", indent, e.Detail)
}
if len(e.Attrs) > 0 {
// Stable key order for deterministic output.
keys := make([]string, 0, len(e.Attrs))
for k := range e.Attrs {
keys = append(keys, k)
}
sort.Strings(keys)
fmt.Fprintf(b, "%s attrs:\n", indent)
for _, k := range keys {
fmt.Fprintf(b, "%s %s=%v\n", indent, k, e.Attrs[k])
}
}
if frames := e.Frames(); len(frames) > 0 {
fmt.Fprintf(b, "%s frames:\n", indent)
for _, fr := range frames {
fmt.Fprintf(b, "%s %s\n%s %s:%s\n", indent, fr.Func, indent, fr.File, strconv.Itoa(fr.Line))
}
}
if e.Cause != nil {
fmt.Fprintf(b, "%scaused by:\n", indent)
var ce *Error
if stderrors.As(e.Cause, &ce) && ce != nil {
ce.appendFull(b, depth+1)
} else {
fmt.Fprintf(b, "%s %s\n", indent, e.Cause.Error())
}
}
}
// AsError extracts a *Error from anywhere in err's wrap chain. It is the
// common shortcut around errors.As for code that always wants this package's
// type.
func AsError(err error) (*Error, bool) {
if err == nil {
return nil, false
}
var e *Error
if stderrors.As(err, &e) {
return e, true
}
return nil, false
}
// Is reports whether err or any error in its chain has the given Code.
// Convenience wrapper that's friendlier than errors.As at call sites that
// only care about code identity.
func Is(err error, code Code) bool {
e, ok := AsError(err)
if !ok {
return false
}
for e != nil {
if e.Code == code {
return true
}
next, ok := AsError(e.Cause)
if !ok {
return false
}
e = next
}
return false
}

View File

@@ -1,90 +0,0 @@
package v2
// This file is a self-contained walkthrough of how a domain integrates with
// pkg/errors/v2. It mirrors what a real pkg/<domain>/errors.go looks like in
// practice — registering codes, constructing typed errors at failure sites,
// and consuming them at API boundaries. The "example.*" namespace is reserved
// for these demo codes so they never collide with a real domain's
// registrations.
// 1. Register codes at package init time. Each Register call panics on
// malformed code or duplicate registration, so misconfiguration is caught
// at process boot, not at first failure.
var (
// A caller-fault, fix-the-input error: rejected before any work happens.
exampleCodeInvalidQuery = Register("example.query.invalid_filter", Meta{
Category: CategoryInvalidInput,
Fault: FaultCaller,
Remediation: RemediationFixInput,
Retry: Retry{Policy: RetryAfterFix},
Refs: map[RefKind]string{
RefDocs: "https://signoz.io/docs/query/filters",
},
})
// A quota error: the caller's request was well-formed but their plan
// doesn't allow it. The recommended remediation is structural (upgrade),
// not "try again later."
exampleCodeQuotaExceeded = Register("example.billing.quota_exceeded", Meta{
Category: CategoryResourceExhausted,
Fault: FaultCaller,
Remediation: RemediationUpgradeLicense,
Retry: Retry{Policy: RetryNever},
})
)
// 2. Construct errors at the failure site. Notice that variable parts of
// the message (the offending field, the limits) live in typed Attrs, not in
// the title prose — a downstream agent can read them without parsing English.
func exampleRejectInvalidFilter(field string) *Error {
return New(exampleCodeInvalidQuery, "filter is not supported",
WithAttr("field", field),
)
}
// 3. Consume errors at the API boundary. Branching on Category gives the
// HTTP status; Retry tells an SDK how to behave; Fault drives logging
// classification (caller errors are warnings, server/upstream errors page).
func exampleClassifyForHTTP(err error) (status int, retry RetryPolicy) {
e, ok := AsError(err)
if !ok {
return 500, RetryNever
}
switch e.Category {
case CategoryInvalidInput, CategoryPrecondition:
status = 400
case CategoryUnauthenticated:
status = 401
case CategoryForbidden:
status = 403
case CategoryNotFound:
status = 404
case CategoryConflict, CategoryAlreadyExists:
status = 409
case CategoryResourceExhausted:
status = 429
case CategoryUnavailable, CategoryTimeout:
status = 503
case CategoryUnimplemented:
status = 501
default:
status = 500
}
return status, e.Retry.Policy
}
// 4. Identify a specific failure mode by Code. Is walks the cause chain so
// a wrapper at the HTTP layer still matches when the root cause was raised
// deep in the call graph.
func exampleIsQuotaExceeded(err error) bool {
return Is(err, exampleCodeQuotaExceeded)
}
// The example helpers are reference-only: they exist to document call-site
// patterns, not to be called from anywhere in the binary. This anchor keeps
// them visible to readers (and the linter) without exporting demo code.
var _ = []any{
exampleRejectInvalidFilter,
exampleClassifyForHTTP,
exampleIsQuotaExceeded,
}

View File

@@ -1,65 +0,0 @@
package v2
import (
"runtime"
"sync"
)
// Frame is a single line in the call stack. Frames[0] is the constructor's
// caller — the authoritative "where this error came from" — and downstream
// consumers can filter (e.g. "frames inside our code") without regex
// reparsing of a pre-formatted stack string.
type Frame struct {
Func string `json:"func,omitempty"`
File string `json:"file,omitempty"`
Line int `json:"line,omitempty"`
}
// frameStack carries the PCs captured at construction plus the resolved
// []Frame slice, behind a sync.Once. Resolving frames into func/file/line is
// expensive (runtime.CallersFrames walks the symbol table); the vast majority
// of errors are constructed and never inspected, so we only pay that cost
// when a consumer actually asks for frames (Frames()/MarshalJSON/%+v).
//
// The PC capture itself is cheap and happens at construction so that
// Frames[0] is a faithful "where" record of the original call site.
type frameStack struct {
pcs []uintptr
once sync.Once
resolved []Frame
}
// captureStack is called by every constructor. skip drops runtime.Callers,
// captureStack itself, and the constructor frame so that the first PC is the
// user code that invoked the constructor.
func captureStack(skip int) *frameStack {
const depth = 32
pcs := make([]uintptr, depth)
n := runtime.Callers(skip, pcs)
if n == 0 {
return nil
}
return &frameStack{pcs: pcs[:n:n]}
}
// frames resolves the captured PCs into []Frame. The resolution is memoized
// — concurrent calls are safe and only one of them does the work.
func (s *frameStack) frames() []Frame {
if s == nil {
return nil
}
s.once.Do(func() {
cf := runtime.CallersFrames(s.pcs)
out := make([]Frame, 0, len(s.pcs))
for {
f, more := cf.Next()
out = append(out, Frame{Func: f.Function, File: f.File, Line: f.Line})
if !more {
break
}
}
s.resolved = out
})
return s.resolved
}

View File

@@ -1,175 +0,0 @@
package v2
import (
"encoding/json"
"net/url"
)
// CodeUnknown is the sentinel returned when AsJSON / AsURLValues are called
// on a non-*Error. A consumer that sees this on the wire should read it as
// "the producer did not raise a v2 Error and we projected it through the
// fallback path" — i.e. somewhere upstream is still using std errors or v1.
var CodeUnknown = Register("unknown.unset", Meta{
Category: CategoryInternal,
Fault: FaultServer,
Retry: Retry{Policy: RetryNever},
})
// JSON is the wire envelope for an Error. It is intentionally a superset of
// v1's pkg/errors.JSON: SDK clients that only read v1's {code, message, url,
// errors[]} keep working, while v2 consumers can branch on the new typed
// fields (category, fault, retry, remediation, attrs, refs, cause).
type JSON struct {
Code string `json:"code" required:"true"`
Title string `json:"title" required:"true"`
Detail string `json:"detail,omitempty"`
Category string `json:"category,omitempty"`
Fault string `json:"fault,omitempty"`
Retry *RetryJSON `json:"retry,omitempty"`
Remediation string `json:"remediation,omitempty"`
Attrs map[string]any `json:"attrs,omitempty"`
Refs map[string]string `json:"refs,omitempty"`
Frames []Frame `json:"frames,omitempty"`
TraceID string `json:"trace_id,omitempty"`
SpanID string `json:"span_id,omitempty"`
Cause *CauseJSON `json:"cause,omitempty"`
}
// RetryJSON renders Retry as an object so consumers can branch on policy
// before consulting AfterMS. AfterMS is omitted unless policy is "after".
type RetryJSON struct {
Policy string `json:"policy"`
AfterMS int64 `json:"after_ms,omitempty"`
}
// CauseJSON is the thin recursive shape for a cause chain. Only code, title,
// and a nested cause are guaranteed — producers may add more, consumers must
// not rely on it.
type CauseJSON struct {
Code string `json:"code,omitempty"`
Title string `json:"title"`
Cause *CauseJSON `json:"cause,omitempty"`
}
// AsJSON projects any error onto the v2 wire envelope. If cause is a
// *Error (anywhere in its wrap chain) every field is filled from it;
// otherwise the result is a CodeUnknown envelope with Title=cause.Error()
// so the wire shape is always valid and never panics.
func AsJSON(cause error) *JSON {
if cause == nil {
return nil
}
e, ok := AsError(cause)
if !ok {
return &JSON{
Code: CodeUnknown.s,
Title: cause.Error(),
Category: CategoryInternal.s,
Fault: FaultServer.s,
}
}
return errorToJSON(e)
}
func errorToJSON(e *Error) *JSON {
out := &JSON{
Code: e.Code.s,
Title: e.Title,
Detail: e.Detail,
Category: e.Category.s,
Fault: e.Fault.s,
Remediation: e.Remediation.s,
Attrs: e.Attrs,
TraceID: e.TraceID,
SpanID: e.SpanID,
}
if (e.Retry.Policy != RetryPolicy{}) {
out.Retry = &RetryJSON{Policy: e.Retry.Policy.s}
if e.Retry.Policy == RetryAfter && e.Retry.After > 0 {
out.Retry.AfterMS = e.Retry.After.Milliseconds()
}
}
if len(e.Refs) > 0 {
out.Refs = make(map[string]string, len(e.Refs))
for k, v := range e.Refs {
out.Refs[k.s] = v
}
}
if frames := e.Frames(); len(frames) > 0 {
out.Frames = frames
}
if e.Cause != nil {
out.Cause = causeToJSON(e.Cause)
}
return out
}
func causeToJSON(err error) *CauseJSON {
if err == nil {
return nil
}
if e, ok := err.(*Error); ok {
c := &CauseJSON{Code: e.Code.s, Title: e.Title}
if e.Cause != nil {
c.Cause = causeToJSON(e.Cause)
}
return c
}
// Non-*Error leaf: only Title is set, no Code.
return &CauseJSON{Title: err.Error()}
}
// AsURLValues projects an error onto a flat url.Values, matching v1's shape
// for callers (e.g. OAuth/SSO redirects) that smuggle errors back through a
// query string. Complex fields (attrs, refs, retry, frames, cause) are
// JSON-marshaled into a single value rather than spread across multiple
// keys, since query strings have no good representation for nested data.
func AsURLValues(cause error) url.Values {
j := AsJSON(cause)
if j == nil {
return url.Values{}
}
v := url.Values{
"code": {j.Code},
"title": {j.Title},
}
if j.Detail != "" {
v.Set("detail", j.Detail)
}
if j.Category != "" {
v.Set("category", j.Category)
}
if j.Fault != "" {
v.Set("fault", j.Fault)
}
if j.Remediation != "" {
v.Set("remediation", j.Remediation)
}
if j.TraceID != "" {
v.Set("trace_id", j.TraceID)
}
if j.SpanID != "" {
v.Set("span_id", j.SpanID)
}
if j.Retry != nil {
if b, err := json.Marshal(j.Retry); err == nil {
v.Set("retry", string(b))
}
}
if len(j.Refs) > 0 {
if b, err := json.Marshal(j.Refs); err == nil {
v.Set("refs", string(b))
}
}
if len(j.Attrs) > 0 {
if b, err := json.Marshal(j.Attrs); err == nil {
v.Set("attrs", string(b))
}
}
if j.Cause != nil {
if b, err := json.Marshal(j.Cause); err == nil {
v.Set("cause", string(b))
}
}
return v
}

View File

@@ -1,85 +0,0 @@
package v2
import "time"
// Option mutates an Error during construction. Options are applied after the
// registered Meta defaults so a per-call WithFault wins over the code's
// default Fault.
type Option func(*Error)
// WithTitle overrides the title (used when Newf's formatted string is not
// what you want, or after a Wrap that took its title from the cause).
func WithTitle(s string) Option { return func(e *Error) { e.Title = s } }
// WithDetail adds a long, user-safe explanation. Detail must never include
// raw cause text; the cause is already in the chain.
func WithDetail(s string) Option { return func(e *Error) { e.Detail = s } }
// WithCategory overrides the registered Category.
func WithCategory(c Category) Option { return func(e *Error) { e.Category = c } }
// WithFault overrides the registered Fault.
func WithFault(f Fault) Option { return func(e *Error) { e.Fault = f } }
// WithRetry overrides the registered Retry.
func WithRetry(r Retry) Option { return func(e *Error) { e.Retry = r } }
// WithRetryAfter is a convenience for the common RetryAfter case.
func WithRetryAfter(d time.Duration) Option {
return func(e *Error) { e.Retry = Retry{Policy: RetryAfter, After: d} }
}
// WithRemediation overrides the registered Remediation.
//
// Convention for "did you mean" hints: stash a []string under
// Attrs["suggestions"], ranked best-first. Each element should be a complete,
// copy-pasteable replacement — not an explanation of what went wrong (use
// WithDetail for that). Once 3-4 domains adopt the convention identically,
// promote to a first-class field.
func WithRemediation(r Remediation) Option { return func(e *Error) { e.Remediation = r } }
// WithRef adds (or replaces) a single reference URL keyed by kind.
func WithRef(kind RefKind, url string) Option {
return func(e *Error) {
if e.Refs == nil {
e.Refs = make(map[RefKind]string, 1)
}
e.Refs[kind] = url
}
}
// WithAttr sets a single typed attribute. Prefer typed per-domain helpers
// (e.g. WithQueryAttrs(q Query)) over raw WithAttr at call sites — they keep
// the attr keys consistent and let the compiler reject typos.
func WithAttr(key string, value any) Option {
return func(e *Error) {
if e.Attrs == nil {
e.Attrs = make(map[string]any, 1)
}
e.Attrs[key] = value
}
}
// WithAttrs merges a map of attributes; later keys win.
func WithAttrs(attrs map[string]any) Option {
return func(e *Error) {
if len(attrs) == 0 {
return
}
if e.Attrs == nil {
e.Attrs = make(map[string]any, len(attrs))
}
for k, v := range attrs {
e.Attrs[k] = v
}
}
}
// WithTrace stamps the error with OTel trace and span IDs so the JSON
// response can link back to the originating span.
func WithTrace(traceID, spanID string) Option {
return func(e *Error) {
e.TraceID = traceID
e.SpanID = spanID
}
}

View File

@@ -171,7 +171,7 @@ func TestAwaitHealthy(t *testing.T) {
func TestAwaitHealthyWithFailure(t *testing.T) {
s1 := &failingHealthyService{
healthyC: make(chan struct{}),
err: errors.Newf(errors.TypeInternal, errors.CodeInternal, "startup failed"),
err: errors.Newf(errors.TypeInternal, errors.CodeInternal,"startup failed"),
}
registry, err := NewRegistry(context.Background(), slog.New(slog.DiscardHandler), NewNamedService(MustNewName("s1"), s1))
@@ -245,7 +245,7 @@ func TestDependsOnStartsAfterDependency(t *testing.T) {
func TestDependsOnFailsWhenDependencyFails(t *testing.T) {
s1 := &failingHealthyService{
healthyC: make(chan struct{}),
err: errors.Newf(errors.TypeInternal, errors.CodeInternal, "s1 crashed"),
err: errors.Newf(errors.TypeInternal, errors.CodeInternal,"s1 crashed"),
}
s2 := newTestService(t)
@@ -291,7 +291,7 @@ func TestDependsOnUnknownServiceIsIgnored(t *testing.T) {
func TestServiceStateFailed(t *testing.T) {
s1 := &failingHealthyService{
healthyC: make(chan struct{}),
err: errors.Newf(errors.TypeInternal, errors.CodeInternal, "fatal error"),
err: errors.Newf(errors.TypeInternal, errors.CodeInternal,"fatal error"),
}
registry, err := NewRegistry(context.Background(), slog.New(slog.DiscardHandler), NewNamedService(MustNewName("s1"), s1))

View File

@@ -15,24 +15,12 @@ func TestQueryBinding_BindQuery(t *testing.T) {
obj any
expected any
}{
{name: "SingleIntField_NonEmptyValue", query: map[string][]string{"a": {"1"}}, obj: &struct {
A int `query:"a"`
}{}, expected: &struct{ A int }{A: 1}},
{name: "SingleIntField_EmptyValue", query: map[string][]string{"a": {""}}, obj: &struct {
A int `query:"a"`
}{}, expected: &struct{ A int }{A: 0}},
{name: "SingleIntField_MissingField", query: map[string][]string{}, obj: &struct {
A int `query:"a"`
}{}, expected: &struct{ A int }{A: 0}},
{name: "SinglePointerIntField_NonEmptyValue", query: map[string][]string{"a": {"1"}}, obj: &struct {
A *int `query:"a"`
}{}, expected: &struct{ A *int }{A: &one}},
{name: "SinglePointerIntField_EmptyValue", query: map[string][]string{"a": {""}}, obj: &struct {
A *int `query:"a"`
}{}, expected: &struct{ A *int }{A: &zero}},
{name: "SinglePointerIntField_MissingField", query: map[string][]string{}, obj: &struct {
A *int `query:"a"`
}{}, expected: &struct{ A *int }{A: nil}},
{name: "SingleIntField_NonEmptyValue", query: map[string][]string{"a": {"1"}}, obj: &struct{A int `query:"a"`}{}, expected: &struct{ A int }{A: 1}},
{name: "SingleIntField_EmptyValue", query: map[string][]string{"a": {""}}, obj: &struct{A int `query:"a"`}{}, expected: &struct{ A int }{A: 0}},
{name: "SingleIntField_MissingField", query: map[string][]string{}, obj: &struct{A int `query:"a"`}{}, expected: &struct{ A int }{A: 0}},
{name: "SinglePointerIntField_NonEmptyValue", query: map[string][]string{"a": {"1"}}, obj: &struct{A *int `query:"a"`}{}, expected: &struct{ A *int }{A: &one}},
{name: "SinglePointerIntField_EmptyValue", query: map[string][]string{"a": {""}}, obj: &struct{A *int `query:"a"`}{}, expected: &struct{ A *int }{A: &zero}},
{name: "SinglePointerIntField_MissingField", query: map[string][]string{}, obj: &struct{A *int `query:"a"`}{}, expected: &struct{ A *int }{A: nil}},
}
for _, testCase := range testCases {

View File

@@ -2,15 +2,14 @@ package handler
import (
"github.com/SigNoz/signoz/pkg/types/audittypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
)
// Option configures optional behaviour on a handler created by New.
type Option func(*handler)
type AuditDef struct {
ResourceKind coretypes.Kind // Typeable.Kind() value, e.g. "dashboard", "user".
Action coretypes.Verb // create, update, delete, etc.
ResourceKind string // AuthZ Typeable.Kind() value, e.g. "dashboard", "user".
Action audittypes.Action // create, update, delete, login, etc.
Category audittypes.ActionCategory // access_control, configuration_change, etc.
ResourceIDParam string // Gorilla mux path param name for the resource ID.
}

View File

@@ -9,7 +9,6 @@ import (
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
@@ -41,18 +40,18 @@ func (middleware *AuthZ) ViewAccess(next http.HandlerFunc) http.HandlerFunc {
return
}
selectors := []coretypes.Selector{
coretypes.TypeRole.MustSelector(authtypes.SigNozAdminRoleName),
coretypes.TypeRole.MustSelector(authtypes.SigNozEditorRoleName),
coretypes.TypeRole.MustSelector(authtypes.SigNozViewerRoleName),
selectors := []authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozEditorRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozViewerRoleName),
}
err = middleware.authzService.CheckWithTupleCreation(
ctx,
claims,
valuer.MustNewUUID(claims.OrgID),
authtypes.Relation{Verb: coretypes.VerbAssignee},
coretypes.NewResourceRole(),
authtypes.RelationAssignee,
authtypes.TypeableRole,
selectors,
selectors,
)
@@ -80,17 +79,17 @@ func (middleware *AuthZ) EditAccess(next http.HandlerFunc) http.HandlerFunc {
return
}
selectors := []coretypes.Selector{
coretypes.TypeRole.MustSelector(authtypes.SigNozAdminRoleName),
coretypes.TypeRole.MustSelector(authtypes.SigNozEditorRoleName),
selectors := []authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozEditorRoleName),
}
err = middleware.authzService.CheckWithTupleCreation(
ctx,
claims,
valuer.MustNewUUID(claims.OrgID),
authtypes.Relation{Verb: coretypes.VerbAssignee},
coretypes.NewResourceRole(),
authtypes.RelationAssignee,
authtypes.TypeableRole,
selectors,
selectors,
)
@@ -118,16 +117,16 @@ func (middleware *AuthZ) AdminAccess(next http.HandlerFunc) http.HandlerFunc {
return
}
selectors := []coretypes.Selector{
coretypes.TypeRole.MustSelector(authtypes.SigNozAdminRoleName),
selectors := []authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName),
}
err = middleware.authzService.CheckWithTupleCreation(
ctx,
claims,
valuer.MustNewUUID(claims.OrgID),
authtypes.Relation{Verb: coretypes.VerbAssignee},
coretypes.NewResourceRole(),
authtypes.RelationAssignee,
authtypes.TypeableRole,
selectors,
selectors,
)
@@ -154,16 +153,16 @@ func (middleware *AuthZ) SelfAccess(next http.HandlerFunc) http.HandlerFunc {
return
}
selectors := []coretypes.Selector{
coretypes.TypeRole.MustSelector(authtypes.SigNozAdminRoleName),
selectors := []authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName),
}
err = middleware.authzService.CheckWithTupleCreation(
req.Context(),
claims,
valuer.MustNewUUID(claims.OrgID),
authtypes.Relation{Verb: coretypes.VerbAssignee},
coretypes.NewResourceRole(),
authtypes.RelationAssignee,
authtypes.TypeableRole,
selectors,
selectors,
)
@@ -186,7 +185,7 @@ func (middleware *AuthZ) OpenAccess(next http.HandlerFunc) http.HandlerFunc {
})
}
func (middleware *AuthZ) Check(next http.HandlerFunc, relation authtypes.Relation, typeable coretypes.Resource, cb authtypes.SelectorCallbackWithClaimsFn, roles []string) http.HandlerFunc {
func (middleware *AuthZ) Check(next http.HandlerFunc, relation authtypes.Relation, typeable authtypes.Typeable, cb authtypes.SelectorCallbackWithClaimsFn, roles []string) http.HandlerFunc {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
@@ -201,9 +200,9 @@ func (middleware *AuthZ) Check(next http.HandlerFunc, relation authtypes.Relatio
return
}
roleSelectors := []coretypes.Selector{}
roleSelectors := []authtypes.Selector{}
for _, role := range roles {
roleSelectors = append(roleSelectors, coretypes.TypeRole.MustSelector(role))
roleSelectors = append(roleSelectors, authtypes.MustNewSelector(authtypes.TypeRole, role))
}
err = middleware.authzService.CheckWithTupleCreation(ctx, claims, valuer.MustNewUUID(claims.OrgID), relation, typeable, selectors, roleSelectors)
@@ -216,7 +215,7 @@ func (middleware *AuthZ) Check(next http.HandlerFunc, relation authtypes.Relatio
})
}
func (middleware *AuthZ) CheckWithoutClaims(next http.HandlerFunc, relation authtypes.Relation, typeable coretypes.Resource, cb authtypes.SelectorCallbackWithoutClaimsFn, roles []string) http.HandlerFunc {
func (middleware *AuthZ) CheckWithoutClaims(next http.HandlerFunc, relation authtypes.Relation, typeable authtypes.Typeable, cb authtypes.SelectorCallbackWithoutClaimsFn, roles []string) http.HandlerFunc {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
orgs, err := middleware.orgGetter.ListByOwnedKeyRange(ctx)
@@ -231,9 +230,9 @@ func (middleware *AuthZ) CheckWithoutClaims(next http.HandlerFunc, relation auth
return
}
roleSelectors := []coretypes.Selector{}
roleSelectors := []authtypes.Selector{}
for _, role := range roles {
roleSelectors = append(roleSelectors, coretypes.TypeRole.MustSelector(role))
roleSelectors = append(roleSelectors, authtypes.MustNewSelector(authtypes.TypeRole, role))
}
err = middleware.authzService.CheckWithTupleCreationWithoutClaims(ctx, orgId, relation, typeable, selectors, roleSelectors)

View File

@@ -4,9 +4,10 @@ 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/coretypes"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -26,7 +27,7 @@ type Module interface {
GetPublicWidgetQueryRange(context.Context, valuer.UUID, uint64, uint64, uint64) (*querybuildertypesv5.QueryRangeResponse, error)
// gets the selectors and org for the given public dashboard
GetPublicDashboardSelectorsAndOrg(context.Context, valuer.UUID, []*types.Organization) ([]coretypes.Selector, valuer.UUID, error)
GetPublicDashboardSelectorsAndOrg(context.Context, valuer.UUID, []*types.Organization) ([]authtypes.Selector, valuer.UUID, error)
// updates the public sharing config for a dashboard
UpdatePublic(context.Context, valuer.UUID, *dashboardtypes.PublicDashboard) error
@@ -49,6 +50,8 @@ 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

@@ -16,7 +16,6 @@ import (
"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/coretypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
@@ -159,15 +158,15 @@ func (handler *handler) LockUnlock(rw http.ResponseWriter, r *http.Request) {
}
isAdmin := false
selectors := []coretypes.Selector{
coretypes.TypeRole.MustSelector(authtypes.SigNozAdminRoleName),
selectors := []authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName),
}
err = handler.authz.CheckWithTupleCreation(
ctx,
claims,
valuer.MustNewUUID(claims.OrgID),
authtypes.Relation{Verb: coretypes.VerbAssignee},
coretypes.NewResourceRole(),
authtypes.RelationAssignee,
authtypes.TypeableRole,
selectors,
selectors,
)

View File

@@ -12,7 +12,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -202,6 +202,14 @@ 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")
@@ -219,7 +227,7 @@ func (module *module) GetPublicWidgetQueryRange(context.Context, valuer.UUID, ui
return nil, errors.Newf(errors.TypeUnsupported, dashboardtypes.ErrCodePublicDashboardUnsupported, "not implemented")
}
func (module *module) GetPublicDashboardSelectorsAndOrg(_ context.Context, _ valuer.UUID, _ []*types.Organization) ([]coretypes.Selector, valuer.UUID, error) {
func (module *module) GetPublicDashboardSelectorsAndOrg(_ context.Context, _ valuer.UUID, _ []*types.Organization) ([]authtypes.Selector, valuer.UUID, error) {
return nil, valuer.UUID{}, errors.Newf(errors.TypeUnsupported, dashboardtypes.ErrCodePublicDashboardUnsupported, "not implemented")
}

View File

@@ -12,7 +12,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/cachetypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/serviceaccounttypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -145,7 +144,7 @@ func (module *module) DeleteRole(ctx context.Context, orgID valuer.UUID, id valu
return err
}
err = module.authz.Revoke(ctx, orgID, []string{role.Name}, authtypes.MustNewSubject(coretypes.NewResourceServiceAccount(), id.String(), orgID, nil))
err = module.authz.Revoke(ctx, orgID, []string{role.Name}, authtypes.MustNewSubject(authtypes.TypeableServiceAccount, id.String(), orgID, nil))
if err != nil {
return err
}
@@ -188,7 +187,7 @@ func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.U
return err
}
err = module.authz.Revoke(ctx, orgID, serviceAccount.RoleNames(), authtypes.MustNewSubject(coretypes.NewResourceServiceAccount(), id.StringValue(), orgID, nil))
err = module.authz.Revoke(ctx, orgID, serviceAccount.RoleNames(), authtypes.MustNewSubject(authtypes.TypeableServiceAccount, id.StringValue(), orgID, nil))
if err != nil {
return err
}
@@ -387,7 +386,7 @@ func (module *module) setRole(ctx context.Context, orgID valuer.UUID, id valuer.
return err
}
err = module.authz.ModifyGrant(ctx, orgID, serviceAccount.RoleNames(), []string{role.Name}, authtypes.MustNewSubject(coretypes.NewResourceServiceAccount(), id.String(), orgID, nil))
err = module.authz.ModifyGrant(ctx, orgID, serviceAccount.RoleNames(), []string{role.Name}, authtypes.MustNewSubject(authtypes.TypeableServiceAccount, id.String(), orgID, nil))
if err != nil {
return err
}

View File

@@ -2,9 +2,8 @@ package tracefunnel
import (
"context"
"net/http"
"github.com/SigNoz/signoz/pkg/valuer"
"net/http"
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
)

View File

@@ -11,7 +11,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -44,8 +43,8 @@ func NewService(
orgGetter: orgGetter,
authz: authz,
config: config,
stopC: make(chan struct{}),
healthyC: make(chan struct{}),
stopC: make(chan struct{}),
healthyC: make(chan struct{}),
}
}
@@ -149,7 +148,7 @@ func (s *service) createOrPromoteRootUser(ctx context.Context, orgID valuer.UUID
orgID,
existingUserRoleNames,
[]string{authtypes.SigNozAdminRoleName},
authtypes.MustNewSubject(coretypes.NewResourceUser(), existingUser.ID.StringValue(), orgID, nil),
authtypes.MustNewSubject(authtypes.TypeableUser, existingUser.ID.StringValue(), orgID, nil),
); err != nil {
return err
}

View File

@@ -19,7 +19,6 @@ import (
"github.com/SigNoz/signoz/pkg/tokenizer"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/emailtypes"
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -177,7 +176,7 @@ func (module *setter) CreateUser(ctx context.Context, user *types.User, opts ...
ctx,
user.OrgID,
createUserOpts.RoleNames,
authtypes.MustNewSubject(coretypes.NewResourceUser(), user.ID.StringValue(), user.OrgID, nil),
authtypes.MustNewSubject(authtypes.TypeableUser, user.ID.StringValue(), user.OrgID, nil),
)
if err != nil {
return err
@@ -237,15 +236,15 @@ func (module *setter) UpdateUserDeprecated(ctx context.Context, orgID valuer.UUI
roleChange := user.Role != "" && user.Role != existingUser.Role
if roleChange {
selectors := []coretypes.Selector{
coretypes.TypeRole.MustSelector(authtypes.SigNozAdminRoleName),
selectors := []authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName),
}
err = module.authz.CheckWithTupleCreation(
ctx,
claims,
valuer.MustNewUUID(claims.OrgID),
authtypes.Relation{Verb: coretypes.VerbAssignee},
coretypes.NewResourceRole(),
authtypes.RelationAssignee,
authtypes.TypeableRole,
selectors,
selectors,
)
@@ -265,7 +264,7 @@ func (module *setter) UpdateUserDeprecated(ctx context.Context, orgID valuer.UUI
orgID,
[]string{authtypes.MustGetSigNozManagedRoleFromExistingRole(existingUser.Role)},
[]string{authtypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)},
authtypes.MustNewSubject(coretypes.NewResourceUser(), id, orgID, nil),
authtypes.MustNewSubject(authtypes.TypeableUser, id, orgID, nil),
)
if err != nil {
return nil, err
@@ -401,7 +400,7 @@ func (module *setter) DeleteUser(ctx context.Context, orgID valuer.UUID, id stri
ctx,
orgID,
roleNames,
authtypes.MustNewSubject(coretypes.NewResourceUser(), id, orgID, nil),
authtypes.MustNewSubject(authtypes.TypeableUser, id, orgID, nil),
)
if err != nil {
return err
@@ -587,7 +586,7 @@ func (module *setter) UpdatePasswordByResetPasswordToken(ctx context.Context, to
ctx,
user.OrgID,
roleNames,
authtypes.MustNewSubject(coretypes.NewResourceUser(), user.ID.StringValue(), user.OrgID, nil),
authtypes.MustNewSubject(authtypes.TypeableUser, user.ID.StringValue(), user.OrgID, nil),
); err != nil {
return err
}
@@ -720,7 +719,7 @@ func (module *setter) CreateFirstUser(ctx context.Context, organization *types.O
return err
}
err = module.createUserWithoutGrant(ctx, user, root.WithFactorPassword(password), root.WithRoleNames(roleNames))
err = module.CreateUser(ctx, user, root.WithFactorPassword(password), root.WithRoleNames(roleNames))
if err != nil {
return err
}
@@ -797,7 +796,7 @@ func (module *setter) activatePendingUser(ctx context.Context, user *types.User,
ctx,
user.OrgID,
createUserOpts.RoleNames,
authtypes.MustNewSubject(coretypes.NewResourceUser(), user.ID.StringValue(), user.OrgID, nil),
authtypes.MustNewSubject(authtypes.TypeableUser, user.ID.StringValue(), user.OrgID, nil),
)
if err != nil {
return err
@@ -891,7 +890,7 @@ func (module *setter) AddUserRole(ctx context.Context, orgID, userID valuer.UUID
orgID,
existingRoles,
[]string{roleName},
authtypes.MustNewSubject(coretypes.NewResourceUser(), existingUser.ID.StringValue(), existingUser.OrgID, nil),
authtypes.MustNewSubject(authtypes.TypeableUser, existingUser.ID.StringValue(), existingUser.OrgID, nil),
); err != nil {
return err
}
@@ -954,7 +953,7 @@ func (module *setter) RemoveUserRole(ctx context.Context, orgID, userID valuer.U
ctx,
orgID,
[]string{roleName},
authtypes.MustNewSubject(coretypes.NewResourceUser(), existingUser.ID.StringValue(), existingUser.OrgID, nil),
authtypes.MustNewSubject(authtypes.TypeableUser, existingUser.ID.StringValue(), existingUser.OrgID, nil),
); err != nil {
return err
}

View File

@@ -4,10 +4,9 @@ package parser
import (
"fmt"
"github.com/antlr4-go/antlr/v4"
"sync"
"unicode"
"github.com/antlr4-go/antlr/v4"
)
// Suppress unused import error

View File

@@ -4,10 +4,9 @@ package parser
import (
"fmt"
"github.com/antlr4-go/antlr/v4"
"sync"
"unicode"
"github.com/antlr4-go/antlr/v4"
)
// Suppress unused import error

View File

@@ -2,12 +2,11 @@ package clickhouseprometheus
import (
"context"
"sort"
"testing"
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
cmock "github.com/srikanthccv/ClickHouse-go-mock"
"github.com/stretchr/testify/require"
"sort"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/SigNoz/signoz/pkg/telemetrystore"

View File

@@ -1,11 +1,10 @@
package prometheustest
import (
"testing"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/promql"
"testing"
)
func TestRemoveExtraLabels(t *testing.T) {

View File

@@ -12,7 +12,6 @@ import (
"github.com/SigNoz/govaluate"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/querybuilder"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
@@ -970,19 +969,11 @@ func (q *querier) prepareFillZeroArgsWithStep(functions []qbtypes.Function, req
updatedFunctions := make([]qbtypes.Function, len(functions))
copy(updatedFunctions, functions)
// funcFillZero expects start/end in milliseconds. req.Start/req.End may
// arrive in s/ms/μs/ns depending on the caller; normalize via ToNanoSecs
// (same pattern used elsewhere in the codebase, e.g. RecommendedStepInterval)
// then convert to ms. Without this, an ns payload makes (end-start)/step
// 10^6× too large and OOMs the process.
startMs := querybuilder.ToNanoSecs(req.Start) / 1_000_000
endMs := querybuilder.ToNanoSecs(req.End) / 1_000_000
for i, fn := range updatedFunctions {
if fn.Name == qbtypes.FunctionNameFillZero && len(fn.Args) == 0 {
fn.Args = []qbtypes.FunctionArg{
{Value: float64(startMs)},
{Value: float64(endMs)},
{Value: float64(req.Start)},
{Value: float64(req.End)},
{Value: float64(step)},
}
updatedFunctions[i] = fn

View File

@@ -251,10 +251,10 @@ func TestEnhancePromQLError(t *testing.T) {
t.Run("quoted metric outside braces patterns", func(t *testing.T) {
tests := []struct {
name string
query string
wantHint bool
wantMetricInHint string
name string
query string
wantHint bool
wantMetricInHint string
}{
{
name: "quoted metric name followed by selector",

View File

@@ -24,7 +24,6 @@ import (
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
"github.com/SigNoz/signoz/pkg/types/retentiontypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -1426,7 +1425,7 @@ func (r *ClickHouseReader) setTTLLogs(ctx context.Context, orgID string, params
// we will change ttl for only the new parts and not the old ones
query += " SETTINGS materialize_ttl_after_modify=0"
ttl := retentiontypes.TTLSetting{
ttl := types.TTLSetting{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
@@ -1461,7 +1460,7 @@ func (r *ClickHouseReader) setTTLLogs(ctx context.Context, orgID string, params
sqlDB.
BunDB().
NewUpdate().
Model(new(retentiontypes.TTLSetting)).
Model(new(types.TTLSetting)).
Set("updated_at = ?", time.Now()).
Set("status = ?", constants.StatusFailed).
Where("id = ?", statusItem.ID.StringValue()).
@@ -1481,7 +1480,7 @@ func (r *ClickHouseReader) setTTLLogs(ctx context.Context, orgID string, params
sqlDB.
BunDB().
NewUpdate().
Model(new(retentiontypes.TTLSetting)).
Model(new(types.TTLSetting)).
Set("updated_at = ?", time.Now()).
Set("status = ?", constants.StatusFailed).
Where("id = ?", statusItem.ID.StringValue()).
@@ -1496,7 +1495,7 @@ func (r *ClickHouseReader) setTTLLogs(ctx context.Context, orgID string, params
sqlDB.
BunDB().
NewUpdate().
Model(new(retentiontypes.TTLSetting)).
Model(new(types.TTLSetting)).
Set("updated_at = ?", time.Now()).
Set("status = ?", constants.StatusSuccess).
Where("id = ?", statusItem.ID.StringValue()).
@@ -1564,7 +1563,7 @@ func (r *ClickHouseReader) setTTLTraces(ctx context.Context, orgID string, param
timestamp = "end"
}
ttl := retentiontypes.TTLSetting{
ttl := types.TTLSetting{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
@@ -1611,7 +1610,7 @@ func (r *ClickHouseReader) setTTLTraces(ctx context.Context, orgID string, param
sqlDB.
BunDB().
NewUpdate().
Model(new(retentiontypes.TTLSetting)).
Model(new(types.TTLSetting)).
Set("updated_at = ?", time.Now()).
Set("status = ?", constants.StatusFailed).
Where("id = ?", statusItem.ID.StringValue()).
@@ -1632,7 +1631,7 @@ func (r *ClickHouseReader) setTTLTraces(ctx context.Context, orgID string, param
sqlDB.
BunDB().
NewUpdate().
Model(new(retentiontypes.TTLSetting)).
Model(new(types.TTLSetting)).
Set("updated_at = ?", time.Now()).
Set("status = ?", constants.StatusFailed).
Where("id = ?", statusItem.ID.StringValue()).
@@ -1647,7 +1646,7 @@ func (r *ClickHouseReader) setTTLTraces(ctx context.Context, orgID string, param
sqlDB.
BunDB().
NewUpdate().
Model(new(retentiontypes.TTLSetting)).
Model(new(types.TTLSetting)).
Set("updated_at = ?", time.Now()).
Set("status = ?", constants.StatusSuccess).
Where("id = ?", statusItem.ID.StringValue()).
@@ -1839,7 +1838,7 @@ func (r *ClickHouseReader) SetTTLV2(ctx context.Context, orgID string, params *m
}
for tableName, queries := range ttlPayload {
customTTL := retentiontypes.TTLSetting{
customTTL := types.TTLSetting{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
@@ -1978,7 +1977,7 @@ func (r *ClickHouseReader) GetCustomRetentionTTL(ctx context.Context, orgID stri
response.Version = "v2"
// Get the latest custom retention TTL setting
customTTL := new(retentiontypes.TTLSetting)
customTTL := new(types.TTLSetting)
err := r.sqlDB.BunDB().NewSelect().
Model(customTTL).
Where("org_id = ?", orgID).
@@ -2047,8 +2046,8 @@ func (r *ClickHouseReader) GetCustomRetentionTTL(ctx context.Context, orgID stri
return response, nil
}
func (r *ClickHouseReader) checkCustomRetentionTTLStatusItem(ctx context.Context, orgID string, tableName string) (*retentiontypes.TTLSetting, error) {
ttl := new(retentiontypes.TTLSetting)
func (r *ClickHouseReader) checkCustomRetentionTTLStatusItem(ctx context.Context, orgID string, tableName string) (*types.TTLSetting, error) {
ttl := new(types.TTLSetting)
err := r.sqlDB.BunDB().NewSelect().
Model(ttl).
Where("table_name = ?", tableName).
@@ -2069,7 +2068,7 @@ func (r *ClickHouseReader) updateCustomRetentionTTLStatus(ctx context.Context, o
statusItem, apiErr := r.checkCustomRetentionTTLStatusItem(ctx, orgID, tableName)
if apiErr == nil && statusItem != nil {
_, dbErr := r.sqlDB.BunDB().NewUpdate().
Model(new(retentiontypes.TTLSetting)).
Model(new(types.TTLSetting)).
Set("updated_at = ?", time.Now()).
Set("status = ?", status).
Where("id = ?", statusItem.ID.StringValue()).
@@ -2236,7 +2235,7 @@ func (r *ClickHouseReader) setTTLMetrics(ctx context.Context, orgID string, para
}
}
metricTTL := func(tableName string) {
ttl := retentiontypes.TTLSetting{
ttl := types.TTLSetting{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
@@ -2283,7 +2282,7 @@ func (r *ClickHouseReader) setTTLMetrics(ctx context.Context, orgID string, para
sqlDB.
BunDB().
NewUpdate().
Model(new(retentiontypes.TTLSetting)).
Model(new(types.TTLSetting)).
Set("updated_at = ?", time.Now()).
Set("status = ?", constants.StatusFailed).
Where("id = ?", statusItem.ID.StringValue()).
@@ -2304,7 +2303,7 @@ func (r *ClickHouseReader) setTTLMetrics(ctx context.Context, orgID string, para
sqlDB.
BunDB().
NewUpdate().
Model(new(retentiontypes.TTLSetting)).
Model(new(types.TTLSetting)).
Set("updated_at = ?", time.Now()).
Set("status = ?", constants.StatusFailed).
Where("id = ?", statusItem.ID.StringValue()).
@@ -2319,7 +2318,7 @@ func (r *ClickHouseReader) setTTLMetrics(ctx context.Context, orgID string, para
sqlDB.
BunDB().
NewUpdate().
Model(new(retentiontypes.TTLSetting)).
Model(new(types.TTLSetting)).
Set("updated_at = ?", time.Now()).
Set("status = ?", constants.StatusSuccess).
Where("id = ?", statusItem.ID.StringValue()).
@@ -2342,7 +2341,7 @@ func (r *ClickHouseReader) deleteTtlTransactions(ctx context.Context, orgID stri
BunDB().
NewSelect().
Column("transaction_id").
Model(new(retentiontypes.TTLSetting)).
Model(new(types.TTLSetting)).
Where("org_id = ?", orgID).
Group("transaction_id").
OrderExpr("MAX(created_at) DESC").
@@ -2357,7 +2356,7 @@ func (r *ClickHouseReader) deleteTtlTransactions(ctx context.Context, orgID stri
sqlDB.
BunDB().
NewDelete().
Model(new(retentiontypes.TTLSetting)).
Model(new(types.TTLSetting)).
Where("transaction_id NOT IN (?)", bun.In(limitTransactions)).
Exec(ctx)
if err != nil {
@@ -2366,9 +2365,9 @@ func (r *ClickHouseReader) deleteTtlTransactions(ctx context.Context, orgID stri
}
// checkTTLStatusItem checks if ttl_status table has an entry for the given table name
func (r *ClickHouseReader) checkTTLStatusItem(ctx context.Context, orgID string, tableName string) (*retentiontypes.TTLSetting, *model.ApiError) {
func (r *ClickHouseReader) checkTTLStatusItem(ctx context.Context, orgID string, tableName string) (*types.TTLSetting, *model.ApiError) {
r.logger.Info("checkTTLStatusItem query", "tableName", tableName)
ttl := new(retentiontypes.TTLSetting)
ttl := new(types.TTLSetting)
err := r.
sqlDB.
BunDB().
@@ -2392,7 +2391,7 @@ func (r *ClickHouseReader) getTTLQueryStatus(ctx context.Context, orgID string,
status := constants.StatusSuccess
for _, tableName := range tableNameArray {
statusItem, apiErr := r.checkTTLStatusItem(ctx, orgID, tableName)
emptyStatusStruct := new(retentiontypes.TTLSetting)
emptyStatusStruct := new(types.TTLSetting)
if statusItem == emptyStatusStruct {
return "", nil
}

View File

@@ -9,8 +9,6 @@ import (
"strings"
"time"
"log/slog"
"github.com/SigNoz/signoz/pkg/query-service/app/metrics/v4/helpers"
"github.com/SigNoz/signoz/pkg/query-service/common"
"github.com/SigNoz/signoz/pkg/query-service/constants"
@@ -19,6 +17,7 @@ import (
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/query-service/postprocess"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"log/slog"
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
"github.com/SigNoz/signoz/pkg/valuer"

View File

@@ -2,7 +2,6 @@ package helpers
import (
"fmt"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
)

View File

@@ -3,9 +3,9 @@ package querier
import (
"context"
"fmt"
"log/slog"
"math"
"strings"
"log/slog"
"testing"
"time"

View File

@@ -3,9 +3,9 @@ package v2
import (
"context"
"fmt"
"log/slog"
"math"
"strings"
"log/slog"
"testing"
"time"

View File

@@ -115,7 +115,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
s := &Server{
config: config,
signoz: signoz,
signoz: signoz,
httpHostPort: constants.HTTPHostPort,
unavailableChannel: make(chan healthcheck.Status),
}
@@ -297,3 +297,4 @@ func (s *Server) Stop(ctx context.Context) error {
return nil
}

View File

@@ -4,7 +4,6 @@ package model
import (
json "encoding/json"
easyjson "github.com/mailru/easyjson"
jlexer "github.com/mailru/easyjson/jlexer"
jwriter "github.com/mailru/easyjson/jwriter"

View File

@@ -247,11 +247,7 @@ func ClickHouseFormattedMetricNames(v interface{}) string {
func AddBackTickToFormatTag(str string) string {
if strings.Contains(str, ".") || strings.Contains(str, "-") {
if strings.HasPrefix(str, "`") && strings.HasSuffix(str, "`") {
return str
} else {
return "`" + str + "`"
}
if strings.HasPrefix(str, "`") && strings.HasSuffix(str, "`") { return str } else { return "`" + str + "`" }
} else {
return str
}

View File

@@ -1,40 +1,15 @@
package querybuilder
import (
"fmt"
"sort"
"strings"
errors "github.com/SigNoz/signoz/pkg/errors/v2"
"github.com/SigNoz/signoz/pkg/errors"
grammar "github.com/SigNoz/signoz/pkg/parser/havingexpression/grammar"
"github.com/antlr4-go/antlr/v4"
"github.com/huandu/go-sqlbuilder"
)
// HAVING-expression validator codes. All three are caller-fault, fix-the-input
// errors — the user wrote an expression we cannot turn into SQL — so retry
// is pointless until the expression itself changes.
var (
codeHavingStringLiteral = errors.Register("querybuilder.having.string_literal", errors.Meta{
Category: errors.CategoryInvalidInput,
Fault: errors.FaultCaller,
Retry: errors.Retry{Policy: errors.RetryAfterFix},
Remediation: errors.RemediationFixInput,
})
codeHavingInvalidReference = errors.Register("querybuilder.having.invalid_reference", errors.Meta{
Category: errors.CategoryInvalidInput,
Fault: errors.FaultCaller,
Retry: errors.Retry{Policy: errors.RetryAfterFix},
Remediation: errors.RemediationFixInput,
})
codeHavingSyntaxError = errors.Register("querybuilder.having.syntax_error", errors.Meta{
Category: errors.CategoryInvalidInput,
Fault: errors.FaultCaller,
Retry: errors.Retry{Policy: errors.RetryAfterFix},
Remediation: errors.RemediationFixInput,
})
)
// havingExpressionRewriteVisitor walks the parse tree of a HavingExpression in a single
// pass, simultaneously rewriting user-facing references to their SQL column names and
// collecting any references that could not be resolved.
@@ -306,10 +281,10 @@ func (r *HavingExpressionRewriter) rewriteAndValidate(expression string) (string
// This is checked before invalid references so that "contains string literals" takes
// priority when a bare string literal is also an unresolvable operand.
if v.hasStringLiteral {
return "", errors.New(codeHavingStringLiteral,
return "", errors.NewInvalidInputf(
errors.CodeInvalidInput,
"`Having` expression contains string literals",
errors.WithDetail("Aggregator results are numeric"),
)
).WithAdditional("Aggregator results are numeric")
}
if len(v.invalid) > 0 {
@@ -319,10 +294,7 @@ func (r *HavingExpressionRewriter) rewriteAndValidate(expression string) (string
validKeys = append(validKeys, k)
}
sort.Strings(validKeys)
opts := []errors.Option{
errors.WithAttr("invalid_refs", v.invalid),
errors.WithAttr("valid_refs", validKeys),
}
additional := []string{"Valid references are: [" + strings.Join(validKeys, ", ") + "]"}
if len(v.invalid) == 1 {
inv := v.invalid[0]
// Only suggest for plain identifier typos, not for unresolved function
@@ -331,13 +303,15 @@ func (r *HavingExpressionRewriter) rewriteAndValidate(expression string) (string
// a simple string substitution produce a corrupt expression.
isFuncCall := strings.Contains(original, inv+"(")
if match, dist := closestMatch(inv, validKeys); !isFuncCall && !strings.Contains(match, "(") && dist <= 3 {
opts = append(opts, errors.WithAttr("suggestions", []string{strings.ReplaceAll(original, inv, match)}))
corrected := strings.ReplaceAll(original, inv, match)
additional = append(additional, "Suggestion: `"+corrected+"`")
}
}
return "", errors.New(codeHavingInvalidReference,
fmt.Sprintf("Invalid references in `Having` expression: [%s]", strings.Join(v.invalid, ", ")),
opts...,
)
return "", errors.NewInvalidInputf(
errors.CodeInvalidInput,
"Invalid references in `Having` expression: [%s]",
strings.Join(v.invalid, ", "),
).WithAdditional(additional...)
}
// Layer 3 ANTLR syntax errors. We parse the original expression, so error messages
@@ -354,20 +328,17 @@ func (r *HavingExpressionRewriter) rewriteAndValidate(expression string) (string
if detail == "" {
detail = "check the expression syntax"
}
opts := []errors.Option{
errors.WithDetail(detail),
errors.WithAttr("syntax_errors", msgs),
}
additional := []string{detail}
// For single-error expressions, try to produce an actionable suggestion.
if len(allSyntaxErrors) == 1 {
if s := havingSuggestion(allSyntaxErrors[0], original); s != "" {
opts = append(opts, errors.WithAttr("suggestions", []string{s}))
additional = append(additional, "Suggestion: `"+s+"`")
}
}
return "", errors.New(codeHavingSyntaxError,
return "", errors.NewInvalidInputf(
errors.CodeInvalidInput,
"Syntax error in `Having` expression",
opts...,
)
).WithAdditional(additional...)
}
return result, nil

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, authz.Config, licensing.Licensing, []authz.OnBeforeRoleDelete) (factory.ProviderFactory[authz.AuthZ, authz.Config], error),
authzCallback func(context.Context, sqlstore.SQLStore, licensing.Licensing, []authz.OnBeforeRoleDelete, dashboard.Module) (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]],
@@ -325,7 +325,7 @@ func New(
// Initialize query parser (needed for dashboard module)
queryParser := queryparser.New(providerSettings)
// Initialize dashboard module
// Initialize dashboard module (needed for authz registry)
dashboard := dashboardModuleCallback(sqlstore, providerSettings, analytics, orgGetter, queryParser, querier, licensing)
// Initialize user getter
@@ -341,7 +341,7 @@ func New(
}
// Initialize authz
authzProviderFactory, err := authzCallback(ctx, sqlstore, config.Authz, licensing, onBeforeRoleDelete)
authzProviderFactory, err := authzCallback(ctx, sqlstore, licensing, onBeforeRoleDelete, dashboard)
if err != nil {
return nil, err
}

View File

@@ -2,7 +2,6 @@ package sqlmigration
import (
"context"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"

View File

@@ -5,9 +5,6 @@ import (
"database/sql"
"encoding/json"
"fmt"
"log/slog"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
@@ -17,6 +14,8 @@ import (
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
"log/slog"
"time"
)
// Shared types for migration

View File

@@ -8,7 +8,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/oklog/ulid/v2"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
@@ -125,7 +125,7 @@ func (migration *migrateRbacToAuthz) Up(ctx context.Context, db *bun.DB) error {
tuples = append(tuples, tuple{
OrgID: orgID,
ID: coretypes.AnonymousUser.StringValue(),
ID: authtypes.AnonymousUser.StringValue(),
Type: "anonymous",
RoleName: "signoz-anonymous",
})

Some files were not shown because too many files have changed in this diff Show More