Compare commits

..

11 Commits

Author SHA1 Message Date
Nikhil Soni
dd788b94dc fix: guide user on empty external api monitoring page
Fixes #3703
2026-01-28 15:56:23 +05:30
Vikrant Gupta
1c815b130c feat(authz): scafolding for rbac migration (#10121)
* feat(authz): initial commit for migrating rbac to openfga

* feat(authz): make the role updates idempotant

* feat(authz): split role module into role and grant

* feat(authz): some naming changes

* feat(authz): integrate the grant module

* feat(authz): add support for migrating existing user role

* feat(authz): add support for migrating existing user role

* feat(authz): figure out the * selector

* feat(authz): merge main

* feat(authz): merge main

* feat(authz): address couple of todos

* feat(authz): address couple of todos

* feat(authz): fix tests and revert public dashboard change

* feat(authz): fix tests and revert public dashboard change

* feat(authz): add open api spec

* feat(authz): add open api spec

* feat(authz): add api key changes and missing migration

* feat(authz): split role into getter and setter

* feat(authz): add integration tests for authz register

* feat(authz): add more tests for user invite and delete

* feat(authz): update user tests

* feat(authz): rename grant to granter

* feat(authz): address review comments

* feat(authz): address review comments

* feat(authz): address review comments

* feat(authz): add the migration for existing roles

* feat(authz): go mod tidy

* feat(authz): fix integration tests

* feat(authz): handle community changes

* feat(authz): handle community changes

* feat(authz): role selectors for open claims

* feat(authz): role selectors for open claims

* feat(authz): prevent duplicate entries for changelog

* feat(authz): scafolding for rbac migration

* feat(authz): scafolding for rbac migration

* feat(authz): scafolding for rbac migration

* feat(authz): scafolding for rbac migration

* feat(authz): scafolding for rbac migration
2026-01-27 21:24:36 +05:30
Vikrant Gupta
9e9879fc8d chore(env): add docs base url (#10112)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* chore(env): add docs base url for staging and production

* chore(env): add docs base url for staging and production

* chore(env): add docs base url for staging and production
2026-01-27 18:14:05 +05:30
Abhishek Kumar Singh
af87c2e80b chore: added Validate function for QueryBuilderFormula struct (#10041) 2026-01-27 15:48:14 +05:30
Karan Balani
e1ac992e5a feat: forgot password api and token expiry (#10073) 2026-01-27 15:31:15 +05:30
Aditya Singh
15161c09e8 Feat: show (cmd + return) as helper text in Run Query button (#10082)
* feat: create common run query btn

* feat: update run query in explorer

* feat: comment

* feat: fix styles

* feat: fix styles

* feat: update style

* feat: update btn in alerts

* feat: added test cases

* feat: replace run query btn

* feat: bg change run query
2026-01-27 14:52:03 +05:30
Ashwin Bhatkal
ee5fbe41eb chore: add eslint rules for no-unused-vars (#10072)
* chore: updated eslint base config with comments

* chore: add eslint rules for no-else-return and curly

* chore: add eslint rules for no-console

* chore: add eslint rules for no-unused-vars

* chore: fix more cases
2026-01-27 14:14:26 +05:30
Vikrant Gupta
f2f3a7b24a chore(lint): enable wastedassign (#10103)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-01-26 20:40:49 +05:30
Ashwin Bhatkal
dd0738ac70 chore: add eslint rules for no-console (#10071)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-01-23 19:06:05 +00:00
Pandey
1c4dfc931f chore: move to clone instead of json marshal (#10076) 2026-01-23 16:34:30 +00:00
Tushar Vats
605d6ba17d feat: extract context and data type from telemetry field name (#9986)
This pull request introduces significant improvements to the handling and normalization of telemetry field keys, adds comprehensive tests for these changes, and refactors JSON unmarshaling for query builder types to ensure consistent normalization and error handling
2026-01-23 13:19:03 +00:00
116 changed files with 3830 additions and 641 deletions

2
.github/CODEOWNERS vendored
View File

@@ -101,7 +101,7 @@
# Integration tests
/tests/integration/ @vikrantgupta25, @srikanthccv
/tests/integration/ @vikrantgupta25
# OpenAPI types generator

View File

@@ -70,6 +70,7 @@ jobs:
echo 'PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> frontend/.env
echo 'APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> frontend/.env
echo 'PYLON_IDENTITY_SECRET="${{ secrets.PYLON_IDENTITY_SECRET }}"' >> frontend/.env
echo 'DOCS_BASE_URL="https://signoz.io"' >> frontend/.env
- name: cache-dotenv
uses: actions/cache@v4
with:

View File

@@ -69,6 +69,7 @@ jobs:
echo 'PYLON_APP_ID="${{ secrets.NP_PYLON_APP_ID }}"' >> frontend/.env
echo 'APPCUES_APP_ID="${{ secrets.NP_APPCUES_APP_ID }}"' >> frontend/.env
echo 'PYLON_IDENTITY_SECRET="${{ secrets.NP_PYLON_IDENTITY_SECRET }}"' >> frontend/.env
echo 'DOCS_BASE_URL="https://staging.signoz.io"' >> frontend/.env
- name: cache-dotenv
uses: actions/cache@v4
with:

View File

@@ -3,8 +3,8 @@ name: gor-signoz
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+'
- "v[0-9]+.[0-9]+.[0-9]+"
- "v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+"
permissions:
contents: write
@@ -36,8 +36,9 @@ jobs:
echo 'PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> .env
echo 'APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> .env
echo 'PYLON_IDENTITY_SECRET="${{ secrets.PYLON_IDENTITY_SECRET }}"' >> .env
echo 'DOCS_BASE_URL="https://signoz.io"' >> .env
- name: build-frontend
run: make js-build
run: make js-build
- name: upload-frontend-artifact
uses: actions/upload-artifact@v4
with:
@@ -104,7 +105,7 @@ jobs:
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser-pro
version: '~> v2'
version: "~> v2"
args: release --config ${{ env.CONFIG_PATH }} --clean --split
workdir: .
env:
@@ -161,7 +162,7 @@ jobs:
if: steps.cache-linux.outputs.cache-hit == 'true' && steps.cache-darwin.outputs.cache-hit == 'true' # only run if caches hit
with:
distribution: goreleaser-pro
version: '~> v2'
version: "~> v2"
args: continue --merge
workdir: .
env:

View File

@@ -12,6 +12,7 @@ linters:
- misspell
- nilnil
- sloglint
- wastedassign
- unparam
- unused
settings:

View File

@@ -1,5 +1,7 @@
{
"eslint.workingDirectories": ["./frontend"],
"eslint.workingDirectories": [
"./frontend"
],
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {

View File

@@ -19,6 +19,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/role"
"github.com/SigNoz/signoz/pkg/modules/role/implrole"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/query-service/app"
"github.com/SigNoz/signoz/pkg/queryparser"
@@ -80,12 +81,15 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
func(ctx context.Context, sqlstore sqlstore.SQLStore) factory.ProviderFactory[authz.AuthZ, authz.Config] {
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx))
},
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, _ role.Module, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing) dashboard.Module {
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, _ role.Setter, _ role.Granter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing) dashboard.Module {
return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser)
},
func(_ licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
return noopgateway.NewProviderFactory()
},
func(store sqlstore.SQLStore, authz authz.AuthZ, licensing licensing.Licensing, _ []role.RegisterTypeable) role.Setter {
return implrole.NewSetter(implrole.NewStore(store), authz)
},
)
if err != nil {
logger.ErrorContext(ctx, "failed to create signoz", "error", err)

View File

@@ -14,6 +14,7 @@ import (
enterpriselicensing "github.com/SigNoz/signoz/ee/licensing"
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
"github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/ee/modules/role/implrole"
enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app"
"github.com/SigNoz/signoz/ee/sqlschema/postgressqlschema"
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
@@ -29,6 +30,7 @@ import (
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/role"
pkgimplrole "github.com/SigNoz/signoz/pkg/modules/role/implrole"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/signoz"
@@ -119,13 +121,17 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
func(ctx context.Context, sqlstore sqlstore.SQLStore) factory.ProviderFactory[authz.AuthZ, authz.Config] {
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx))
},
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, role role.Module, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, role, queryParser, querier, licensing)
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, roleSetter role.Setter, granter role.Granter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, roleSetter, granter, queryParser, querier, licensing)
},
func(licensing licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
return httpgateway.NewProviderFactory(licensing)
},
func(store sqlstore.SQLStore, authz authz.AuthZ, licensing licensing.Licensing, registry []role.RegisterTypeable) role.Setter {
return implrole.NewSetter(pkgimplrole.NewStore(store), authz, licensing, registry)
},
)
if err != nil {
logger.ErrorContext(ctx, "failed to create signoz", "error", err)
return err

View File

@@ -291,3 +291,12 @@ flagger:
float:
integer:
object:
##################### User #####################
user:
password:
reset:
# Whether to allow users to reset their password themselves.
allow_self: true
# The duration within which a user can reset their password.
max_token_lifetime: 6h

View File

@@ -209,7 +209,7 @@ paths:
/api/v1/dashboards/{id}/public:
delete:
deprecated: false
description: This endpoints deletes the public sharing config and disables the
description: This endpoint deletes the public sharing config and disables the
public sharing of a dashboard
operationId: DeletePublicDashboard
parameters:
@@ -253,7 +253,7 @@ paths:
- dashboard
get:
deprecated: false
description: This endpoints returns public sharing config for a dashboard
description: This endpoint returns public sharing config for a dashboard
operationId: GetPublicDashboard
parameters:
- in: path
@@ -301,7 +301,7 @@ paths:
- dashboard
post:
deprecated: false
description: This endpoints creates public sharing config and enables public
description: This endpoint creates public sharing config and enables public
sharing of the dashboard
operationId: CreatePublicDashboard
parameters:
@@ -355,7 +355,7 @@ paths:
- dashboard
put:
deprecated: false
description: This endpoints updates the public sharing config for a dashboard
description: This endpoint updates the public sharing config for a dashboard
operationId: UpdatePublicDashboard
parameters:
- in: path
@@ -671,7 +671,7 @@ paths:
/api/v1/global/config:
get:
deprecated: false
description: This endpoints returns global config
description: This endpoint returns global config
operationId: GetGlobalConfig
responses:
"200":
@@ -1447,8 +1447,7 @@ paths:
/api/v1/public/dashboards/{id}:
get:
deprecated: false
description: This endpoints returns the sanitized dashboard data for public
access
description: This endpoint returns the sanitized dashboard data for public access
operationId: GetPublicDashboardData
parameters:
- in: path
@@ -1579,6 +1578,228 @@ paths:
summary: Reset password
tags:
- users
/api/v1/roles:
get:
deprecated: false
description: This endpoint lists all roles
operationId: ListRoles
responses:
"200":
content:
application/json:
schema:
properties:
data:
items:
$ref: '#/components/schemas/RoletypesRole'
type: array
status:
type: string
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: List roles
tags:
- role
post:
deprecated: false
description: This endpoint creates a role
operationId: CreateRole
responses:
"201":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/TypesIdentifiable'
status:
type: string
type: object
description: Created
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Create role
tags:
- role
/api/v1/roles/{id}:
delete:
deprecated: false
description: This endpoint deletes a role
operationId: DeleteRole
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Delete role
tags:
- role
get:
deprecated: false
description: This endpoint gets a role
operationId: GetRole
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/RoletypesRole'
status:
type: string
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Get role
tags:
- role
patch:
deprecated: false
description: This endpoint patches a role
operationId: PatchRole
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Patch role
tags:
- role
/api/v1/user:
get:
deprecated: false
@@ -1985,6 +2206,35 @@ paths:
summary: Update user preference
tags:
- preferences
/api/v2/factor_password/forgot:
post:
deprecated: false
description: This endpoint initiates the forgot password flow by sending a reset
password email
operationId: ForgotPassword
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TypesPostableForgotPassword'
responses:
"204":
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
summary: Forgot password
tags:
- users
/api/v2/features:
get:
deprecated: false
@@ -3859,6 +4109,25 @@ components:
status:
type: string
type: object
RoletypesRole:
properties:
createdAt:
format: date-time
type: string
description:
type: string
id:
type: string
name:
type: string
orgId:
type: string
type:
type: string
updatedAt:
format: date-time
type: string
type: object
TypesChangePasswordRequest:
properties:
newPassword:
@@ -3979,6 +4248,15 @@ components:
token:
type: string
type: object
TypesPostableForgotPassword:
properties:
email:
type: string
frontendBaseURL:
type: string
orgId:
type: string
type: object
TypesPostableInvite:
properties:
email:
@@ -3999,6 +4277,9 @@ components:
type: object
TypesResetPasswordToken:
properties:
expiresAt:
format: date-time
type: string
id:
type: string
passwordId:

View File

@@ -47,7 +47,7 @@ func (provider *provider) Check(ctx context.Context, tuple *openfgav1.TupleKey)
return provider.pkgAuthzService.Check(ctx, tuple)
}
func (provider *provider) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, relation authtypes.Relation, _ authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector) error {
func (provider *provider) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, relation authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector, _ []authtypes.Selector) error {
subject, err := authtypes.NewSubject(authtypes.TypeableUser, claims.UserID, orgID, nil)
if err != nil {
return err
@@ -66,7 +66,7 @@ func (provider *provider) CheckWithTupleCreation(ctx context.Context, claims aut
return nil
}
func (provider *provider) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, relation authtypes.Relation, _ authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector) error {
func (provider *provider) 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

View File

@@ -26,12 +26,13 @@ type module struct {
pkgDashboardModule dashboard.Module
store dashboardtypes.Store
settings factory.ScopedProviderSettings
role role.Module
roleSetter role.Setter
granter role.Granter
querier querier.Querier
licensing licensing.Licensing
}
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, role role.Module, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, roleSetter role.Setter, granter role.Granter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
scopedProviderSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard")
pkgDashboardModule := pkgimpldashboard.NewModule(store, settings, analytics, orgGetter, queryParser)
@@ -39,7 +40,8 @@ func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, an
pkgDashboardModule: pkgDashboardModule,
store: store,
settings: scopedProviderSettings,
role: role,
roleSetter: roleSetter,
granter: granter,
querier: querier,
licensing: licensing,
}
@@ -59,12 +61,12 @@ func (module *module) CreatePublic(ctx context.Context, orgID valuer.UUID, publi
return errors.Newf(errors.TypeAlreadyExists, dashboardtypes.ErrCodePublicDashboardAlreadyExists, "dashboard with id %s is already public", storablePublicDashboard.DashboardID)
}
role, err := module.role.GetOrCreate(ctx, roletypes.NewRole(roletypes.AnonymousUserRoleName, roletypes.AnonymousUserRoleDescription, roletypes.RoleTypeManaged.StringValue(), orgID))
role, err := module.roleSetter.GetOrCreate(ctx, orgID, roletypes.NewRole(roletypes.SigNozAnonymousRoleName, roletypes.SigNozAnonymousRoleDescription, roletypes.RoleTypeManaged, orgID))
if err != nil {
return err
}
err = module.role.Assign(ctx, role.ID, orgID, authtypes.MustNewSubject(authtypes.TypeableAnonymous, authtypes.AnonymousUser.StringValue(), orgID, nil))
err = module.granter.Grant(ctx, orgID, roletypes.SigNozAnonymousRoleName, authtypes.MustNewSubject(authtypes.TypeableAnonymous, authtypes.AnonymousUser.StringValue(), orgID, nil))
if err != nil {
return err
}
@@ -77,7 +79,7 @@ func (module *module) CreatePublic(ctx context.Context, orgID valuer.UUID, publi
authtypes.MustNewSelector(authtypes.TypeMetaResource, publicDashboard.ID.String()),
)
err = module.role.PatchObjects(ctx, orgID, role.ID, authtypes.RelationRead, []*authtypes.Object{additionObject}, nil)
err = module.roleSetter.PatchObjects(ctx, orgID, role.ID, authtypes.RelationRead, []*authtypes.Object{additionObject}, nil)
if err != nil {
return err
}
@@ -193,7 +195,7 @@ func (module *module) DeletePublic(ctx context.Context, orgID valuer.UUID, dashb
return err
}
role, err := module.role.GetOrCreate(ctx, roletypes.NewRole(roletypes.AnonymousUserRoleName, roletypes.AnonymousUserRoleDescription, roletypes.RoleTypeManaged.StringValue(), orgID))
role, err := module.roleSetter.GetOrCreate(ctx, orgID, roletypes.NewRole(roletypes.SigNozAnonymousRoleName, roletypes.SigNozAnonymousRoleDescription, roletypes.RoleTypeManaged, orgID))
if err != nil {
return err
}
@@ -206,7 +208,7 @@ func (module *module) DeletePublic(ctx context.Context, orgID valuer.UUID, dashb
authtypes.MustNewSelector(authtypes.TypeMetaResource, publicDashboard.ID.String()),
)
err = module.role.PatchObjects(ctx, orgID, role.ID, authtypes.RelationRead, nil, []*authtypes.Object{deletionObject})
err = module.roleSetter.PatchObjects(ctx, orgID, role.ID, authtypes.RelationRead, nil, []*authtypes.Object{deletionObject})
if err != nil {
return err
}
@@ -270,7 +272,7 @@ func (module *module) deletePublic(ctx context.Context, orgID valuer.UUID, dashb
return err
}
role, err := module.role.GetOrCreate(ctx, roletypes.NewRole(roletypes.AnonymousUserRoleName, roletypes.AnonymousUserRoleDescription, roletypes.RoleTypeManaged.StringValue(), orgID))
role, err := module.roleSetter.GetOrCreate(ctx, orgID, roletypes.NewRole(roletypes.SigNozAnonymousRoleName, roletypes.SigNozAnonymousRoleDescription, roletypes.RoleTypeManaged, orgID))
if err != nil {
return err
}
@@ -283,7 +285,7 @@ func (module *module) deletePublic(ctx context.Context, orgID valuer.UUID, dashb
authtypes.MustNewSelector(authtypes.TypeMetaResource, publicDashboard.ID.String()),
)
err = module.role.PatchObjects(ctx, orgID, role.ID, authtypes.RelationRead, nil, []*authtypes.Object{deletionObject})
err = module.roleSetter.PatchObjects(ctx, orgID, role.ID, authtypes.RelationRead, nil, []*authtypes.Object{deletionObject})
if err != nil {
return err
}

View File

@@ -0,0 +1,165 @@
package implrole
import (
"context"
"slices"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/role"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type setter struct {
store roletypes.Store
authz authz.AuthZ
licensing licensing.Licensing
registry []role.RegisterTypeable
}
func NewSetter(store roletypes.Store, authz authz.AuthZ, licensing licensing.Licensing, registry []role.RegisterTypeable) role.Setter {
return &setter{
store: store,
authz: authz,
licensing: licensing,
registry: registry,
}
}
func (setter *setter) Create(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) error {
_, err := setter.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())
}
return setter.store.Create(ctx, roletypes.NewStorableRoleFromRole(role))
}
func (setter *setter) GetOrCreate(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) (*roletypes.Role, error) {
_, err := setter.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())
}
existingRole, err := setter.store.GetByOrgIDAndName(ctx, role.OrgID, role.Name)
if err != nil {
if !errors.Ast(err, errors.TypeNotFound) {
return nil, err
}
}
if existingRole != nil {
return roletypes.NewRoleFromStorableRole(existingRole), nil
}
err = setter.store.Create(ctx, roletypes.NewStorableRoleFromRole(role))
if err != nil {
return nil, err
}
return role, nil
}
func (setter *setter) GetResources(_ context.Context) []*authtypes.Resource {
typeables := make([]authtypes.Typeable, 0)
for _, register := range setter.registry {
typeables = append(typeables, register.MustGetTypeables()...)
}
// role module cannot self register itself!
typeables = append(typeables, setter.MustGetTypeables()...)
resources := make([]*authtypes.Resource, 0)
for _, typeable := range typeables {
resources = append(resources, &authtypes.Resource{Name: typeable.Name(), Type: typeable.Type()})
}
return resources
}
func (setter *setter) GetObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation) ([]*authtypes.Object, error) {
storableRole, err := setter.store.Get(ctx, orgID, id)
if err != nil {
return nil, err
}
objects := make([]*authtypes.Object, 0)
for _, resource := range setter.GetResources(ctx) {
if slices.Contains(authtypes.TypeableRelations[resource.Type], relation) {
resourceObjects, err := setter.
authz.
ListObjects(
ctx,
authtypes.MustNewSubject(authtypes.TypeableRole, storableRole.ID.String(), orgID, &authtypes.RelationAssignee),
relation,
authtypes.MustNewTypeableFromType(resource.Type, resource.Name),
)
if err != nil {
return nil, err
}
objects = append(objects, resourceObjects...)
}
}
return objects, nil
}
func (setter *setter) Patch(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) error {
_, err := setter.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())
}
return setter.store.Update(ctx, orgID, roletypes.NewStorableRoleFromRole(role))
}
func (setter *setter) PatchObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation, additions, deletions []*authtypes.Object) error {
_, err := setter.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())
}
additionTuples, err := roletypes.GetAdditionTuples(id, orgID, relation, additions)
if err != nil {
return err
}
deletionTuples, err := roletypes.GetDeletionTuples(id, orgID, relation, deletions)
if err != nil {
return err
}
err = setter.authz.Write(ctx, additionTuples, deletionTuples)
if err != nil {
return err
}
return nil
}
func (setter *setter) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
_, err := setter.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())
}
storableRole, err := setter.store.Get(ctx, orgID, id)
if err != nil {
return err
}
role := roletypes.NewRoleFromStorableRole(storableRole)
err = role.CanEditDelete()
if err != nil {
return err
}
return setter.store.Delete(ctx, orgID, id)
}
func (setter *setter) MustGetTypeables() []authtypes.Typeable {
return []authtypes.Typeable{authtypes.TypeableRole, roletypes.TypeableResourcesRoles}
}

View File

@@ -211,7 +211,7 @@ func (s Server) HealthCheckStatus() chan healthcheck.Status {
func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*http.Server, error) {
r := baseapp.NewRouter()
am := middleware.NewAuthZ(s.signoz.Instrumentation.Logger(), s.signoz.Modules.OrgGetter, s.signoz.Authz)
am := middleware.NewAuthZ(s.signoz.Instrumentation.Logger(), s.signoz.Modules.OrgGetter, s.signoz.Authz, s.signoz.Modules.RoleGetter)
r.Use(otelmux.Middleware(
"apiserver",

View File

@@ -38,7 +38,7 @@ module.exports = {
'import', // Import/export linting
'sonarjs', // Code quality/complexity
// TODO: Uncomment after running: yarn add -D eslint-plugin-spellcheck
// 'spellcheck',
// 'spellcheck', // Correct spellings
],
settings: {
react: {
@@ -60,12 +60,18 @@ module.exports = {
'no-debugger': 'error', // Disallows debugger statements in production code
curly: 'error', // Requires curly braces for all control statements
eqeqeq: ['error', 'always', { null: 'ignore' }], // Enforces === and !== (allows == null for null/undefined check)
// TODO: Enable after fixing ~15 console.log statements
// 'no-console': ['error', { allow: ['warn', 'error'] }], // Warns on console.log, allows console.warn/error
'no-console': ['error', { allow: ['warn', 'error'] }], // Warns on console.log, allows console.warn/error
// TypeScript rules
'@typescript-eslint/explicit-function-return-type': 'error', // Requires explicit return types on functions
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
// Disallows unused variables/args
'error',
{
argsIgnorePattern: '^_', // Allows unused args prefixed with _ (e.g., _unusedParam)
varsIgnorePattern: '^_', // Allows unused vars prefixed with _ (e.g., _unusedVar)
},
],
'@typescript-eslint/no-explicit-any': 'warn', // Warns when using 'any' type (consider upgrading to error)
// TODO: Change to 'error' after fixing ~80 empty function placeholders in providers/contexts
'@typescript-eslint/no-empty-function': 'off', // Disallows empty function bodies

View File

@@ -41,7 +41,7 @@ export const getConsumerLagDetails = async (
> => {
const { detailType, ...restProps } = props;
const response = await axios.post(
`/messaging-queues/kafka/consumer-lag/${props.detailType}`,
`/messaging-queues/kafka/consumer-lag/${detailType}`,
{
...restProps,
},

View File

@@ -43,16 +43,17 @@ export const omitIdFromQuery = (query: Query | null): any => ({
builder: {
...query?.builder,
queryData: query?.builder.queryData.map((queryData) => {
const { id, ...rest } = queryData.aggregateAttribute || {};
const { id: _aggregateAttributeId, ...rest } =
queryData.aggregateAttribute || {};
const newAggregateAttribute = rest;
const newGroupByAttributes = queryData.groupBy.map((groupByAttribute) => {
const { id, ...rest } = groupByAttribute;
const { id: _groupByAttributeId, ...rest } = groupByAttribute;
return rest;
});
const newItems = queryData.filters?.items?.map((item) => {
const { id, ...newItem } = item;
const { id: _itemId, ...newItem } = item;
if (item.key) {
const { id, ...rest } = item.key;
const { id: _keyId, ...rest } = item.key;
return {
...newItem,
key: rest,

View File

@@ -45,7 +45,6 @@ function Pre({
}
function Code({
node,
inline,
className = 'blog-code',
children,

View File

@@ -29,7 +29,6 @@ import {
QUERY_BUILDER_OPERATORS_BY_KEY_TYPE,
queryOperatorSuggestions,
} from 'constants/antlrQueryConstants';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useDebounce from 'hooks/useDebounce';
import { debounce, isNull } from 'lodash-es';
@@ -208,8 +207,6 @@ function QuerySearch({
const lastValueRef = useRef<string>('');
const isMountedRef = useRef<boolean>(true);
const { handleRunQuery } = useQueryBuilder();
const { selectedDashboard } = useDashboard();
const dynamicVariables = useMemo(

View File

@@ -87,7 +87,7 @@ function TraceOperatorEditor({
// Track if the query was changed externally (from props) vs internally (user input)
const [isExternalQueryChange, setIsExternalQueryChange] = useState(false);
const [lastExternalValue, setLastExternalValue] = useState<string>('');
const { currentQuery, handleRunQuery } = useQueryBuilder();
const { currentQuery } = useQueryBuilder();
const queryOptions = useMemo(
() =>

View File

@@ -5,7 +5,6 @@ import { EditorView } from '@uiw/react-codemirror';
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
import { initialQueriesMap } from 'constants/queryBuilder';
import * as UseQBModule from 'hooks/queryBuilder/useQueryBuilder';
import { fireEvent, render, userEvent, waitFor } from 'tests/test-utils';
import type { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
import { DataSource } from 'types/common/queryBuilder';
@@ -121,13 +120,8 @@ jest.mock('api/querySuggestions/getValueSuggestion', () => ({
// Note: We're NOT mocking CodeMirror here - using the real component
// This provides integration testing with the actual CodeMirror editor
const handleRunQueryMock = ((UseQBModule as unknown) as {
handleRunQuery: jest.MockedFunction<() => void>;
}).handleRunQuery;
const SAMPLE_KEY_TYPING = 'http.';
const SAMPLE_VALUE_TYPING_INCOMPLETE = "service.name = '";
const SAMPLE_VALUE_TYPING_COMPLETE = "service.name = 'frontend'";
const SAMPLE_STATUS_QUERY = "http.status_code = '200'";
describe('QuerySearch (Integration with Real CodeMirror)', () => {

View File

@@ -796,12 +796,12 @@ export const adjustQueryForV5 = (currentQuery: Query): Query => {
});
const {
aggregateAttribute,
aggregateOperator,
timeAggregation,
spaceAggregation,
reduceTo,
filters,
aggregateAttribute: _aggregateAttribute,
aggregateOperator: _aggregateOperator,
timeAggregation: _timeAggregation,
spaceAggregation: _spaceAggregation,
reduceTo: _reduceTo,
filters: _filters,
...retainedQuery
} = query;

View File

@@ -1,14 +1,5 @@
import './Slider.styles.scss';
import { IQuickFiltersConfig } from 'components/QuickFilters/types';
interface ISliderProps {
filter: IQuickFiltersConfig;
}
// not needed for now build when required
export default function Slider(props: ISliderProps): JSX.Element {
const { filter } = props;
console.log(filter);
export default function Slider(): JSX.Element {
return <div>Slider</div>;
}

View File

@@ -303,15 +303,9 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
/>
);
case FiltersType.DURATION:
return (
<Duration
filter={filter}
onFilterChange={onFilterChange}
source={source}
/>
);
return <Duration filter={filter} onFilterChange={onFilterChange} />;
case FiltersType.SLIDER:
return <Slider filter={filter} />;
return <Slider />;
// eslint-disable-next-line sonarjs/no-duplicated-branches
default:
return (

View File

@@ -26,6 +26,7 @@ import { ApiMonitoringHardcodedAttributeKeys } from '../../constants';
import { DEFAULT_PARAMS, useApiMonitoringParams } from '../../queryParams';
import { columnsConfig, formatDataForTable } from '../../utils';
import DomainDetails from './DomainDetails/DomainDetails';
import DOCLINKS from 'utils/docLinks';
function DomainList(): JSX.Element {
const [params, setParams] = useApiMonitoringParams();
@@ -145,7 +146,17 @@ function DomainList(): JSX.Element {
/>
<Typography.Text className="no-filtered-domains-message">
This query had no results. Edit your query and try again!
No External API calls detected. To automatically detect them, ensure
Client spans are being sent with required attributes.
<br />
Read more about <span> </span>
<a
href={DOCLINKS.EXTERNAL_API_MONITORING}
target="_blank"
rel="noreferrer"
>
configuring External API monitoring.
</a>
</Typography.Text>
</div>
</div>

View File

@@ -1,7 +1,6 @@
/* eslint-disable react/jsx-props-no-spreading */
import { fireEvent, render, screen } from '@testing-library/react';
import { QueryParams } from 'constants/query';
import { initialQueriesMap } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { defaultPostableAlertRuleV2 } from 'container/CreateAlertV2/constants';
import { getCreateAlertLocalStateFromAlertDef } from 'container/CreateAlertV2/utils';

View File

@@ -73,7 +73,7 @@ export function sanitizeDashboardData(
const updatedVariables = Object.entries(selectedData.variables).reduce(
(acc, [key, value]) => {
const { selectedValue, ...rest } = value;
const { selectedValue: _selectedValue, ...rest } = value;
acc[key] = rest;
return acc;
},

View File

@@ -9,10 +9,11 @@ import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { isEmpty } from 'lodash-es';
import { Atom, Play, Terminal } from 'lucide-react';
import { Atom, Terminal } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { AlertTypes } from 'types/api/alerts/alertTypes';
@@ -165,9 +166,8 @@ function QuerySection({
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<Button
type="primary"
onClick={(): void => {
<RunQueryBtn
onStageRunQuery={(): void => {
runQuery();
logEvent('Alert: Stage and run query', {
dataSource: ALERTS_DATA_SOURCE_MAP[alertType],
@@ -176,11 +176,7 @@ function QuerySection({
queryType: queryCategory,
});
}}
className="stage-run-query"
icon={<Play size={14} />}
>
Stage & Run Query
</Button>
/>
</span>
}
items={tabs}
@@ -199,14 +195,7 @@ function QuerySection({
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<Button
type="primary"
onClick={runQuery}
className="stage-run-query"
icon={<Play size={14} />}
>
Stage & Run Query
</Button>
<RunQueryBtn onStageRunQuery={runQuery} />
</span>
}
items={items}

View File

@@ -1,14 +1,6 @@
/* eslint-disable react/display-name */
import { PlusOutlined } from '@ant-design/icons';
import {
Button,
Dropdown,
Flex,
Input,
MenuProps,
Tag,
Typography,
} from 'antd';
import { Button, Flex, Input, Typography } from 'antd';
import type { ColumnsType } from 'antd/es/table/interface';
import saveAlertApi from 'api/alerts/save';
import logEvent from 'api/common/logEvent';

View File

@@ -91,7 +91,7 @@ function Summary(): JSX.Element {
const queryFiltersWithoutId = useMemo(() => {
const filtersWithoutId = {
...queryFilters,
items: queryFilters.items.map(({ id, ...rest }) => rest),
items: queryFilters.items.map(({ id: _id, ...rest }) => rest),
};
return JSON.stringify(filtersWithoutId);
}, [queryFilters]);

View File

@@ -12,6 +12,7 @@ import {
getDefaultWidgetData,
PANEL_TYPE_TO_QUERY_TYPES,
} from 'container/NewWidget/utils';
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
// import { QueryBuilder } from 'container/QueryBuilder';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
@@ -20,7 +21,7 @@ import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { defaultTo, isUndefined } from 'lodash-es';
import { Atom, Play, Terminal } from 'lucide-react';
import { Atom, Terminal } from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
getNextWidgets,
@@ -28,20 +29,14 @@ import {
getSelectedWidgetIndex,
} from 'providers/Dashboard/util';
import { useCallback, useEffect, useMemo } from 'react';
import { UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import ClickHouseQueryContainer from './QueryBuilder/clickHouse';
import PromQLQueryContainer from './QueryBuilder/promQL';
function QuerySection({
selectedGraph,
queryResponse,
}: QueryProps): JSX.Element {
function QuerySection({ selectedGraph }: QueryProps): JSX.Element {
const {
currentQuery,
handleRunQuery: handleRunQueryFromQueryBuilder,
@@ -242,15 +237,7 @@ function QuerySection({
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<TextToolTip text="This will temporarily save the current query and graph state. This will persist across tab change" />
<Button
loading={queryResponse.isFetching}
type="primary"
onClick={handleRunQuery}
className="stage-run-query"
icon={<Play size={14} />}
>
Stage & Run Query
</Button>
<RunQueryBtn label="Stage & Run Query" onStageRunQuery={handleRunQuery} />
</span>
}
items={items}
@@ -261,10 +248,6 @@ function QuerySection({
interface QueryProps {
selectedGraph: PANEL_TYPES;
queryResponse: UseQueryResult<
SuccessResponse<MetricRangePayloadProps, unknown>,
Error
>;
}
export default QuerySection;

View File

@@ -64,7 +64,7 @@ function LeftContainer({
enableDrillDown={enableDrillDown}
/>
<QueryContainer className="query-section-left-container">
<QuerySection selectedGraph={selectedGraph} queryResponse={queryResponse} />
<QuerySection selectedGraph={selectedGraph} />
{selectedGraph === PANEL_TYPES.LIST && (
<ExplorerColumnsRenderer
selectedLogFields={selectedLogFields}

View File

@@ -166,7 +166,7 @@ function UpdateContextLinks({
onSave(newContextLink);
} catch (error) {
// Form validation failed, don't call onSave
console.log('Form validation failed:', error);
console.error('Form validation failed:', error);
}
};

View File

@@ -0,0 +1,37 @@
.run-query-btn {
display: flex;
min-width: 132px;
align-items: center;
gap: 6px;
.ant-btn-icon {
margin: 0 !important;
}
}
.cancel-query-btn {
display: flex;
min-width: 132px;
align-items: center;
gap: 6px;
}
.cmd-hint {
display: inline-flex;
align-items: center;
gap: 2px;
padding: 2px 4px;
border-radius: 4px;
//not using var here to support opacity 60%. To be handled at design system level.
background: rgba(35, 38, 46, 0.6);
line-height: 1;
font-size: 10px;
}
.lightMode {
.cmd-hint {
color: var(--bg-ink-200);
//not using var here to support opacity 60%. To be handled at design system level.
background: rgba(231, 232, 236, 0.8);
}
}

View File

@@ -0,0 +1,53 @@
import './RunQueryBtn.scss';
import { Button } from 'antd';
import {
ChevronUp,
Command,
CornerDownLeft,
Loader2,
Play,
} from 'lucide-react';
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
interface RunQueryBtnProps {
label?: string;
isLoadingQueries?: boolean;
handleCancelQuery?: () => void;
onStageRunQuery?: () => void;
}
function RunQueryBtn({
label,
isLoadingQueries,
handleCancelQuery,
onStageRunQuery,
}: RunQueryBtnProps): JSX.Element {
const isMac = getUserOperatingSystem() === UserOperatingSystem.MACOS;
return isLoadingQueries ? (
<Button
type="default"
icon={<Loader2 size={14} className="loading-icon animate-spin" />}
className="cancel-query-btn periscope-btn danger"
onClick={handleCancelQuery}
>
Cancel
</Button>
) : (
<Button
type="primary"
className="run-query-btn periscope-btn primary"
disabled={isLoadingQueries || !onStageRunQuery}
onClick={onStageRunQuery}
icon={<Play size={14} />}
>
{label || 'Run Query'}
<div className="cmd-hint">
{isMac ? <Command size={12} /> : <ChevronUp size={12} />}
<CornerDownLeft size={12} />
</div>
</Button>
);
}
export default RunQueryBtn;

View File

@@ -0,0 +1,82 @@
// frontend/src/container/QueryBuilder/components/RunQueryBtn/__tests__/RunQueryBtn.test.tsx
import { fireEvent, render, screen } from '@testing-library/react';
import RunQueryBtn from '../RunQueryBtn';
// Mock OS util
jest.mock('utils/getUserOS', () => ({
getUserOperatingSystem: jest.fn(),
UserOperatingSystem: { MACOS: 'mac', WINDOWS: 'win', LINUX: 'linux' },
}));
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
describe('RunQueryBtn', () => {
test('renders run state and triggers on click', () => {
(getUserOperatingSystem as jest.Mock).mockReturnValue(
UserOperatingSystem.MACOS,
);
const onRun = jest.fn();
render(<RunQueryBtn onStageRunQuery={onRun} />);
const btn = screen.getByRole('button', { name: /run query/i });
expect(btn).toBeEnabled();
fireEvent.click(btn);
expect(onRun).toHaveBeenCalledTimes(1);
});
test('disabled when onStageRunQuery is undefined', () => {
(getUserOperatingSystem as jest.Mock).mockReturnValue(
UserOperatingSystem.MACOS,
);
render(<RunQueryBtn />);
expect(screen.getByRole('button', { name: /run query/i })).toBeDisabled();
});
test('shows cancel state and calls handleCancelQuery', () => {
(getUserOperatingSystem as jest.Mock).mockReturnValue(
UserOperatingSystem.MACOS,
);
const onCancel = jest.fn();
render(<RunQueryBtn isLoadingQueries handleCancelQuery={onCancel} />);
const cancel = screen.getByRole('button', { name: /cancel/i });
fireEvent.click(cancel);
expect(onCancel).toHaveBeenCalledTimes(1);
});
test('shows Command + CornerDownLeft on mac', () => {
(getUserOperatingSystem as jest.Mock).mockReturnValue(
UserOperatingSystem.MACOS,
);
const { container } = render(
<RunQueryBtn onStageRunQuery={(): void => {}} />,
);
expect(container.querySelector('.lucide-command')).toBeInTheDocument();
expect(
container.querySelector('.lucide-corner-down-left'),
).toBeInTheDocument();
});
test('shows ChevronUp + CornerDownLeft on non-mac', () => {
(getUserOperatingSystem as jest.Mock).mockReturnValue(
UserOperatingSystem.WINDOWS,
);
const { container } = render(
<RunQueryBtn onStageRunQuery={(): void => {}} />,
);
expect(container.querySelector('.lucide-chevron-up')).toBeInTheDocument();
expect(container.querySelector('.lucide-command')).not.toBeInTheDocument();
expect(
container.querySelector('.lucide-corner-down-left'),
).toBeInTheDocument();
});
test('renders custom label when provided', () => {
(getUserOperatingSystem as jest.Mock).mockReturnValue(
UserOperatingSystem.MACOS,
);
const onRun = jest.fn();
render(<RunQueryBtn onStageRunQuery={onRun} label="Stage & Run Query" />);
expect(
screen.getByRole('button', { name: /stage & run query/i }),
).toBeInTheDocument();
});
});

View File

@@ -1,12 +1,12 @@
import './ToolbarActions.styles.scss';
import { Button } from 'antd';
import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { Loader2, Play } from 'lucide-react';
import { MutableRefObject, useEffect } from 'react';
import { useQueryClient } from 'react-query';
import RunQueryBtn from '../RunQueryBtn/RunQueryBtn';
interface RightToolbarActionsProps {
onStageRunQuery: () => void;
isLoadingQueries?: boolean;
@@ -42,14 +42,7 @@ export default function RightToolbarActions({
if (showLiveLogs) {
return (
<div className="right-toolbar-actions-container">
<Button
type="primary"
className="run-query-btn periscope-btn primary"
disabled
icon={<Play size={14} />}
>
Run Query
</Button>
<RunQueryBtn />
</div>
);
}
@@ -65,26 +58,11 @@ export default function RightToolbarActions({
return (
<div className="right-toolbar-actions-container">
{isLoadingQueries ? (
<Button
type="default"
icon={<Loader2 size={14} className="loading-icon animate-spin" />}
className="cancel-query-btn periscope-btn danger"
onClick={handleCancelQuery}
>
Cancel
</Button>
) : (
<Button
type="primary"
className="run-query-btn periscope-btn primary"
disabled={isLoadingQueries}
onClick={onStageRunQuery}
icon={<Play size={14} />}
>
Run Query
</Button>
)}
<RunQueryBtn
isLoadingQueries={isLoadingQueries}
handleCancelQuery={handleCancelQuery}
onStageRunQuery={onStageRunQuery}
/>
</div>
);
}

View File

@@ -136,7 +136,6 @@ const useAggregateDrilldown = ({
query,
// panelType,
aggregateData: aggregateDataWithTimeRange,
widgetId,
onClose,
});

View File

@@ -129,7 +129,6 @@ const useBaseAggregateOptions = ({
const handleBaseDrilldown = useCallback(
(key: string): void => {
console.log('Base drilldown:', { key, aggregateData });
const route = getRoute(key);
const timeRange = aggregateData?.timeRange;
const filtersToAdd = aggregateData?.filters || [];

View File

@@ -17,7 +17,6 @@ interface UseBaseAggregateOptionsProps {
query?: Query;
// panelType?: PANEL_TYPES;
aggregateData?: AggregateData | null;
widgetId?: string;
onClose: () => void;
}
@@ -27,7 +26,6 @@ const useDashboardVarConfig = ({
query,
// panelType,
aggregateData,
widgetId,
onClose,
}: UseBaseAggregateOptionsProps): {
dashbaordVariablesConfig: {
@@ -83,11 +81,6 @@ const useDashboardVarConfig = ({
dashboardVar: [string, IDashboardVariable],
fieldValue: any,
) => {
console.log('Setting variable:', {
fieldName,
dashboardVarId: dashboardVar[0],
fieldValue,
});
onValueUpdate(fieldName, dashboardVar[1]?.id, fieldValue, false);
onClose();
},
@@ -96,10 +89,6 @@ const useDashboardVarConfig = ({
const handleUnsetVariable = useCallback(
(fieldName: string, dashboardVar: [string, IDashboardVariable]) => {
console.log('Unsetting variable:', {
fieldName,
dashboardVarId: dashboardVar[0],
});
onValueUpdate(fieldName, dashboardVar[0], null, false);
onClose();
},
@@ -109,12 +98,6 @@ const useDashboardVarConfig = ({
const handleCreateVariable = useCallback(
(fieldName: string, fieldValue: string | number | boolean) => {
const source = getSourceFromQuery();
console.log('Creating variable from drilldown:', {
fieldName,
fieldValue,
source,
widgetId,
});
createVariable(
fieldName,
fieldValue,
@@ -125,7 +108,7 @@ const useDashboardVarConfig = ({
);
onClose();
},
[createVariable, getSourceFromQuery, widgetId, onClose],
[createVariable, getSourceFromQuery, onClose],
);
const contextItems = useMemo(

View File

@@ -19,20 +19,6 @@
display: flex;
align-items: center;
gap: 8px;
.cancel-query-btn {
min-width: 96px;
display: flex;
align-items: center;
gap: 2px;
}
.run-query-btn {
min-width: 96px;
display: flex;
align-items: center;
gap: 2px;
}
}
}

View File

@@ -31,8 +31,8 @@ export const normalizeSteps = (steps: FunnelStepData[]): FunnelStepData[] => {
...step.filters,
items: step.filters.items.map((item) => {
const {
id: unusedId,
isIndexed,
id: _unusedId,
isIndexed: _isIndexed,
...keyObj
} = item.key as BaseAutocompleteData;
return {

View File

@@ -143,7 +143,7 @@ export const useValidateFunnelSteps = ({
selectedTime,
steps.map((step) => {
// eslint-disable-next-line @typescript-eslint/naming-convention
const { latency_type, ...rest } = step;
const { latency_type: _latency_type, ...rest } = step;
return rest;
}),
],

View File

@@ -55,7 +55,7 @@ const useDragColumns = <T>(storageKey: LOCALSTORAGE): UseDragColumns<T> => {
const parsedDraggedColumns = await JSON.parse(localStorageColumns);
nextDraggedColumns = parsedDraggedColumns;
} catch (e) {
console.log('error while parsing json');
console.error('error while parsing json: ', e);
} finally {
redirectWithDraggedColumns(nextDraggedColumns);
}

View File

@@ -4,11 +4,6 @@ import parser from 'lib/logql/parser';
describe('lib/logql/parser', () => {
test('parse valid queries', () => {
logqlQueries.forEach((queryObject) => {
try {
parser(queryObject.query);
} catch (e) {
console.log(e);
}
expect(parser(queryObject.query)).toEqual(queryObject.parsedQuery);
});
});

View File

@@ -4,11 +4,7 @@ import { reverseParser } from 'lib/logql/reverseParser';
describe('lib/logql/reverseParser', () => {
test('reverse parse valid queries', () => {
logqlQueries.forEach((queryObject) => {
try {
expect(reverseParser(queryObject.parsedQuery)).toEqual(queryObject.query);
} catch (e) {
console.log(e);
}
expect(reverseParser(queryObject.parsedQuery)).toEqual(queryObject.query);
});
});
});

View File

@@ -11,8 +11,8 @@ describe('getYAxisScale', () => {
keyIndex: 1,
thresholdValue: 10,
thresholdUnit: 'percentunit',
moveThreshold(dragIndex, hoverIndex): void {
console.log(dragIndex, hoverIndex);
moveThreshold(): void {
// no-op
},
selectedGraph: PANEL_TYPES.TIME_SERIES,
},
@@ -21,8 +21,8 @@ describe('getYAxisScale', () => {
keyIndex: 2,
thresholdValue: 20,
thresholdUnit: 'percentunit',
moveThreshold(dragIndex, hoverIndex): void {
console.log(dragIndex, hoverIndex);
moveThreshold(): void {
// no-op
},
selectedGraph: PANEL_TYPES.TIME_SERIES,
},

View File

@@ -9,7 +9,6 @@ import {
MessagingQueuesPayloadProps,
} from 'api/messagingQueues/getConsumerLagDetails';
import axios from 'axios';
import { isNumber } from 'chart.js/helpers';
import cx from 'classnames';
import { ColumnTypeRender } from 'components/Logs/TableView/types';
import { SOMETHING_WENT_WRONG } from 'constants/api';

View File

@@ -275,7 +275,7 @@ export function setConfigDetail(
},
): void {
// remove "key" and its value from the paramsToSet object
const { key, ...restParamsToSet } = paramsToSet || {};
const { key: _key, ...restParamsToSet } = paramsToSet || {};
if (!isEmpty(restParamsToSet)) {
const configDetail = {

View File

@@ -145,7 +145,7 @@ export const removeFilter = (
const updatedValues = prevValue.filter((item: any) => item !== value);
if (updatedValues.length === 0) {
const { [filterType]: item, ...remainingFilters } = prevFilters;
const { [filterType]: _item, ...remainingFilters } = prevFilters;
return Object.keys(remainingFilters).length > 0
? (remainingFilters as FilterType)
: undefined;
@@ -175,7 +175,7 @@ export const removeAllFilters = (
return prevFilters;
}
const { [filterType]: item, ...remainingFilters } = prevFilters;
const { [filterType]: _item, ...remainingFilters } = prevFilters;
return Object.keys(remainingFilters).length > 0
? (remainingFilters as Record<

View File

@@ -20,8 +20,7 @@ export const parseQueryIntoSpanKind = (
current = parsedValue;
}
} catch (error) {
console.log(error);
console.log('error while parsing json');
console.error('error while parsing json: ', error);
}
}

View File

@@ -19,7 +19,7 @@ export const parseQueryIntoCurrent = (
current = parseInt(parsedValue, 10);
}
} catch (error) {
console.log('error while parsing json');
console.error('error while parsing json: ', error);
}
}

View File

@@ -20,8 +20,7 @@ export const parseQueryIntoOrder = (
current = parsedValue;
}
} catch (error) {
console.log(error);
console.log('error while parsing json');
console.error('error while parsing json: ', error);
}
}

View File

@@ -20,8 +20,7 @@ export const parseAggregateOrderParams = (
current = parsedValue;
}
} catch (error) {
console.log(error);
console.log('error while parsing json');
console.error('error while parsing json: ', error);
}
}

View File

@@ -6,6 +6,8 @@ const DOCLINKS = {
'https://signoz.io/docs/product-features/trace-explorer/?utm_source=product&utm_medium=traces-explorer-trace-tab#traces-view',
METRICS_EXPLORER_EMPTY_STATE:
'https://signoz.io/docs/userguide/send-metrics-cloud/',
EXTERNAL_API_MONITORING:
'https://signoz.io/docs/external-api-monitoring/overview/',
};
export default DOCLINKS;

View File

@@ -17,7 +17,7 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
ID: "CreatePublicDashboard",
Tags: []string{"dashboard"},
Summary: "Create public dashboard",
Description: "This endpoints creates public sharing config and enables public sharing of the dashboard",
Description: "This endpoint creates public sharing config and enables public sharing of the dashboard",
Request: new(dashboardtypes.PostablePublicDashboard),
RequestContentType: "",
Response: new(types.Identifiable),
@@ -34,7 +34,7 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
ID: "GetPublicDashboard",
Tags: []string{"dashboard"},
Summary: "Get public dashboard",
Description: "This endpoints returns public sharing config for a dashboard",
Description: "This endpoint returns public sharing config for a dashboard",
Request: nil,
RequestContentType: "",
Response: new(dashboardtypes.GettablePublicDasbhboard),
@@ -51,7 +51,7 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
ID: "UpdatePublicDashboard",
Tags: []string{"dashboard"},
Summary: "Update public dashboard",
Description: "This endpoints updates the public sharing config for a dashboard",
Description: "This endpoint updates the public sharing config for a dashboard",
Request: new(dashboardtypes.UpdatablePublicDashboard),
RequestContentType: "",
Response: nil,
@@ -68,7 +68,7 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
ID: "DeletePublicDashboard",
Tags: []string{"dashboard"},
Summary: "Delete public dashboard",
Description: "This endpoints deletes the public sharing config and disables the public sharing of a dashboard",
Description: "This endpoint deletes the public sharing config and disables the public sharing of a dashboard",
Request: nil,
RequestContentType: "",
Response: nil,
@@ -83,7 +83,7 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/public/dashboards/{id}", handler.New(provider.authZ.CheckWithoutClaims(
provider.dashboardHandler.GetPublicData,
authtypes.RelationRead, authtypes.RelationRead,
authtypes.RelationRead,
dashboardtypes.TypeableMetaResourcePublicDashboard,
func(req *http.Request, orgs []*types.Organization) ([]authtypes.Selector, valuer.UUID, error) {
id, err := valuer.NewUUID(mux.Vars(req)["id"])
@@ -92,11 +92,11 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
}
return provider.dashboardModule.GetPublicDashboardSelectorsAndOrg(req.Context(), id, orgs)
}), handler.OpenAPIDef{
}, []string{}), handler.OpenAPIDef{
ID: "GetPublicDashboardData",
Tags: []string{"dashboard"},
Summary: "Get public dashboard data",
Description: "This endpoints returns the sanitized dashboard data for public access",
Description: "This endpoint returns the sanitized dashboard data for public access",
Request: nil,
RequestContentType: "",
Response: new(dashboardtypes.GettablePublicDashboardData),
@@ -111,7 +111,7 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/public/dashboards/{id}/widgets/{idx}/query_range", handler.New(provider.authZ.CheckWithoutClaims(
provider.dashboardHandler.GetPublicWidgetQueryRange,
authtypes.RelationRead, authtypes.RelationRead,
authtypes.RelationRead,
dashboardtypes.TypeableMetaResourcePublicDashboard,
func(req *http.Request, orgs []*types.Organization) ([]authtypes.Selector, valuer.UUID, error) {
id, err := valuer.NewUUID(mux.Vars(req)["id"])
@@ -120,7 +120,7 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
}
return provider.dashboardModule.GetPublicDashboardSelectorsAndOrg(req.Context(), id, orgs)
}), handler.OpenAPIDef{
}, []string{}), handler.OpenAPIDef{
ID: "GetPublicDashboardWidgetQueryRange",
Tags: []string{"dashboard"},
Summary: "Get query range result",

View File

@@ -13,7 +13,7 @@ func (provider *provider) addGlobalRoutes(router *mux.Router) error {
ID: "GetGlobalConfig",
Tags: []string{"global"},
Summary: "Get global config",
Description: "This endpoints returns global config",
Description: "This endpoint returns global config",
Request: nil,
RequestContentType: "",
Response: new(types.GettableGlobalConfig),

View File

@@ -17,6 +17,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/preference"
"github.com/SigNoz/signoz/pkg/modules/promote"
"github.com/SigNoz/signoz/pkg/modules/role"
"github.com/SigNoz/signoz/pkg/modules/session"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/types"
@@ -41,6 +42,8 @@ type provider struct {
dashboardHandler dashboard.Handler
metricsExplorerHandler metricsexplorer.Handler
gatewayHandler gateway.Handler
roleGetter role.Getter
roleHandler role.Handler
}
func NewFactory(
@@ -58,9 +61,11 @@ func NewFactory(
dashboardHandler dashboard.Handler,
metricsExplorerHandler metricsexplorer.Handler,
gatewayHandler gateway.Handler,
roleGetter role.Getter,
roleHandler role.Handler,
) factory.ProviderFactory[apiserver.APIServer, apiserver.Config] {
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, providerSettings factory.ProviderSettings, config apiserver.Config) (apiserver.APIServer, error) {
return newProvider(ctx, providerSettings, config, orgGetter, authz, orgHandler, userHandler, sessionHandler, authDomainHandler, preferenceHandler, globalHandler, promoteHandler, flaggerHandler, dashboardModule, dashboardHandler, metricsExplorerHandler, gatewayHandler)
return newProvider(ctx, providerSettings, config, orgGetter, authz, orgHandler, userHandler, sessionHandler, authDomainHandler, preferenceHandler, globalHandler, promoteHandler, flaggerHandler, dashboardModule, dashboardHandler, metricsExplorerHandler, gatewayHandler, roleGetter, roleHandler)
})
}
@@ -82,6 +87,8 @@ func newProvider(
dashboardHandler dashboard.Handler,
metricsExplorerHandler metricsexplorer.Handler,
gatewayHandler gateway.Handler,
roleGetter role.Getter,
roleHandler role.Handler,
) (apiserver.APIServer, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/apiserver/signozapiserver")
router := mux.NewRouter().UseEncodedPath()
@@ -102,9 +109,11 @@ func newProvider(
dashboardHandler: dashboardHandler,
metricsExplorerHandler: metricsExplorerHandler,
gatewayHandler: gatewayHandler,
roleGetter: roleGetter,
roleHandler: roleHandler,
}
provider.authZ = middleware.NewAuthZ(settings.Logger(), orgGetter, authz)
provider.authZ = middleware.NewAuthZ(settings.Logger(), orgGetter, authz, roleGetter)
if err := provider.AddToRouter(router); err != nil {
return nil, err
@@ -162,6 +171,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
return err
}
if err := provider.addRoleRoutes(router); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,99 @@
package signozapiserver
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/gorilla/mux"
)
func (provider *provider) addRoleRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/roles", handler.New(provider.authZ.AdminAccess(provider.roleHandler.Create), handler.OpenAPIDef{
ID: "CreateRole",
Tags: []string{"role"},
Summary: "Create role",
Description: "This endpoint creates a role",
Request: nil,
RequestContentType: "",
Response: new(types.Identifiable),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/roles", handler.New(provider.authZ.AdminAccess(provider.roleHandler.List), handler.OpenAPIDef{
ID: "ListRoles",
Tags: []string{"role"},
Summary: "List roles",
Description: "This endpoint lists all roles",
Request: nil,
RequestContentType: "",
Response: make([]*roletypes.Role, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authZ.AdminAccess(provider.roleHandler.Get), handler.OpenAPIDef{
ID: "GetRole",
Tags: []string{"role"},
Summary: "Get role",
Description: "This endpoint gets a role",
Request: nil,
RequestContentType: "",
Response: new(roletypes.Role),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authZ.AdminAccess(provider.roleHandler.Patch), handler.OpenAPIDef{
ID: "PatchRole",
Tags: []string{"role"},
Summary: "Patch role",
Description: "This endpoint patches a role",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPatch).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authZ.AdminAccess(provider.roleHandler.Delete), handler.OpenAPIDef{
ID: "DeleteRole",
Tags: []string{"role"},
Summary: "Delete role",
Description: "This endpoint deletes a role",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -315,5 +315,22 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/factor_password/forgot", handler.New(provider.authZ.OpenAccess(provider.userHandler.ForgotPassword), handler.OpenAPIDef{
ID: "ForgotPassword",
Tags: []string{"users"},
Summary: "Forgot password",
Description: "This endpoint initiates the forgot password flow by sending a reset password email",
Request: new(types.PostableForgotPassword),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{},
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -16,9 +16,10 @@ type AuthZ interface {
Check(context.Context, *openfgav1.TupleKey) error
// CheckWithTupleCreation takes upon the responsibility for generating the tuples alongside everything Check does.
CheckWithTupleCreation(context.Context, authtypes.Claims, valuer.UUID, authtypes.Relation, authtypes.Relation, authtypes.Typeable, []authtypes.Selector) error
CheckWithTupleCreation(context.Context, authtypes.Claims, valuer.UUID, authtypes.Relation, authtypes.Typeable, []authtypes.Selector, []authtypes.Selector) error
CheckWithTupleCreationWithoutClaims(context.Context, valuer.UUID, authtypes.Relation, authtypes.Relation, authtypes.Typeable, []authtypes.Selector) error
// CheckWithTupleCreationWithoutClaims checks permissions for anonymous users.
CheckWithTupleCreationWithoutClaims(context.Context, valuer.UUID, authtypes.Relation, authtypes.Typeable, []authtypes.Selector, []authtypes.Selector) error
// Batch Check returns error when the upstream authorization server is unavailable or for all the tuples of subject (s) doesn't have relation (r) on object (o).
BatchCheck(context.Context, []*openfgav1.TupleKey) error

View File

@@ -152,17 +152,17 @@ func (provider *provider) BatchCheck(ctx context.Context, tupleReq []*openfgav1.
}
}
return errors.New(errors.TypeForbidden, authtypes.ErrCodeAuthZForbidden, "")
return errors.Newf(errors.TypeForbidden, authtypes.ErrCodeAuthZForbidden, "none of the subjects are allowed for requested access")
}
func (provider *provider) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, _ authtypes.Relation, translation authtypes.Relation, _ authtypes.Typeable, _ []authtypes.Selector) error {
func (provider *provider) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, _ authtypes.Relation, _ authtypes.Typeable, _ []authtypes.Selector, roleSelectors []authtypes.Selector) error {
subject, err := authtypes.NewSubject(authtypes.TypeableUser, claims.UserID, orgID, nil)
if err != nil {
return err
}
tuples, err := authtypes.TypeableOrganization.Tuples(subject, translation, []authtypes.Selector{authtypes.MustNewSelector(authtypes.TypeOrganization, orgID.StringValue())}, orgID)
tuples, err := authtypes.TypeableRole.Tuples(subject, authtypes.RelationAssignee, roleSelectors, orgID)
if err != nil {
return err
}
@@ -175,13 +175,13 @@ func (provider *provider) CheckWithTupleCreation(ctx context.Context, claims aut
return nil
}
func (provider *provider) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, _ authtypes.Relation, translation authtypes.Relation, _ authtypes.Typeable, _ []authtypes.Selector) error {
func (provider *provider) 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
}
tuples, err := authtypes.TypeableOrganization.Tuples(subject, translation, []authtypes.Selector{authtypes.MustNewSelector(authtypes.TypeOrganization, orgID.StringValue())}, orgID)
tuples, err := authtypes.TypeableRole.Tuples(subject, authtypes.RelationAssignee, roleSelectors, orgID)
if err != nil {
return err
}
@@ -195,6 +195,10 @@ func (provider *provider) CheckWithTupleCreationWithoutClaims(ctx context.Contex
}
func (provider *provider) Write(ctx context.Context, additions []*openfgav1.TupleKey, deletions []*openfgav1.TupleKey) error {
if len(additions) == 0 && len(deletions) == 0 {
return nil
}
storeID, modelID := provider.getStoreIDandModelID()
deletionTuplesWithoutCondition := make([]*openfgav1.TupleKeyWithoutCondition, len(deletions))
for idx, tuple := range deletions {

View File

@@ -34,11 +34,11 @@ func TestProviderStartStop(t *testing.T) {
sqlstore.Mock().ExpectQuery("SELECT authorization_model_id, schema_version, type, type_definition, serialized_protobuf FROM authorization_model WHERE authorization_model_id = (.+) AND store = (.+)").WithArgs("01K44QQKXR6F729W160NFCJT58", "01K3V0NTN47MPTMEV1PD5ST6ZC").WillReturnRows(modelRows)
sqlstore.Mock().ExpectExec("INSERT INTO authorization_model (.+) VALUES (.+)").WillReturnResult(sqlmock.NewResult(1, 1))
go func() {
err := provider.Start(context.Background())
require.NoError(t, err)
}()
// wait for the service to start
time.Sleep(time.Second * 2)

View File

@@ -7,6 +7,7 @@ import (
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/role"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
@@ -20,14 +21,15 @@ type AuthZ struct {
logger *slog.Logger
orgGetter organization.Getter
authzService authz.AuthZ
roleGetter role.Getter
}
func NewAuthZ(logger *slog.Logger, orgGetter organization.Getter, authzService authz.AuthZ) *AuthZ {
func NewAuthZ(logger *slog.Logger, orgGetter organization.Getter, authzService authz.AuthZ, roleGetter role.Getter) *AuthZ {
if logger == nil {
panic("cannot build authz middleware, logger is empty")
}
return &AuthZ{logger: logger, orgGetter: orgGetter, authzService: authzService}
return &AuthZ{logger: logger, orgGetter: orgGetter, authzService: authzService, roleGetter: roleGetter}
}
func (middleware *AuthZ) ViewAccess(next http.HandlerFunc) http.HandlerFunc {
@@ -109,9 +111,10 @@ func (middleware *AuthZ) OpenAccess(next http.HandlerFunc) http.HandlerFunc {
})
}
func (middleware *AuthZ) Check(next http.HandlerFunc, relation authtypes.Relation, translation authtypes.Relation, typeable authtypes.Typeable, cb authtypes.SelectorCallbackWithClaimsFn) 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) {
claims, err := authtypes.ClaimsFromContext(req.Context())
ctx := req.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
@@ -129,7 +132,18 @@ func (middleware *AuthZ) Check(next http.HandlerFunc, relation authtypes.Relatio
return
}
err = middleware.authzService.CheckWithTupleCreation(req.Context(), claims, orgId, relation, translation, typeable, selectors)
roles, err := middleware.roleGetter.ListByOrgIDAndNames(req.Context(), orgId, roles)
if err != nil {
render.Error(rw, err)
return
}
roleSelectors := []authtypes.Selector{}
for _, role := range roles {
selectors = append(selectors, authtypes.MustNewSelector(authtypes.TypeRole, role.ID.String()))
}
err = middleware.authzService.CheckWithTupleCreation(ctx, claims, orgId, relation, typeable, selectors, roleSelectors)
if err != nil {
render.Error(rw, err)
return
@@ -139,7 +153,7 @@ func (middleware *AuthZ) Check(next http.HandlerFunc, relation authtypes.Relatio
})
}
func (middleware *AuthZ) CheckWithoutClaims(next http.HandlerFunc, relation authtypes.Relation, translation authtypes.Relation, typeable authtypes.Typeable, cb authtypes.SelectorCallbackWithoutClaimsFn) 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)
@@ -154,7 +168,7 @@ func (middleware *AuthZ) CheckWithoutClaims(next http.HandlerFunc, relation auth
return
}
err = middleware.authzService.CheckWithTupleCreationWithoutClaims(ctx, orgID, relation, translation, typeable, selectors)
err = middleware.authzService.CheckWithTupleCreationWithoutClaims(ctx, orgID, relation, typeable, selectors, selectors)
if err != nil {
render.Error(rw, err)
return

View File

@@ -0,0 +1,63 @@
package implrole
import (
"context"
"github.com/SigNoz/signoz/pkg/modules/role"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type getter struct {
store roletypes.Store
}
func NewGetter(store roletypes.Store) role.Getter {
return &getter{store: store}
}
func (getter *getter) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*roletypes.Role, error) {
storableRole, err := getter.store.Get(ctx, orgID, id)
if err != nil {
return nil, err
}
return roletypes.NewRoleFromStorableRole(storableRole), nil
}
func (getter *getter) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*roletypes.Role, error) {
storableRole, err := getter.store.GetByOrgIDAndName(ctx, orgID, name)
if err != nil {
return nil, err
}
return roletypes.NewRoleFromStorableRole(storableRole), nil
}
func (getter *getter) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes.Role, error) {
storableRoles, err := getter.store.List(ctx, orgID)
if err != nil {
return nil, err
}
roles := make([]*roletypes.Role, len(storableRoles))
for idx, storableRole := range storableRoles {
roles[idx] = roletypes.NewRoleFromStorableRole(storableRole)
}
return roles, nil
}
func (getter *getter) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*roletypes.Role, error) {
storableRoles, err := getter.store.ListByOrgIDAndNames(ctx, orgID, names)
if err != nil {
return nil, err
}
roles := make([]*roletypes.Role, len(storableRoles))
for idx, storable := range storableRoles {
roles[idx] = roletypes.NewRoleFromStorableRole(storable)
}
return roles, nil
}

View File

@@ -0,0 +1,108 @@
package implrole
import (
"context"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/modules/role"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type granter struct {
store roletypes.Store
authz authz.AuthZ
}
func NewGranter(store roletypes.Store, authz authz.AuthZ) role.Granter {
return &granter{store: store, authz: authz}
}
func (granter *granter) Grant(ctx context.Context, orgID valuer.UUID, name string, subject string) error {
role, err := granter.store.GetByOrgIDAndName(ctx, orgID, name)
if err != nil {
return err
}
tuples, err := authtypes.TypeableRole.Tuples(
subject,
authtypes.RelationAssignee,
[]authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, role.ID.StringValue()),
},
orgID,
)
if err != nil {
return err
}
return granter.authz.Write(ctx, tuples, nil)
}
func (granter *granter) GrantByID(ctx context.Context, orgID valuer.UUID, id valuer.UUID, subject string) error {
tuples, err := authtypes.TypeableRole.Tuples(
subject,
authtypes.RelationAssignee,
[]authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, id.StringValue()),
},
orgID,
)
if err != nil {
return err
}
return granter.authz.Write(ctx, tuples, nil)
}
func (granter *granter) ModifyGrant(ctx context.Context, orgID valuer.UUID, existingRoleName string, updatedRoleName string, subject string) error {
err := granter.Revoke(ctx, orgID, existingRoleName, subject)
if err != nil {
return err
}
err = granter.Grant(ctx, orgID, updatedRoleName, subject)
if err != nil {
return err
}
return nil
}
func (granter *granter) Revoke(ctx context.Context, orgID valuer.UUID, name string, subject string) error {
role, err := granter.store.GetByOrgIDAndName(ctx, orgID, name)
if err != nil {
return err
}
tuples, err := authtypes.TypeableRole.Tuples(
subject,
authtypes.RelationAssignee,
[]authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, role.ID.StringValue()),
},
orgID,
)
if err != nil {
return err
}
return granter.authz.Write(ctx, nil, tuples)
}
func (granter *granter) CreateManagedRoles(ctx context.Context, _ valuer.UUID, managedRoles []*roletypes.Role) error {
err := granter.store.RunInTx(ctx, func(ctx context.Context) error {
for _, role := range managedRoles {
err := granter.store.Create(ctx, roletypes.NewStorableRoleFromRole(role))
if err != nil {
return err
}
}
return nil
})
if err != nil {
return err
}
return nil
}

View File

@@ -14,11 +14,12 @@ import (
)
type handler struct {
module role.Module
setter role.Setter
getter role.Getter
}
func NewHandler(module role.Module) role.Handler {
return &handler{module: module}
func NewHandler(setter role.Setter, getter role.Getter) role.Handler {
return &handler{setter: setter, getter: getter}
}
func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
@@ -35,7 +36,7 @@ func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
return
}
err = handler.module.Create(ctx, roletypes.NewRole(req.Name, req.Description, roletypes.RoleTypeCustom.StringValue(), valuer.MustNewUUID(claims.OrgID)))
err = handler.setter.Create(ctx, valuer.MustNewUUID(claims.OrgID), roletypes.NewRole(req.Name, req.Description, roletypes.RoleTypeCustom, valuer.MustNewUUID(claims.OrgID)))
if err != nil {
render.Error(rw, err)
return
@@ -63,7 +64,7 @@ func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
return
}
role, err := handler.module.Get(ctx, valuer.MustNewUUID(claims.OrgID), roleID)
role, err := handler.getter.Get(ctx, valuer.MustNewUUID(claims.OrgID), roleID)
if err != nil {
render.Error(rw, err)
return
@@ -102,7 +103,7 @@ func (handler *handler) GetObjects(rw http.ResponseWriter, r *http.Request) {
return
}
objects, err := handler.module.GetObjects(ctx, valuer.MustNewUUID(claims.OrgID), roleID, relation)
objects, err := handler.setter.GetObjects(ctx, valuer.MustNewUUID(claims.OrgID), roleID, relation)
if err != nil {
render.Error(rw, err)
return
@@ -113,7 +114,7 @@ func (handler *handler) GetObjects(rw http.ResponseWriter, r *http.Request) {
func (handler *handler) GetResources(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
resources := handler.module.GetResources(ctx)
resources := handler.setter.GetResources(ctx)
var resourceRelations = struct {
Resources []*authtypes.Resource `json:"resources"`
@@ -133,7 +134,7 @@ func (handler *handler) List(rw http.ResponseWriter, r *http.Request) {
return
}
roles, err := handler.module.List(ctx, valuer.MustNewUUID(claims.OrgID))
roles, err := handler.getter.List(ctx, valuer.MustNewUUID(claims.OrgID))
if err != nil {
render.Error(rw, err)
return
@@ -162,14 +163,19 @@ func (handler *handler) Patch(rw http.ResponseWriter, r *http.Request) {
return
}
role, err := handler.module.Get(ctx, valuer.MustNewUUID(claims.OrgID), id)
role, err := handler.getter.Get(ctx, valuer.MustNewUUID(claims.OrgID), id)
if err != nil {
render.Error(rw, err)
return
}
role.PatchMetadata(req.Name, req.Description)
err = handler.module.Patch(ctx, valuer.MustNewUUID(claims.OrgID), role)
err = role.PatchMetadata(req.Name, req.Description)
if err != nil {
render.Error(rw, err)
return
}
err = handler.setter.Patch(ctx, valuer.MustNewUUID(claims.OrgID), role)
if err != nil {
render.Error(rw, err)
return
@@ -204,13 +210,19 @@ func (handler *handler) PatchObjects(rw http.ResponseWriter, r *http.Request) {
return
}
patchableObjects, err := roletypes.NewPatchableObjects(req.Additions, req.Deletions, relation)
role, err := handler.getter.Get(ctx, valuer.MustNewUUID(claims.OrgID), id)
if err != nil {
render.Error(rw, err)
return
}
err = handler.module.PatchObjects(ctx, valuer.MustNewUUID(claims.OrgID), id, relation, patchableObjects.Additions, patchableObjects.Deletions)
patchableObjects, err := role.NewPatchableObjects(req.Additions, req.Deletions, relation)
if err != nil {
render.Error(rw, err)
return
}
err = handler.setter.PatchObjects(ctx, valuer.MustNewUUID(claims.OrgID), id, relation, patchableObjects.Additions, patchableObjects.Deletions)
if err != nil {
render.Error(rw, err)
return
@@ -233,7 +245,7 @@ func (handler *handler) Delete(rw http.ResponseWriter, r *http.Request) {
return
}
err = handler.module.Delete(ctx, valuer.MustNewUUID(claims.OrgID), id)
err = handler.setter.Delete(ctx, valuer.MustNewUUID(claims.OrgID), id)
if err != nil {
render.Error(rw, err)
return

View File

@@ -1,164 +0,0 @@
package implrole
import (
"context"
"slices"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/modules/role"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type module struct {
store roletypes.Store
registry []role.RegisterTypeable
authz authz.AuthZ
}
func NewModule(store roletypes.Store, authz authz.AuthZ, registry []role.RegisterTypeable) role.Module {
return &module{
store: store,
authz: authz,
registry: registry,
}
}
func (module *module) Create(ctx context.Context, role *roletypes.Role) error {
return module.store.Create(ctx, roletypes.NewStorableRoleFromRole(role))
}
func (module *module) GetOrCreate(ctx context.Context, role *roletypes.Role) (*roletypes.Role, error) {
existingRole, err := module.store.GetByNameAndOrgID(ctx, role.Name, role.OrgID)
if err != nil {
if !errors.Ast(err, errors.TypeNotFound) {
return nil, err
}
}
if existingRole != nil {
return roletypes.NewRoleFromStorableRole(existingRole), nil
}
err = module.store.Create(ctx, roletypes.NewStorableRoleFromRole(role))
if err != nil {
return nil, err
}
return role, nil
}
func (module *module) GetResources(_ context.Context) []*authtypes.Resource {
typeables := make([]authtypes.Typeable, 0)
for _, register := range module.registry {
typeables = append(typeables, register.MustGetTypeables()...)
}
// role module cannot self register itself!
typeables = append(typeables, module.MustGetTypeables()...)
resources := make([]*authtypes.Resource, 0)
for _, typeable := range typeables {
resources = append(resources, &authtypes.Resource{Name: typeable.Name(), Type: typeable.Type()})
}
return resources
}
func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*roletypes.Role, error) {
storableRole, err := module.store.Get(ctx, orgID, id)
if err != nil {
return nil, err
}
return roletypes.NewRoleFromStorableRole(storableRole), nil
}
func (module *module) GetObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation) ([]*authtypes.Object, error) {
storableRole, err := module.store.Get(ctx, orgID, id)
if err != nil {
return nil, err
}
objects := make([]*authtypes.Object, 0)
for _, resource := range module.GetResources(ctx) {
if slices.Contains(authtypes.TypeableRelations[resource.Type], relation) {
resourceObjects, err := module.
authz.
ListObjects(
ctx,
authtypes.MustNewSubject(authtypes.TypeableRole, storableRole.ID.String(), orgID, &authtypes.RelationAssignee),
relation,
authtypes.MustNewTypeableFromType(resource.Type, resource.Name),
)
if err != nil {
return nil, err
}
objects = append(objects, resourceObjects...)
}
}
return objects, nil
}
func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes.Role, error) {
storableRoles, err := module.store.List(ctx, orgID)
if err != nil {
return nil, err
}
roles := make([]*roletypes.Role, len(storableRoles))
for idx, storableRole := range storableRoles {
roles[idx] = roletypes.NewRoleFromStorableRole(storableRole)
}
return roles, nil
}
func (module *module) Patch(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) error {
return module.store.Update(ctx, orgID, roletypes.NewStorableRoleFromRole(role))
}
func (module *module) PatchObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation, additions, deletions []*authtypes.Object) error {
additionTuples, err := roletypes.GetAdditionTuples(id, orgID, relation, additions)
if err != nil {
return err
}
deletionTuples, err := roletypes.GetDeletionTuples(id, orgID, relation, deletions)
if err != nil {
return err
}
err = module.authz.Write(ctx, additionTuples, deletionTuples)
if err != nil {
return err
}
return nil
}
func (module *module) Assign(ctx context.Context, id valuer.UUID, orgID valuer.UUID, subject string) error {
tuples, err := authtypes.TypeableRole.Tuples(
subject,
authtypes.RelationAssignee,
[]authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, id.StringValue()),
},
orgID,
)
if err != nil {
return err
}
return module.authz.Write(ctx, tuples, nil)
}
func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
return module.store.Delete(ctx, orgID, id)
}
func (module *module) MustGetTypeables() []authtypes.Typeable {
return []authtypes.Typeable{authtypes.TypeableRole, roletypes.TypeableResourcesRoles}
}

View File

@@ -0,0 +1,53 @@
package implrole
import (
"context"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/modules/role"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type setter struct {
store roletypes.Store
authz authz.AuthZ
}
func NewSetter(store roletypes.Store, authz authz.AuthZ) role.Setter {
return &setter{store: store, authz: authz}
}
func (setter *setter) Create(_ context.Context, _ valuer.UUID, _ *roletypes.Role) error {
return errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented")
}
func (setter *setter) GetOrCreate(_ context.Context, _ valuer.UUID, _ *roletypes.Role) (*roletypes.Role, error) {
return nil, errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented")
}
func (setter *setter) GetResources(_ context.Context) []*authtypes.Resource {
return nil
}
func (setter *setter) GetObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation) ([]*authtypes.Object, error) {
return nil, errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented")
}
func (setter *setter) Patch(_ context.Context, _ valuer.UUID, _ *roletypes.Role) error {
return errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented")
}
func (setter *setter) PatchObjects(_ context.Context, _ valuer.UUID, _ valuer.UUID, _ authtypes.Relation, _, _ []*authtypes.Object) error {
return errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented")
}
func (setter *setter) Delete(_ context.Context, _ valuer.UUID, _ valuer.UUID) error {
return errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented")
}
func (setter *setter) MustGetTypeables() []authtypes.Typeable {
return nil
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
type store struct {
@@ -20,7 +21,7 @@ func NewStore(sqlstore sqlstore.SQLStore) roletypes.Store {
func (store *store) Create(ctx context.Context, role *roletypes.StorableRole) error {
_, err := store.
sqlstore.
BunDB().
BunDBCtx(ctx).
NewInsert().
Model(role).
Exec(ctx)
@@ -35,7 +36,7 @@ func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID)
role := new(roletypes.StorableRole)
err := store.
sqlstore.
BunDB().
BunDBCtx(ctx).
NewSelect().
Model(role).
Where("org_id = ?", orgID).
@@ -48,11 +49,11 @@ func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID)
return role, nil
}
func (store *store) GetByNameAndOrgID(ctx context.Context, name string, orgID valuer.UUID) (*roletypes.StorableRole, error) {
func (store *store) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*roletypes.StorableRole, error) {
role := new(roletypes.StorableRole)
err := store.
sqlstore.
BunDB().
BunDBCtx(ctx).
NewSelect().
Model(role).
Where("org_id = ?", orgID).
@@ -69,13 +70,30 @@ func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes.S
roles := make([]*roletypes.StorableRole, 0)
err := store.
sqlstore.
BunDB().
BunDBCtx(ctx).
NewSelect().
Model(&roles).
Where("org_id = ?", orgID).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, roletypes.ErrCodeRoleNotFound, "no roles found in org_id: %s", orgID)
return nil, err
}
return roles, nil
}
func (store *store) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*roletypes.StorableRole, error) {
roles := make([]*roletypes.StorableRole, 0)
err := store.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(&roles).
Where("org_id = ?", orgID).
Where("name IN (?)", bun.In(names)).
Scan(ctx)
if err != nil {
return nil, err
}
return roles, nil
@@ -84,7 +102,7 @@ func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes.S
func (store *store) Update(ctx context.Context, orgID valuer.UUID, role *roletypes.StorableRole) error {
_, err := store.
sqlstore.
BunDB().
BunDBCtx(ctx).
NewUpdate().
Model(role).
WherePK().
@@ -100,7 +118,7 @@ func (store *store) Update(ctx context.Context, orgID valuer.UUID, role *roletyp
func (store *store) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
_, err := store.
sqlstore.
BunDB().
BunDBCtx(ctx).
NewDelete().
Model(new(roletypes.StorableRole)).
Where("org_id = ?", orgID).

View File

@@ -9,22 +9,16 @@ import (
"github.com/SigNoz/signoz/pkg/valuer"
)
type Module interface {
type Setter interface {
// Creates the role.
Create(context.Context, *roletypes.Role) error
Create(context.Context, valuer.UUID, *roletypes.Role) error
// Gets the role if it exists or creates one.
GetOrCreate(context.Context, *roletypes.Role) (*roletypes.Role, error)
// Gets the role
Get(context.Context, valuer.UUID, valuer.UUID) (*roletypes.Role, error)
GetOrCreate(context.Context, valuer.UUID, *roletypes.Role) (*roletypes.Role, error)
// Gets the objects associated with the given role and relation.
GetObjects(context.Context, valuer.UUID, valuer.UUID, authtypes.Relation) ([]*authtypes.Object, error)
// Lists all the roles for the organization.
List(context.Context, valuer.UUID) ([]*roletypes.Role, error)
// Gets all the typeable resources registered from role registry.
GetResources(context.Context) []*authtypes.Resource
@@ -37,12 +31,40 @@ type Module interface {
// Deletes the role and tuples in authorization server.
Delete(context.Context, valuer.UUID, valuer.UUID) error
// Assigns role to the given subject.
Assign(context.Context, valuer.UUID, valuer.UUID, string) error
RegisterTypeable
}
type Getter interface {
// Gets the role
Get(context.Context, valuer.UUID, valuer.UUID) (*roletypes.Role, error)
// Gets the role by org_id and name
GetByOrgIDAndName(context.Context, valuer.UUID, string) (*roletypes.Role, error)
// Lists all the roles for the organization.
List(context.Context, valuer.UUID) ([]*roletypes.Role, error)
// Lists all the roles for the organization filtered by name
ListByOrgIDAndNames(context.Context, valuer.UUID, []string) ([]*roletypes.Role, error)
}
type Granter interface {
// Grants a role to the subject based on role name.
Grant(context.Context, valuer.UUID, string, string) error
// Grants a role to the subject based on role id.
GrantByID(context.Context, valuer.UUID, valuer.UUID, string) error
// Revokes a granted role from the subject based on role name.
Revoke(context.Context, valuer.UUID, string, string) error
// Changes the granted role for the subject based on role name.
ModifyGrant(context.Context, valuer.UUID, string, string, string) error
// Bootstrap the managed roles.
CreateManagedRoles(context.Context, valuer.UUID, []*roletypes.Role) error
}
type RegisterTypeable interface {
MustGetTypeables() []authtypes.Typeable
}

View File

@@ -0,0 +1,43 @@
package user
import (
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
)
type Config struct {
Password PasswordConfig `mapstructure:"password"`
}
type PasswordConfig struct {
Reset ResetConfig `mapstructure:"reset"`
}
type ResetConfig struct {
AllowSelf bool `mapstructure:"allow_self"`
MaxTokenLifetime time.Duration `mapstructure:"max_token_lifetime"`
}
func NewConfigFactory() factory.ConfigFactory {
return factory.NewConfigFactory(factory.MustNewName("user"), newConfig)
}
func newConfig() factory.Config {
return &Config{
Password: PasswordConfig{
Reset: ResetConfig{
AllowSelf: false,
MaxTokenLifetime: 6 * time.Hour,
},
},
}
}
func (c Config) Validate() error {
if c.Password.Reset.MaxTokenLifetime <= 0 {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "user::password::reset::max_token_lifetime must be positive")
}
return nil
}

View File

@@ -332,6 +332,25 @@ func (handler *handler) ChangePassword(w http.ResponseWriter, r *http.Request) {
render.Success(w, http.StatusNoContent, nil)
}
func (h *handler) ForgotPassword(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
req := new(types.PostableForgotPassword)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(w, err)
return
}
err := h.module.ForgotPassword(ctx, req.OrgID, req.Email, req.FrontendBaseURL)
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusNoContent, nil)
}
func (h *handler) CreateAPIKey(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()

View File

@@ -12,11 +12,14 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/role"
"github.com/SigNoz/signoz/pkg/modules/user"
root "github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/tokenizer"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/emailtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/dustin/go-humanize"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
@@ -27,11 +30,13 @@ type Module struct {
emailing emailing.Emailing
settings factory.ScopedProviderSettings
orgSetter organization.Setter
granter role.Granter
analytics analytics.Analytics
config user.Config
}
// This module is a WIP, don't take inspiration from this.
func NewModule(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing emailing.Emailing, providerSettings factory.ProviderSettings, orgSetter organization.Setter, analytics analytics.Analytics) root.Module {
func NewModule(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing emailing.Emailing, providerSettings factory.ProviderSettings, orgSetter organization.Setter, granter role.Granter, analytics analytics.Analytics, config user.Config) root.Module {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/user/impluser")
return &Module{
store: store,
@@ -40,6 +45,8 @@ func NewModule(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing em
settings: settings,
orgSetter: orgSetter,
analytics: analytics,
granter: granter,
config: config,
}
}
@@ -223,7 +230,6 @@ func (m *Module) UpdateUser(ctx context.Context, orgID valuer.UUID, id string, u
}
user.UpdatedAt = time.Now()
updatedUser, err := m.store.UpdateUser(ctx, orgID, id, user)
if err != nil {
return nil, err
@@ -254,8 +260,8 @@ func (m *Module) UpdateUser(ctx context.Context, orgID valuer.UUID, id string, u
return updatedUser, nil
}
func (m *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id string, deletedBy string) error {
user, err := m.store.GetUser(ctx, valuer.MustNewUUID(id))
func (module *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id string, deletedBy string) error {
user, err := module.store.GetUser(ctx, valuer.MustNewUUID(id))
if err != nil {
return err
}
@@ -265,7 +271,7 @@ func (m *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id string, d
}
// don't allow to delete the last admin user
adminUsers, err := m.store.GetUsersByRoleAndOrgID(ctx, types.RoleAdmin, orgID)
adminUsers, err := module.store.GetUsersByRoleAndOrgID(ctx, types.RoleAdmin, orgID)
if err != nil {
return err
}
@@ -274,11 +280,11 @@ func (m *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id string, d
return errors.New(errors.TypeForbidden, errors.CodeForbidden, "cannot delete the last admin")
}
if err := m.store.DeleteUser(ctx, orgID.String(), user.ID.StringValue()); err != nil {
if err := module.store.DeleteUser(ctx, orgID.String(), user.ID.StringValue()); err != nil {
return err
}
m.analytics.TrackUser(ctx, user.OrgID.String(), user.ID.String(), "User Deleted", map[string]any{
module.analytics.TrackUser(ctx, user.OrgID.String(), user.ID.String(), "User Deleted", map[string]any{
"deleted_by": deletedBy,
})
@@ -302,33 +308,91 @@ func (module *Module) GetOrCreateResetPasswordToken(ctx context.Context, userID
}
}
resetPasswordToken, err := types.NewResetPasswordToken(password.ID)
// check if a token already exists for this password id
existingResetPasswordToken, err := module.store.GetResetPasswordTokenByPasswordID(ctx, password.ID)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return nil, err // return the error if it is not a not found error
}
// return the existing token if it is not expired
if existingResetPasswordToken != nil && !existingResetPasswordToken.IsExpired() {
return existingResetPasswordToken, nil // return the existing token if it is not expired
}
// delete the existing token entry
if existingResetPasswordToken != nil {
if err := module.store.DeleteResetPasswordTokenByPasswordID(ctx, password.ID); err != nil {
return nil, err
}
}
// create a new token
resetPasswordToken, err := types.NewResetPasswordToken(password.ID, time.Now().Add(module.config.Password.Reset.MaxTokenLifetime))
if err != nil {
return nil, err
}
// create a new token
err = module.store.CreateResetPasswordToken(ctx, resetPasswordToken)
if err != nil {
if !errors.Ast(err, errors.TypeAlreadyExists) {
return nil, err
}
// if the token already exists, we return the existing token
resetPasswordToken, err = module.store.GetResetPasswordTokenByPasswordID(ctx, password.ID)
if err != nil {
return nil, err
}
return nil, err
}
return resetPasswordToken, nil
}
func (module *Module) ForgotPassword(ctx context.Context, orgID valuer.UUID, email valuer.Email, frontendBaseURL string) error {
if !module.config.Password.Reset.AllowSelf {
return errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "users are not allowed to reset their password themselves, please contact an admin to reset your password")
}
user, err := module.store.GetUserByEmailAndOrgID(ctx, email, orgID)
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {
return nil // for security reasons
}
return err
}
token, err := module.GetOrCreateResetPasswordToken(ctx, user.ID)
if err != nil {
module.settings.Logger().ErrorContext(ctx, "failed to create reset password token", "error", err)
return err
}
resetLink := fmt.Sprintf("%s/password-reset?token=%s", frontendBaseURL, token.Token)
tokenLifetime := module.config.Password.Reset.MaxTokenLifetime
humanizedTokenLifetime := strings.TrimSpace(humanize.RelTime(time.Now(), time.Now().Add(tokenLifetime), "", ""))
if err := module.emailing.SendHTML(
ctx,
user.Email.String(),
"Reset your SigNoz password",
emailtypes.TemplateNameResetPassword,
map[string]any{
"Name": user.DisplayName,
"Link": resetLink,
"Expiry": humanizedTokenLifetime,
},
); err != nil {
module.settings.Logger().ErrorContext(ctx, "failed to send reset password email", "error", err)
return nil
}
return nil
}
func (module *Module) UpdatePasswordByResetPasswordToken(ctx context.Context, token string, passwd string) error {
resetPasswordToken, err := module.store.GetResetPasswordToken(ctx, token)
if err != nil {
return err
}
if resetPasswordToken.IsExpired() {
return errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "reset password token has expired")
}
password, err := module.store.GetPassword(ctx, resetPasswordToken.PasswordID)
if err != nil {
return err
@@ -414,7 +478,7 @@ func (module *Module) CreateFirstUser(ctx context.Context, organization *types.O
}
if err = module.store.RunInTx(ctx, func(ctx context.Context) error {
err := module.orgSetter.Create(ctx, organization)
err = module.orgSetter.Create(ctx, organization)
if err != nil {
return err
}

View File

@@ -391,6 +391,18 @@ func (store *store) GetResetPasswordTokenByPasswordID(ctx context.Context, passw
return resetPasswordToken, nil
}
func (store *store) DeleteResetPasswordTokenByPasswordID(ctx context.Context, passwordID valuer.UUID) error {
_, err := store.sqlstore.BunDB().NewDelete().
Model(&types.ResetPasswordToken{}).
Where("password_id = ?", passwordID).
Exec(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to delete reset password token")
}
return nil
}
func (store *store) GetResetPasswordToken(ctx context.Context, token string) (*types.ResetPasswordToken, error) {
resetPasswordRequest := new(types.ResetPasswordToken)

View File

@@ -30,6 +30,9 @@ type Module interface {
// Updates password of user to the new password. It also deletes all reset password tokens for the user.
UpdatePassword(ctx context.Context, userID valuer.UUID, oldPassword string, password string) error
// Initiate forgot password flow for a user
ForgotPassword(ctx context.Context, orgID valuer.UUID, email valuer.Email, frontendBaseURL string) error
UpdateUser(ctx context.Context, orgID valuer.UUID, id string, user *types.User, updatedBy string) (*types.User, error)
DeleteUser(ctx context.Context, orgID valuer.UUID, id string, deletedBy string) error
@@ -92,6 +95,7 @@ type Handler interface {
GetResetPasswordToken(http.ResponseWriter, *http.Request)
ResetPassword(http.ResponseWriter, *http.Request)
ChangePassword(http.ResponseWriter, *http.Request)
ForgotPassword(http.ResponseWriter, *http.Request)
// API KEY
CreateAPIKey(http.ResponseWriter, *http.Request)

View File

@@ -36,29 +36,6 @@ func NewBucketCache(settings factory.ProviderSettings, cache cache.Cache, cacheT
}
}
// cachedBucket represents a cached time bucket
type cachedBucket struct {
StartMs uint64 `json:"startMs"`
EndMs uint64 `json:"endMs"`
Type qbtypes.RequestType `json:"type"`
Value json.RawMessage `json:"value"`
Stats qbtypes.ExecStats `json:"stats"`
}
// cachedData represents the full cached data for a query
type cachedData struct {
Buckets []*cachedBucket `json:"buckets"`
Warnings []string `json:"warnings"`
}
func (c *cachedData) UnmarshalBinary(data []byte) error {
return json.Unmarshal(data, c)
}
func (c *cachedData) MarshalBinary() ([]byte, error) {
return json.Marshal(c)
}
// GetMissRanges returns cached data and missing time ranges
func (bc *bucketCache) GetMissRanges(
ctx context.Context,
@@ -78,7 +55,7 @@ func (bc *bucketCache) GetMissRanges(
bc.logger.DebugContext(ctx, "cache key", "cache_key", cacheKey)
// Try to get cached data
var data cachedData
var data qbtypes.CachedData
err := bc.cache.Get(ctx, orgID, cacheKey, &data)
if err != nil {
if !errors.Ast(err, errors.TypeNotFound) {
@@ -147,9 +124,9 @@ func (bc *bucketCache) Put(ctx context.Context, orgID valuer.UUID, q qbtypes.Que
cacheKey := bc.generateCacheKey(q)
// Get existing cached data
var existingData cachedData
var existingData qbtypes.CachedData
if err := bc.cache.Get(ctx, orgID, cacheKey, &existingData); err != nil {
existingData = cachedData{}
existingData = qbtypes.CachedData{}
}
// Trim the result to exclude data within flux interval
@@ -203,7 +180,7 @@ func (bc *bucketCache) Put(ctx context.Context, orgID valuer.UUID, q qbtypes.Que
uniqueWarnings := bc.deduplicateWarnings(allWarnings)
// Create updated cached data
updatedData := cachedData{
updatedData := qbtypes.CachedData{
Buckets: mergedBuckets,
Warnings: uniqueWarnings,
}
@@ -222,7 +199,7 @@ func (bc *bucketCache) generateCacheKey(q qbtypes.Query) string {
}
// findMissingRangesWithStep identifies time ranges not covered by cached buckets with step alignment
func (bc *bucketCache) findMissingRangesWithStep(buckets []*cachedBucket, startMs, endMs uint64, stepMs uint64) []*qbtypes.TimeRange {
func (bc *bucketCache) findMissingRangesWithStep(buckets []*qbtypes.CachedBucket, startMs, endMs uint64, stepMs uint64) []*qbtypes.TimeRange {
// When step is 0 or window is too small to be cached, use simple algorithm
if stepMs == 0 || (startMs+stepMs) > endMs {
return bc.findMissingRangesBasic(buckets, startMs, endMs)
@@ -265,7 +242,7 @@ func (bc *bucketCache) findMissingRangesWithStep(buckets []*cachedBucket, startM
}
if needsSort {
slices.SortFunc(buckets, func(a, b *cachedBucket) int {
slices.SortFunc(buckets, func(a, b *qbtypes.CachedBucket) int {
if a.StartMs < b.StartMs {
return -1
}
@@ -339,7 +316,7 @@ func (bc *bucketCache) findMissingRangesWithStep(buckets []*cachedBucket, startM
}
// findMissingRangesBasic is the simple algorithm without step alignment
func (bc *bucketCache) findMissingRangesBasic(buckets []*cachedBucket, startMs, endMs uint64) []*qbtypes.TimeRange {
func (bc *bucketCache) findMissingRangesBasic(buckets []*qbtypes.CachedBucket, startMs, endMs uint64) []*qbtypes.TimeRange {
// Check if already sorted before sorting
needsSort := false
for i := 1; i < len(buckets); i++ {
@@ -350,7 +327,7 @@ func (bc *bucketCache) findMissingRangesBasic(buckets []*cachedBucket, startMs,
}
if needsSort {
slices.SortFunc(buckets, func(a, b *cachedBucket) int {
slices.SortFunc(buckets, func(a, b *qbtypes.CachedBucket) int {
if a.StartMs < b.StartMs {
return -1
}
@@ -421,9 +398,9 @@ func (bc *bucketCache) findMissingRangesBasic(buckets []*cachedBucket, startMs,
}
// filterRelevantBuckets returns buckets that overlap with the requested time range
func (bc *bucketCache) filterRelevantBuckets(buckets []*cachedBucket, startMs, endMs uint64) []*cachedBucket {
func (bc *bucketCache) filterRelevantBuckets(buckets []*qbtypes.CachedBucket, startMs, endMs uint64) []*qbtypes.CachedBucket {
// Pre-allocate with estimated capacity
relevant := make([]*cachedBucket, 0, len(buckets))
relevant := make([]*qbtypes.CachedBucket, 0, len(buckets))
for _, bucket := range buckets {
// Check if bucket overlaps with requested range
@@ -433,7 +410,7 @@ func (bc *bucketCache) filterRelevantBuckets(buckets []*cachedBucket, startMs, e
}
// Sort by start time
slices.SortFunc(relevant, func(a, b *cachedBucket) int {
slices.SortFunc(relevant, func(a, b *qbtypes.CachedBucket) int {
if a.StartMs < b.StartMs {
return -1
}
@@ -447,7 +424,7 @@ func (bc *bucketCache) filterRelevantBuckets(buckets []*cachedBucket, startMs, e
}
// mergeBuckets combines multiple cached buckets into a single result
func (bc *bucketCache) mergeBuckets(ctx context.Context, buckets []*cachedBucket, warnings []string) *qbtypes.Result {
func (bc *bucketCache) mergeBuckets(ctx context.Context, buckets []*qbtypes.CachedBucket, warnings []string) *qbtypes.Result {
if len(buckets) == 0 {
return &qbtypes.Result{}
}
@@ -480,7 +457,7 @@ func (bc *bucketCache) mergeBuckets(ctx context.Context, buckets []*cachedBucket
}
// mergeTimeSeriesValues merges time series data from multiple buckets
func (bc *bucketCache) mergeTimeSeriesValues(ctx context.Context, buckets []*cachedBucket) *qbtypes.TimeSeriesData {
func (bc *bucketCache) mergeTimeSeriesValues(ctx context.Context, buckets []*qbtypes.CachedBucket) *qbtypes.TimeSeriesData {
// Estimate capacity based on bucket count
estimatedSeries := len(buckets) * 10
@@ -631,7 +608,7 @@ func (bc *bucketCache) isEmptyResult(result *qbtypes.Result) (isEmpty bool, isFi
}
// resultToBuckets converts a query result into time-based buckets
func (bc *bucketCache) resultToBuckets(ctx context.Context, result *qbtypes.Result, startMs, endMs uint64) []*cachedBucket {
func (bc *bucketCache) resultToBuckets(ctx context.Context, result *qbtypes.Result, startMs, endMs uint64) []*qbtypes.CachedBucket {
// Check if result is empty
isEmpty, isFiltered := bc.isEmptyResult(result)
@@ -652,7 +629,7 @@ func (bc *bucketCache) resultToBuckets(ctx context.Context, result *qbtypes.Resu
// Always create a bucket, even for empty filtered results
// This ensures we don't re-query for data that doesn't exist
return []*cachedBucket{
return []*qbtypes.CachedBucket{
{
StartMs: startMs,
EndMs: endMs,
@@ -664,9 +641,9 @@ func (bc *bucketCache) resultToBuckets(ctx context.Context, result *qbtypes.Resu
}
// mergeAndDeduplicateBuckets combines and deduplicates bucket lists
func (bc *bucketCache) mergeAndDeduplicateBuckets(existing, fresh []*cachedBucket) []*cachedBucket {
func (bc *bucketCache) mergeAndDeduplicateBuckets(existing, fresh []*qbtypes.CachedBucket) []*qbtypes.CachedBucket {
// Create a map to deduplicate by time range
bucketMap := make(map[string]*cachedBucket)
bucketMap := make(map[string]*qbtypes.CachedBucket)
// Add existing buckets
for _, bucket := range existing {
@@ -681,13 +658,13 @@ func (bc *bucketCache) mergeAndDeduplicateBuckets(existing, fresh []*cachedBucke
}
// Convert back to slice with pre-allocated capacity
result := make([]*cachedBucket, 0, len(bucketMap))
result := make([]*qbtypes.CachedBucket, 0, len(bucketMap))
for _, bucket := range bucketMap {
result = append(result, bucket)
}
// Sort by start time
slices.SortFunc(result, func(a, b *cachedBucket) int {
slices.SortFunc(result, func(a, b *qbtypes.CachedBucket) int {
if a.StartMs < b.StartMs {
return -1
}

View File

@@ -147,14 +147,14 @@ func BenchmarkBucketCache_MergeTimeSeriesValues(b *testing.B) {
for _, tc := range testCases {
b.Run(tc.name, func(b *testing.B) {
// Create test buckets
buckets := make([]*cachedBucket, tc.numBuckets)
buckets := make([]*qbtypes.CachedBucket, tc.numBuckets)
for i := 0; i < tc.numBuckets; i++ {
startMs := uint64(i * 10000)
endMs := uint64((i + 1) * 10000)
result := createBenchmarkResultWithSeries(startMs, endMs, 1000, tc.numSeries, tc.numValues)
valueBytes, _ := json.Marshal(result.Value)
buckets[i] = &cachedBucket{
buckets[i] = &qbtypes.CachedBucket{
StartMs: startMs,
EndMs: endMs,
Type: qbtypes.RequestTypeTimeSeries,
@@ -417,8 +417,8 @@ func createBenchmarkResultWithSeries(startMs, endMs uint64, _ uint64, numSeries,
}
// Helper function to create buckets with specific gap patterns
func createBucketsWithPattern(numBuckets int, pattern string) []*cachedBucket {
buckets := make([]*cachedBucket, 0, numBuckets)
func createBucketsWithPattern(numBuckets int, pattern string) []*qbtypes.CachedBucket {
buckets := make([]*qbtypes.CachedBucket, 0, numBuckets)
for i := 0; i < numBuckets; i++ {
// Skip some buckets based on pattern
@@ -432,7 +432,7 @@ func createBucketsWithPattern(numBuckets int, pattern string) []*cachedBucket {
startMs := uint64(i * 10000)
endMs := uint64((i + 1) * 10000)
buckets = append(buckets, &cachedBucket{
buckets = append(buckets, &qbtypes.CachedBucket{
StartMs: startMs,
EndMs: endMs,
Type: qbtypes.RequestTypeTimeSeries,

View File

@@ -521,7 +521,7 @@ func TestBucketCache_FindMissingRanges_EdgeCases(t *testing.T) {
bc := NewBucketCache(instrumentationtest.New().ToProviderSettings(), memCache, cacheTTL, defaultFluxInterval).(*bucketCache)
// Test with buckets that have gaps and overlaps
buckets := []*cachedBucket{
buckets := []*qbtypes.CachedBucket{
{StartMs: 1000, EndMs: 2000},
{StartMs: 2500, EndMs: 3500},
{StartMs: 3000, EndMs: 4000}, // Overlaps with previous
@@ -1097,7 +1097,7 @@ func TestBucketCache_FindMissingRangesWithStep(t *testing.T) {
tests := []struct {
name string
buckets []*cachedBucket
buckets []*qbtypes.CachedBucket
startMs uint64
endMs uint64
stepMs uint64
@@ -1106,7 +1106,7 @@ func TestBucketCache_FindMissingRangesWithStep(t *testing.T) {
}{
{
name: "start_not_aligned_to_step",
buckets: []*cachedBucket{},
buckets: []*qbtypes.CachedBucket{},
startMs: 1500, // Not aligned to 1000ms step
endMs: 5000,
stepMs: 1000,
@@ -1118,7 +1118,7 @@ func TestBucketCache_FindMissingRangesWithStep(t *testing.T) {
},
{
name: "end_not_aligned_to_step",
buckets: []*cachedBucket{},
buckets: []*qbtypes.CachedBucket{},
startMs: 1000,
endMs: 4500, // Not aligned to 1000ms step
stepMs: 1000,
@@ -1129,7 +1129,7 @@ func TestBucketCache_FindMissingRangesWithStep(t *testing.T) {
},
{
name: "bucket_boundaries_not_aligned",
buckets: []*cachedBucket{
buckets: []*qbtypes.CachedBucket{
{StartMs: 1500, EndMs: 2500}, // Not aligned
},
startMs: 1000,
@@ -1143,7 +1143,7 @@ func TestBucketCache_FindMissingRangesWithStep(t *testing.T) {
},
{
name: "small_window_less_than_step",
buckets: []*cachedBucket{},
buckets: []*qbtypes.CachedBucket{},
startMs: 1000,
endMs: 1500, // Less than one step
stepMs: 1000,
@@ -1154,7 +1154,7 @@ func TestBucketCache_FindMissingRangesWithStep(t *testing.T) {
},
{
name: "zero_step_uses_basic_algorithm",
buckets: []*cachedBucket{},
buckets: []*qbtypes.CachedBucket{},
startMs: 1000,
endMs: 5000,
stepMs: 0,

View File

@@ -209,7 +209,7 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server,
r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap)
r.Use(middleware.NewComment().Wrap)
am := middleware.NewAuthZ(s.signoz.Instrumentation.Logger(), s.signoz.Modules.OrgGetter, s.signoz.Authz)
am := middleware.NewAuthZ(s.signoz.Instrumentation.Logger(), s.signoz.Modules.OrgGetter, s.signoz.Authz, s.signoz.Modules.RoleGetter)
api.RegisterRoutes(r, am)
api.RegisterLogsRoutes(r, am)

View File

@@ -22,6 +22,7 @@ import (
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/instrumentation"
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/ruler"
@@ -109,6 +110,9 @@ type Config struct {
// Flagger config
Flagger flagger.Config `mapstructure:"flagger"`
// User config
User user.Config `mapstructure:"user"`
}
// DeprecatedFlags are the flags that are deprecated and scheduled for removal.
@@ -171,6 +175,7 @@ func NewConfig(ctx context.Context, logger *slog.Logger, resolverConfig config.R
tokenizer.NewConfigFactory(),
metricsexplorer.NewConfigFactory(),
flagger.NewConfigFactory(),
user.NewConfigFactory(),
}
conf, err := config.New(ctx, resolverConfig, configFactories)

View File

@@ -17,6 +17,8 @@ import (
"github.com/SigNoz/signoz/pkg/modules/quickfilter/implquickfilter"
"github.com/SigNoz/signoz/pkg/modules/rawdataexport"
"github.com/SigNoz/signoz/pkg/modules/rawdataexport/implrawdataexport"
"github.com/SigNoz/signoz/pkg/modules/role"
"github.com/SigNoz/signoz/pkg/modules/role/implrole"
"github.com/SigNoz/signoz/pkg/modules/savedview"
"github.com/SigNoz/signoz/pkg/modules/savedview/implsavedview"
"github.com/SigNoz/signoz/pkg/modules/services"
@@ -41,6 +43,7 @@ type Handlers struct {
Global global.Handler
FlaggerHandler flagger.Handler
GatewayHandler gateway.Handler
Role role.Handler
}
func NewHandlers(modules Modules, providerSettings factory.ProviderSettings, querier querier.Querier, licensing licensing.Licensing, global global.Global, flaggerService flagger.Flagger, gatewayService gateway.Gateway) Handlers {
@@ -57,5 +60,6 @@ func NewHandlers(modules Modules, providerSettings factory.ProviderSettings, que
Global: signozglobal.NewHandler(global),
FlaggerHandler: flagger.NewHandler(flaggerService),
GatewayHandler: gateway.NewHandler(gatewayService),
Role: implrole.NewHandler(modules.RoleSetter, modules.RoleGetter),
}
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/SigNoz/signoz/pkg/factory/factorytest"
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
"github.com/SigNoz/signoz/pkg/modules/role/implrole"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/sharder"
"github.com/SigNoz/signoz/pkg/sharder/noopsharder"
@@ -40,7 +41,10 @@ func TestNewHandlers(t *testing.T) {
queryParser := queryparser.New(providerSettings)
require.NoError(t, err)
dashboardModule := impldashboard.NewModule(impldashboard.NewStore(sqlstore), providerSettings, nil, orgGetter, queryParser)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule)
roleSetter := implrole.NewSetter(implrole.NewStore(sqlstore), nil)
roleGetter := implrole.NewGetter(implrole.NewStore(sqlstore))
grantModule := implrole.NewGranter(implrole.NewStore(sqlstore), nil)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, roleSetter, roleGetter, grantModule)
handlers := NewHandlers(modules, providerSettings, nil, nil, nil, nil, nil)

View File

@@ -25,6 +25,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/quickfilter/implquickfilter"
"github.com/SigNoz/signoz/pkg/modules/rawdataexport"
"github.com/SigNoz/signoz/pkg/modules/rawdataexport/implrawdataexport"
"github.com/SigNoz/signoz/pkg/modules/role"
"github.com/SigNoz/signoz/pkg/modules/savedview"
"github.com/SigNoz/signoz/pkg/modules/savedview/implsavedview"
"github.com/SigNoz/signoz/pkg/modules/services"
@@ -66,6 +67,9 @@ type Modules struct {
SpanPercentile spanpercentile.Module
MetricsExplorer metricsexplorer.Module
Promote promote.Module
RoleSetter role.Setter
RoleGetter role.Getter
Granter role.Granter
}
func NewModules(
@@ -85,10 +89,13 @@ func NewModules(
queryParser queryparser.QueryParser,
config Config,
dashboard dashboard.Module,
roleSetter role.Setter,
roleGetter role.Getter,
granter role.Granter,
) Modules {
quickfilter := implquickfilter.NewModule(implquickfilter.NewStore(sqlstore))
orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter)
user := impluser.NewModule(impluser.NewStore(sqlstore, providerSettings), tokenizer, emailing, providerSettings, orgSetter, analytics)
user := impluser.NewModule(impluser.NewStore(sqlstore, providerSettings), tokenizer, emailing, providerSettings, orgSetter, granter, analytics, config.User)
userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings))
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
@@ -110,5 +117,8 @@ func NewModules(
Services: implservices.NewModule(querier, telemetryStore),
MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, providerSettings, config.MetricsExplorer),
Promote: implpromote.NewModule(telemetryMetadataStore, telemetryStore),
RoleSetter: roleSetter,
RoleGetter: roleGetter,
Granter: granter,
}
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/SigNoz/signoz/pkg/factory/factorytest"
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
"github.com/SigNoz/signoz/pkg/modules/role/implrole"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/sharder"
"github.com/SigNoz/signoz/pkg/sharder/noopsharder"
@@ -40,7 +41,10 @@ func TestNewModules(t *testing.T) {
queryParser := queryparser.New(providerSettings)
require.NoError(t, err)
dashboardModule := impldashboard.NewModule(impldashboard.NewStore(sqlstore), providerSettings, nil, orgGetter, queryParser)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule)
roleSetter := implrole.NewSetter(implrole.NewStore(sqlstore), nil)
roleGetter := implrole.NewGetter(implrole.NewStore(sqlstore))
grantModule := implrole.NewGranter(implrole.NewStore(sqlstore), nil)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, roleSetter, roleGetter, grantModule)
reflectVal := reflect.ValueOf(modules)
for i := 0; i < reflectVal.NumField(); i++ {

View File

@@ -19,6 +19,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/preference"
"github.com/SigNoz/signoz/pkg/modules/promote"
"github.com/SigNoz/signoz/pkg/modules/role"
"github.com/SigNoz/signoz/pkg/modules/session"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
@@ -49,6 +50,8 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
struct{ dashboard.Handler }{},
struct{ metricsexplorer.Handler }{},
struct{ gateway.Handler }{},
struct{ role.Getter }{},
struct{ role.Handler }{},
).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})
if err != nil {
return nil, err

View File

@@ -161,6 +161,7 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewUpdateUserPreferenceFactory(sqlstore, sqlschema),
sqlmigration.NewUpdateOrgPreferenceFactory(sqlstore, sqlschema),
sqlmigration.NewRenameOrgDomainsFactory(sqlstore, sqlschema),
sqlmigration.NewAddResetPasswordTokenExpiryFactory(sqlstore, sqlschema),
)
}
@@ -242,6 +243,8 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
handlers.Dashboard,
handlers.MetricsExplorer,
handlers.GatewayHandler,
modules.RoleGetter,
handlers.Role,
),
)
}

View File

@@ -90,8 +90,9 @@ func New(
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) factory.ProviderFactory[authz.AuthZ, authz.Config],
dashboardModuleCallback func(sqlstore.SQLStore, factory.ProviderSettings, analytics.Analytics, organization.Getter, role.Module, queryparser.QueryParser, querier.Querier, licensing.Licensing) dashboard.Module,
dashboardModuleCallback func(sqlstore.SQLStore, factory.ProviderSettings, analytics.Analytics, organization.Getter, role.Setter, role.Granter, queryparser.QueryParser, querier.Querier, licensing.Licensing) dashboard.Module,
gatewayProviderFactory func(licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config],
roleSetterCallback func(sqlstore.SQLStore, authz.AuthZ, licensing.Licensing, []role.RegisterTypeable) role.Setter,
) (*SigNoz, error) {
// Initialize instrumentation
instrumentation, err := instrumentation.New(ctx, config.Instrumentation, version.Info, "signoz")
@@ -280,6 +281,12 @@ func New(
return nil, err
}
// Initialize user getter
userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings))
// Initialize the role getter
roleGetter := implrole.NewGetter(implrole.NewStore(sqlstore))
// Initialize authz
authzProviderFactory := authzCallback(ctx, sqlstore)
authz, err := authzProviderFactory.New(ctx, providerSettings, authz.Config{})
@@ -287,9 +294,6 @@ func New(
return nil, err
}
// Initialize user getter
userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings))
// Initialize notification manager from the available notification manager provider factories
nfManager, err := factory.NewProviderFromNamedMap(
ctx,
@@ -386,9 +390,10 @@ func New(
}
// Initialize all modules
roleModule := implrole.NewModule(implrole.NewStore(sqlstore), authz, nil)
dashboardModule := dashboardModuleCallback(sqlstore, providerSettings, analytics, orgGetter, roleModule, queryParser, querier, licensing)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboardModule)
roleSetter := roleSetterCallback(sqlstore, authz, licensing, nil)
granter := implrole.NewGranter(implrole.NewStore(sqlstore), authz)
dashboard := dashboardModuleCallback(sqlstore, providerSettings, analytics, orgGetter, roleSetter, granter, queryParser, querier, licensing)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, roleSetter, roleGetter, granter)
// Initialize all handlers for the modules
handlers := NewHandlers(modules, providerSettings, querier, licensing, global, flagger, gateway)

View File

@@ -0,0 +1,83 @@
package sqlmigration
import (
"context"
"time"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type addResetPasswordTokenExpiry struct {
sqlstore sqlstore.SQLStore
sqlschema sqlschema.SQLSchema
}
func NewAddResetPasswordTokenExpiryFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("add_reset_password_token_expiry"), func(ctx context.Context, providerSettings factory.ProviderSettings, config Config) (SQLMigration, error) {
return newAddResetPasswordTokenExpiry(ctx, providerSettings, config, sqlstore, sqlschema)
})
}
func newAddResetPasswordTokenExpiry(_ context.Context, _ factory.ProviderSettings, _ Config, sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) (SQLMigration, error) {
return &addResetPasswordTokenExpiry{
sqlstore: sqlstore,
sqlschema: sqlschema,
}, nil
}
func (migration *addResetPasswordTokenExpiry) Register(migrations *migrate.Migrations) error {
if err := migrations.Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
func (migration *addResetPasswordTokenExpiry) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
// get the reset_password_token table
table, uniqueConstraints, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("reset_password_token"))
if err != nil {
return err
}
// add a new column `expires_at`
column := &sqlschema.Column{
Name: sqlschema.ColumnName("expires_at"),
DataType: sqlschema.DataTypeTimestamp,
Nullable: true,
}
// for existing rows set
defaultValueForExistingRows := time.Now()
sqls := migration.sqlschema.Operator().AddColumn(table, uniqueConstraints, column, defaultValueForExistingRows)
for _, sql := range sqls {
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
return err
}
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (migration *addResetPasswordTokenExpiry) Down(ctx context.Context, db *bun.DB) error {
return nil
}

View File

@@ -279,7 +279,7 @@ func (c *conditionBuilder) buildSpanScopeCondition(key *telemetrytypes.Telemetry
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "span scope field %s only supports '=' operator", key.Name)
}
isTrue := false
var isTrue bool
switch v := value.(type) {
case bool:
isTrue = v

View File

@@ -24,7 +24,7 @@ var (
typeRoleSelectorRegex = regexp.MustCompile(`^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$`)
typeAnonymousSelectorRegex = regexp.MustCompile(`^\*$`)
typeOrganizationSelectorRegex = regexp.MustCompile(`^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$`)
typeMetaResourceSelectorRegex = regexp.MustCompile(`^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$`)
typeMetaResourceSelectorRegex = regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`)
// metaresources selectors are used to select either all or none
typeMetaResourcesSelectorRegex = regexp.MustCompile(`^\*$`)
)

View File

@@ -12,12 +12,13 @@ import (
var (
// Templates is a list of all the templates that are supported by the emailing service.
// This list should be updated whenever a new template is added.
Templates = []TemplateName{TemplateNameInvitationEmail, TemplateNameUpdateRole}
Templates = []TemplateName{TemplateNameInvitationEmail, TemplateNameUpdateRole, TemplateNameResetPassword}
)
var (
TemplateNameInvitationEmail = TemplateName{valuer.NewString("invitation_email")}
TemplateNameUpdateRole = TemplateName{valuer.NewString("update_role")}
TemplateNameResetPassword = TemplateName{valuer.NewString("reset_password_email")}
)
type TemplateName struct{ valuer.String }
@@ -28,6 +29,8 @@ func NewTemplateName(name string) (TemplateName, error) {
return TemplateNameInvitationEmail, nil
case TemplateNameUpdateRole.StringValue():
return TemplateNameUpdateRole, nil
case TemplateNameResetPassword.StringValue():
return TemplateNameResetPassword, nil
default:
return TemplateName{}, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid template name: %s", name)
}

View File

@@ -35,12 +35,19 @@ type ChangePasswordRequest struct {
NewPassword string `json:"newPassword"`
}
type PostableForgotPassword struct {
OrgID valuer.UUID `json:"orgId"`
Email valuer.Email `json:"email"`
FrontendBaseURL string `json:"frontendBaseURL"`
}
type ResetPasswordToken struct {
bun.BaseModel `bun:"table:reset_password_token"`
Identifiable
Token string `bun:"token,type:text,notnull" json:"token"`
PasswordID valuer.UUID `bun:"password_id,type:text,notnull,unique" json:"passwordId"`
ExpiresAt time.Time `bun:"expires_at,type:timestamptz,nullzero" json:"expiresAt"`
}
type FactorPassword struct {
@@ -136,13 +143,14 @@ func NewHashedPassword(password string) (string, error) {
return string(hashedPassword), nil
}
func NewResetPasswordToken(passwordID valuer.UUID) (*ResetPasswordToken, error) {
func NewResetPasswordToken(passwordID valuer.UUID, expiresAt time.Time) (*ResetPasswordToken, error) {
return &ResetPasswordToken{
Identifiable: Identifiable{
ID: valuer.GenerateUUID(),
},
Token: valuer.GenerateUUID().String(),
PasswordID: passwordID,
ExpiresAt: expiresAt,
}, nil
}
@@ -208,3 +216,7 @@ func (f *FactorPassword) Equals(password string) bool {
func comparePassword(hashedPassword string, password string) bool {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) == nil
}
func (r *ResetPasswordToken) IsExpired() bool {
return r.ExpiresAt.Before(time.Now())
}

View File

@@ -553,6 +553,18 @@ func (f Function) Copy() Function {
return c
}
// Validate validates the name and args for the function
func (f Function) Validate() error {
if err := f.Name.Validate(); err != nil {
return err
}
// Validate args for function
if err := f.ValidateArgs(); err != nil {
return err
}
return nil
}
type LimitBy struct {
// keys to limit by
Keys []string `json:"keys"`

View File

@@ -1,6 +1,8 @@
package querybuildertypesv5
import (
"fmt"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
@@ -131,12 +133,44 @@ func (q *QueryBuilderQuery[T]) UnmarshalJSON(data []byte) error {
var temp Alias
// Use UnmarshalJSONWithContext for better error messages
if err := UnmarshalJSONWithContext(data, &temp, "query spec"); err != nil {
if err := UnmarshalJSONWithContext(data, &temp, fmt.Sprintf("query spec for %T", q)); err != nil {
return err
}
// Copy the decoded values back to the original struct
*q = QueryBuilderQuery[T](temp)
// Nomarlize the query after unmarshaling
q.Normalize()
return nil
}
// Normalize normalizes all the field keys in the query
func (q *QueryBuilderQuery[T]) Normalize() {
// normalize select fields
for idx := range q.SelectFields {
q.SelectFields[idx].Normalize()
}
// normalize group by fields
for idx := range q.GroupBy {
q.GroupBy[idx].Normalize()
}
// normalize order by fields
for idx := range q.Order {
q.Order[idx].Key.Normalize()
}
// normalize secondary aggregations
for idx := range q.SecondaryAggregations {
for jdx := range q.SecondaryAggregations[idx].Order {
q.SecondaryAggregations[idx].Order[jdx].Key.Normalize()
}
for jdx := range q.SecondaryAggregations[idx].GroupBy {
q.SecondaryAggregations[idx].GroupBy[jdx].Normalize()
}
}
}

View File

@@ -0,0 +1,653 @@
package querybuildertypesv5
import (
"encoding/json"
"testing"
"time"
"github.com/SigNoz/signoz/pkg/types/metrictypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestQueryBuilderQuery_Copy(t *testing.T) {
t.Run("copy with all fields populated", func(t *testing.T) {
original := QueryBuilderQuery[TraceAggregation]{
Name: "A",
StepInterval: Step{Duration: 60 * time.Second},
Signal: telemetrytypes.SignalTraces,
Source: telemetrytypes.SourceUnspecified,
Aggregations: []TraceAggregation{
{
Expression: "count()",
Alias: "trace_count",
},
},
Disabled: false,
Filter: &Filter{
Expression: "service.name = 'frontend'",
},
GroupBy: []GroupByKey{
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "service.name",
FieldContext: telemetrytypes.FieldContextResource,
},
},
},
Order: []OrderBy{
{
Key: OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "timestamp",
FieldContext: telemetrytypes.FieldContextSpan,
},
},
Direction: OrderDirectionDesc,
},
},
SelectFields: []telemetrytypes.TelemetryFieldKey{
{
Name: "trace_id",
FieldContext: telemetrytypes.FieldContextSpan,
},
},
Limit: 100,
Offset: 0,
Cursor: "cursor123",
LimitBy: &LimitBy{
Value: "10",
Keys: []string{
"service.name",
},
},
Having: &Having{
Expression: "count() > 100",
},
SecondaryAggregations: []SecondaryAggregation{
{
Limit: 10,
Order: []OrderBy{
{
Key: OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "value",
FieldContext: telemetrytypes.FieldContextSpan,
},
},
Direction: OrderDirectionAsc,
},
},
GroupBy: []GroupByKey{
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "region",
FieldContext: telemetrytypes.FieldContextResource,
},
},
},
},
},
Functions: []Function{
{
Name: FunctionNameTimeShift,
Args: []FunctionArg{
{
Name: "shift",
Value: "1h",
},
},
},
},
Legend: "{{service.name}}",
ShiftBy: 3600000,
}
// Create a copy
copied := original.Copy()
// Assert that values are equal
assert.Equal(t, original.Name, copied.Name)
assert.Equal(t, original.StepInterval, copied.StepInterval)
assert.Equal(t, original.Signal, copied.Signal)
assert.Equal(t, original.Source, copied.Source)
assert.Equal(t, original.Disabled, copied.Disabled)
assert.Equal(t, original.Limit, copied.Limit)
assert.Equal(t, original.Offset, copied.Offset)
assert.Equal(t, original.Cursor, copied.Cursor)
assert.Equal(t, original.Legend, copied.Legend)
assert.Equal(t, original.ShiftBy, copied.ShiftBy)
// Assert deep copies for slices and pointers
require.NotNil(t, copied.Aggregations)
assert.Equal(t, len(original.Aggregations), len(copied.Aggregations))
assert.Equal(t, original.Aggregations[0].Expression, copied.Aggregations[0].Expression)
require.NotNil(t, copied.Filter)
assert.Equal(t, original.Filter.Expression, copied.Filter.Expression)
require.NotNil(t, copied.GroupBy)
assert.Equal(t, len(original.GroupBy), len(copied.GroupBy))
assert.Equal(t, original.GroupBy[0].Name, copied.GroupBy[0].Name)
require.NotNil(t, copied.Order)
assert.Equal(t, len(original.Order), len(copied.Order))
assert.Equal(t, original.Order[0].Key.Name, copied.Order[0].Key.Name)
require.NotNil(t, copied.SelectFields)
assert.Equal(t, len(original.SelectFields), len(copied.SelectFields))
assert.Equal(t, original.SelectFields[0].Name, copied.SelectFields[0].Name)
require.NotNil(t, copied.LimitBy)
assert.Equal(t, original.LimitBy.Value, copied.LimitBy.Value)
assert.Equal(t, len(original.LimitBy.Keys), len(copied.LimitBy.Keys))
require.NotNil(t, copied.Having)
assert.Equal(t, original.Having.Expression, copied.Having.Expression)
require.NotNil(t, copied.SecondaryAggregations)
assert.Equal(t, len(original.SecondaryAggregations), len(copied.SecondaryAggregations))
assert.Equal(t, original.SecondaryAggregations[0].Limit, copied.SecondaryAggregations[0].Limit)
require.NotNil(t, copied.Functions)
assert.Equal(t, len(original.Functions), len(copied.Functions))
assert.Equal(t, original.Functions[0].Name, copied.Functions[0].Name)
// Verify independence - modify copied and ensure original is unchanged
copied.Name = "B"
assert.Equal(t, "A", original.Name)
copied.Aggregations[0].Expression = "sum()"
assert.Equal(t, "count()", original.Aggregations[0].Expression)
copied.Filter.Expression = "modified"
assert.Equal(t, "service.name = 'frontend'", original.Filter.Expression)
copied.GroupBy[0].Name = "modified"
assert.Equal(t, "service.name", original.GroupBy[0].Name)
copied.Order[0].Key.Name = "modified"
assert.Equal(t, "timestamp", original.Order[0].Key.Name)
copied.SelectFields[0].Name = "modified"
assert.Equal(t, "trace_id", original.SelectFields[0].Name)
copied.LimitBy.Value = "999"
assert.Equal(t, "10", original.LimitBy.Value)
copied.Having.Expression = "modified"
assert.Equal(t, "count() > 100", original.Having.Expression)
copied.SecondaryAggregations[0].Limit = 999
assert.Equal(t, 10, original.SecondaryAggregations[0].Limit)
copied.Functions[0].Name = FunctionNameAbsolute
assert.Equal(t, FunctionNameTimeShift, original.Functions[0].Name)
})
t.Run("copy with nil fields", func(t *testing.T) {
original := QueryBuilderQuery[TraceAggregation]{
Name: "A",
Signal: telemetrytypes.SignalTraces,
}
copied := original.Copy()
assert.Equal(t, original.Name, copied.Name)
assert.Equal(t, original.Signal, copied.Signal)
assert.Nil(t, copied.Aggregations)
assert.Nil(t, copied.Filter)
assert.Nil(t, copied.GroupBy)
assert.Nil(t, copied.Order)
assert.Nil(t, copied.SelectFields)
assert.Nil(t, copied.LimitBy)
assert.Nil(t, copied.Having)
assert.Nil(t, copied.SecondaryAggregations)
assert.Nil(t, copied.Functions)
})
t.Run("copy metric aggregation query", func(t *testing.T) {
original := QueryBuilderQuery[MetricAggregation]{
Name: "M",
Signal: telemetrytypes.SignalMetrics,
Aggregations: []MetricAggregation{
{
MetricName: "cpu_usage",
SpaceAggregation: metrictypes.SpaceAggregationAvg,
TimeAggregation: metrictypes.TimeAggregationAvg,
},
},
}
copied := original.Copy()
assert.Equal(t, original.Name, copied.Name)
assert.Equal(t, original.Signal, copied.Signal)
require.NotNil(t, copied.Aggregations)
assert.Equal(t, original.Aggregations[0].MetricName, copied.Aggregations[0].MetricName)
assert.Equal(t, original.Aggregations[0].SpaceAggregation, copied.Aggregations[0].SpaceAggregation)
// Verify independence
copied.Aggregations[0].MetricName = "modified"
assert.Equal(t, "cpu_usage", original.Aggregations[0].MetricName)
})
t.Run("copy log aggregation query", func(t *testing.T) {
original := QueryBuilderQuery[LogAggregation]{
Name: "L",
Signal: telemetrytypes.SignalLogs,
Aggregations: []LogAggregation{
{
Expression: "count()",
Alias: "log_count",
},
},
}
copied := original.Copy()
assert.Equal(t, original.Name, copied.Name)
assert.Equal(t, original.Signal, copied.Signal)
require.NotNil(t, copied.Aggregations)
assert.Equal(t, original.Aggregations[0].Expression, copied.Aggregations[0].Expression)
// Verify independence
copied.Aggregations[0].Expression = "sum()"
assert.Equal(t, "count()", original.Aggregations[0].Expression)
})
}
func TestQueryBuilderQuery_Normalize(t *testing.T) {
t.Run("normalize select fields", func(t *testing.T) {
query := QueryBuilderQuery[TraceAggregation]{
SelectFields: []telemetrytypes.TelemetryFieldKey{
{
Name: "service.name",
FieldContext: telemetrytypes.FieldContextResource,
},
{
Name: "span.name",
FieldContext: telemetrytypes.FieldContextSpan,
},
},
}
query.Normalize()
// Normalize only changes FieldContext, not the Name
assert.Equal(t, "service.name", query.SelectFields[0].Name)
assert.Equal(t, telemetrytypes.FieldContextResource, query.SelectFields[0].FieldContext)
assert.Equal(t, "span.name", query.SelectFields[1].Name)
assert.Equal(t, telemetrytypes.FieldContextSpan, query.SelectFields[1].FieldContext)
})
t.Run("normalize group by fields", func(t *testing.T) {
query := QueryBuilderQuery[TraceAggregation]{
GroupBy: []GroupByKey{
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "service.name",
FieldContext: telemetrytypes.FieldContextResource,
},
},
},
}
query.Normalize()
assert.Equal(t, "service.name", query.GroupBy[0].Name)
assert.Equal(t, telemetrytypes.FieldContextResource, query.GroupBy[0].FieldContext)
})
t.Run("normalize order by fields", func(t *testing.T) {
query := QueryBuilderQuery[TraceAggregation]{
Order: []OrderBy{
{
Key: OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "timestamp",
FieldContext: telemetrytypes.FieldContextSpan,
},
},
Direction: OrderDirectionDesc,
},
},
}
query.Normalize()
assert.Equal(t, "timestamp", query.Order[0].Key.Name)
assert.Equal(t, telemetrytypes.FieldContextSpan, query.Order[0].Key.FieldContext)
})
t.Run("normalize secondary aggregations", func(t *testing.T) {
query := QueryBuilderQuery[TraceAggregation]{
SecondaryAggregations: []SecondaryAggregation{
{
Order: []OrderBy{
{
Key: OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "value",
FieldContext: telemetrytypes.FieldContextSpan,
},
},
Direction: OrderDirectionAsc,
},
},
GroupBy: []GroupByKey{
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "region",
FieldContext: telemetrytypes.FieldContextResource,
},
},
},
},
},
}
query.Normalize()
assert.Equal(t, "value", query.SecondaryAggregations[0].Order[0].Key.Name)
assert.Equal(t, telemetrytypes.FieldContextSpan, query.SecondaryAggregations[0].Order[0].Key.FieldContext)
assert.Equal(t, "region", query.SecondaryAggregations[0].GroupBy[0].Name)
assert.Equal(t, telemetrytypes.FieldContextResource, query.SecondaryAggregations[0].GroupBy[0].FieldContext)
})
t.Run("normalize with nil fields", func(t *testing.T) {
query := QueryBuilderQuery[TraceAggregation]{
Name: "A",
Signal: telemetrytypes.SignalTraces,
}
// Should not panic
query.Normalize()
assert.Equal(t, "A", query.Name)
})
t.Run("normalize all fields together", func(t *testing.T) {
query := QueryBuilderQuery[TraceAggregation]{
SelectFields: []telemetrytypes.TelemetryFieldKey{
{Name: "service.name", FieldContext: telemetrytypes.FieldContextResource},
},
GroupBy: []GroupByKey{
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "host.name",
FieldContext: telemetrytypes.FieldContextResource,
},
},
},
Order: []OrderBy{
{
Key: OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "duration",
FieldContext: telemetrytypes.FieldContextSpan,
},
},
},
},
SecondaryAggregations: []SecondaryAggregation{
{
Order: []OrderBy{
{
Key: OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "count",
FieldContext: telemetrytypes.FieldContextSpan,
},
},
},
},
GroupBy: []GroupByKey{
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "status.code",
FieldContext: telemetrytypes.FieldContextSpan,
},
},
},
},
},
}
query.Normalize()
assert.Equal(t, "service.name", query.SelectFields[0].Name)
assert.Equal(t, "host.name", query.GroupBy[0].Name)
assert.Equal(t, "duration", query.Order[0].Key.Name)
assert.Equal(t, "count", query.SecondaryAggregations[0].Order[0].Key.Name)
assert.Equal(t, "status.code", query.SecondaryAggregations[0].GroupBy[0].Name)
})
}
func TestQueryBuilderQuery_UnmarshalJSON(t *testing.T) {
t.Run("valid trace query", func(t *testing.T) {
jsonData := `{
"name": "A",
"signal": "traces",
"stepInterval": 60,
"aggregations": [{
"expression": "count()",
"alias": "trace_count"
}],
"filter": {
"expression": "service.name = 'frontend'"
},
"groupBy": [{
"name": "service.name",
"fieldContext": "resource"
}],
"order": [{
"key": {
"name": "service.name",
"fieldContext": "resource"
},
"direction": "desc"
}],
"limit": 100
}`
var query QueryBuilderQuery[TraceAggregation]
err := json.Unmarshal([]byte(jsonData), &query)
require.NoError(t, err)
assert.Equal(t, "A", query.Name)
assert.Equal(t, telemetrytypes.SignalTraces, query.Signal)
assert.Equal(t, int64(60000), query.StepInterval.Milliseconds())
assert.Equal(t, 1, len(query.Aggregations))
assert.Equal(t, "count()", query.Aggregations[0].Expression)
assert.Equal(t, "trace_count", query.Aggregations[0].Alias)
require.NotNil(t, query.Filter)
assert.Equal(t, "service.name = 'frontend'", query.Filter.Expression)
assert.Equal(t, 1, len(query.GroupBy))
assert.Equal(t, "service.name", query.GroupBy[0].Name)
assert.Equal(t, telemetrytypes.FieldContextResource, query.GroupBy[0].FieldContext)
assert.Equal(t, 1, len(query.Order))
assert.Equal(t, "service.name", query.Order[0].Key.Name)
assert.Equal(t, telemetrytypes.FieldContextResource, query.Order[0].Key.FieldContext)
assert.Equal(t, OrderDirectionDesc, query.Order[0].Direction)
assert.Equal(t, 100, query.Limit)
})
t.Run("valid metric query", func(t *testing.T) {
jsonData := `{
"name": "M",
"signal": "metrics",
"stepInterval": "1m",
"aggregations": [{
"metricName": "cpu_usage",
"spaceAggregation": "avg",
"timeAggregation": "avg"
}]
}`
var query QueryBuilderQuery[MetricAggregation]
err := json.Unmarshal([]byte(jsonData), &query)
require.NoError(t, err)
assert.Equal(t, "M", query.Name)
assert.Equal(t, telemetrytypes.SignalMetrics, query.Signal)
assert.Equal(t, int64(60000), query.StepInterval.Milliseconds())
assert.Equal(t, 1, len(query.Aggregations))
assert.Equal(t, "cpu_usage", query.Aggregations[0].MetricName)
})
t.Run("valid log query", func(t *testing.T) {
jsonData := `{
"name": "L",
"signal": "logs",
"aggregations": [{
"expression": "count()",
"alias": "log_count"
}]
}`
var query QueryBuilderQuery[LogAggregation]
err := json.Unmarshal([]byte(jsonData), &query)
require.NoError(t, err)
assert.Equal(t, "L", query.Name)
assert.Equal(t, telemetrytypes.SignalLogs, query.Signal)
assert.Equal(t, 1, len(query.Aggregations))
assert.Equal(t, "count()", query.Aggregations[0].Expression)
})
t.Run("unknown field error", func(t *testing.T) {
jsonData := `{
"name": "A",
"signal": "traces",
"unknownField": "value"
}`
var query QueryBuilderQuery[TraceAggregation]
err := json.Unmarshal([]byte(jsonData), &query)
assert.Error(t, err)
assert.Contains(t, err.Error(), "unknown field")
})
t.Run("query with all optional fields", func(t *testing.T) {
// NOTE: This json payload is not realistic, just for testing all fields
jsonData := `{
"name": "A",
"signal": "traces",
"stepInterval": "5m",
"source": "traces",
"aggregations": [{
"expression": "count()",
"alias": "span.count"
}],
"disabled": true,
"filter": {
"expression": "service.name = 'api'"
},
"groupBy": [{
"name": "service.name",
"fieldContext": "resource"
}],
"order": [{
"key": {
"name": "timestamp",
"fieldContext": "span"
},
"direction": "asc"
}],
"selectFields": [{
"name": "trace_id",
"fieldContext": "span"
}],
"limit": 50,
"offset": 10,
"cursor": "cursor123",
"limitBy": {
"value": "5",
"keys": ["service.name"]
},
"having": {
"expression": "count() > 10"
},
"secondaryAggregations": [{
"limit": 20,
"order": [{
"key": {
"name": "value",
"fieldContext": "span"
},
"direction": "desc"
}],
"groupBy": [{
"name": "region",
"fieldContext": "resource"
}]
}],
"functions": [{
"name": "timeShift",
"args": [{
"name": "shift",
"value": "1h"
}]
}],
"legend": "{{service.name}}"
}`
var query QueryBuilderQuery[TraceAggregation]
err := json.Unmarshal([]byte(jsonData), &query)
require.NoError(t, err)
assert.Equal(t, "A", query.Name)
assert.Equal(t, telemetrytypes.SignalTraces, query.Signal)
assert.Equal(t, int64(300000), query.StepInterval.Milliseconds())
// Source is set in the JSON, so it should be "traces", not SourceUnspecified
assert.Equal(t, "traces", query.Source.String.StringValue())
assert.True(t, query.Disabled)
assert.Equal(t, query.Aggregations[0].Expression, "count()")
assert.Equal(t, query.Aggregations[0].Alias, "span.count")
assert.NotNil(t, query.Filter)
assert.NotNil(t, query.GroupBy)
assert.NotNil(t, query.Order)
assert.NotNil(t, query.SelectFields)
assert.Equal(t, 50, query.Limit)
assert.Equal(t, 10, query.Offset)
assert.Equal(t, "cursor123", query.Cursor)
assert.NotNil(t, query.LimitBy)
assert.NotNil(t, query.Having)
assert.NotNil(t, query.SecondaryAggregations)
assert.NotNil(t, query.Functions)
assert.Equal(t, "{{service.name}}", query.Legend)
})
t.Run("normalization happens during unmarshaling", func(t *testing.T) {
jsonData := `{
"name": "A",
"signal": "traces",
"selectFields": [{
"name": "resource.service.name"
}],
"groupBy": [{
"name": "resource.host.name"
}],
"order": [{
"key": {
"name": "span.duration"
},
"direction": "desc"
}]
}`
var query QueryBuilderQuery[TraceAggregation]
err := json.Unmarshal([]byte(jsonData), &query)
require.NoError(t, err)
// FieldContext should be normalized, Name should remain as-is
assert.Equal(t, "service.name", query.SelectFields[0].Name)
assert.Equal(t, telemetrytypes.FieldContextResource, query.SelectFields[0].FieldContext)
assert.Equal(t, "host.name", query.GroupBy[0].Name)
assert.Equal(t, telemetrytypes.FieldContextResource, query.GroupBy[0].FieldContext)
assert.Equal(t, "duration", query.Order[0].Key.Name)
assert.Equal(t, telemetrytypes.FieldContextSpan, query.Order[0].Key.FieldContext)
})
}

View File

@@ -0,0 +1,61 @@
package querybuildertypesv5
import (
"bytes"
"encoding/json"
"maps"
"github.com/SigNoz/signoz/pkg/types/cachetypes"
)
var _ cachetypes.Cacheable = (*CachedData)(nil)
type CachedBucket struct {
StartMs uint64 `json:"startMs"`
EndMs uint64 `json:"endMs"`
Type RequestType `json:"type"`
Value json.RawMessage `json:"value"`
Stats ExecStats `json:"stats"`
}
func (c *CachedBucket) Clone() *CachedBucket {
return &CachedBucket{
StartMs: c.StartMs,
EndMs: c.EndMs,
Type: c.Type,
Value: bytes.Clone(c.Value),
Stats: ExecStats{
RowsScanned: c.Stats.RowsScanned,
BytesScanned: c.Stats.BytesScanned,
DurationMS: c.Stats.DurationMS,
StepIntervals: maps.Clone(c.Stats.StepIntervals),
},
}
}
// CachedData represents the full cached data for a query
type CachedData struct {
Buckets []*CachedBucket `json:"buckets"`
Warnings []string `json:"warnings"`
}
func (c *CachedData) UnmarshalBinary(data []byte) error {
return json.Unmarshal(data, c)
}
func (c *CachedData) MarshalBinary() ([]byte, error) {
return json.Marshal(c)
}
func (c *CachedData) Clone() cachetypes.Cacheable {
clonedCachedData := new(CachedData)
clonedCachedData.Buckets = make([]*CachedBucket, len(c.Buckets))
for i := range c.Buckets {
clonedCachedData.Buckets[i] = c.Buckets[i].Clone()
}
clonedCachedData.Warnings = make([]string, len(c.Warnings))
copy(clonedCachedData.Warnings, c.Warnings)
return clonedCachedData
}

View File

@@ -0,0 +1,87 @@
package querybuildertypesv5
import (
"encoding/json"
"testing"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/stretchr/testify/assert"
)
func createBuckets_TimeSeries(numBuckets int) []*CachedBucket {
buckets := make([]*CachedBucket, numBuckets)
for i := 0; i < numBuckets; i++ {
startMs := uint64(i * 10000)
endMs := uint64((i + 1) * 10000)
timeSeriesData := &TimeSeriesData{
QueryName: "A",
Aggregations: []*AggregationBucket{
{
Index: 0,
Series: []*TimeSeries{
{
Labels: []*Label{
{Key: telemetrytypes.TelemetryFieldKey{Name: "service"}, Value: "test"},
},
Values: []*TimeSeriesValue{
{Timestamp: 1672563720000, Value: 1, Partial: true}, // 12:02
{Timestamp: 1672563900000, Value: 2}, // 12:05
{Timestamp: 1672564200000, Value: 2.5}, // 12:10
{Timestamp: 1672564500000, Value: 2.6}, // 12:15
{Timestamp: 1672566600000, Value: 2.9}, // 12:50
{Timestamp: 1672566900000, Value: 3}, // 12:55
{Timestamp: 1672567080000, Value: 4, Partial: true}, // 12:58
},
},
},
},
},
}
value, err := json.Marshal(timeSeriesData)
if err != nil {
panic(err)
}
buckets[i] = &CachedBucket{
StartMs: startMs,
EndMs: endMs,
Type: RequestTypeTimeSeries,
Value: json.RawMessage(value),
Stats: ExecStats{
RowsScanned: uint64(i * 500),
BytesScanned: uint64(i * 10000),
DurationMS: uint64(i * 1000),
},
}
}
return buckets
}
func BenchmarkCachedData_JSONMarshal_10kbuckets(b *testing.B) {
buckets := createBuckets_TimeSeries(10000)
data := &CachedData{Buckets: buckets}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := json.Marshal(data)
assert.NoError(b, err)
}
}
func BenchmarkCachedData_Clone_10kbuckets(b *testing.B) {
buckets := createBuckets_TimeSeries(10000)
data := &CachedData{Buckets: buckets}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = data.Clone()
}
}

View File

@@ -73,6 +73,43 @@ func (f *QueryBuilderFormula) UnmarshalJSON(data []byte) error {
return nil
}
// Validate checks if the QueryBuilderFormula fields are valid
func (f QueryBuilderFormula) Validate() error {
// Validate name is not blank
if strings.TrimSpace(f.Name) == "" {
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"formula name cannot be blank",
)
}
// Validate expression is not blank
if strings.TrimSpace(f.Expression) == "" {
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"formula expression cannot be blank",
)
}
// Validate functions if present
for i, fn := range f.Functions {
if err := fn.Validate(); err != nil {
fnId := fmt.Sprintf("function #%d", i+1)
if f.Name != "" {
fnId = fmt.Sprintf("function #%d in formula '%s'", i+1, f.Name)
}
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"invalid %s: %s",
fnId,
err.Error(),
)
}
}
return nil
}
// small container to store the query name and index or alias reference
// for a variable in the formula expression
// read below for more details on aggregation references

View File

@@ -1,10 +1,13 @@
package querybuildertypesv5
import (
"fmt"
"math"
"slices"
"strconv"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -33,6 +36,46 @@ var (
FunctionNameFillZero = FunctionName{valuer.NewString("fillZero")}
)
// Validate checks if the FunctionName is valid and one of the known types
func (fn FunctionName) Validate() error {
validFunctions := []FunctionName{
FunctionNameCutOffMin,
FunctionNameCutOffMax,
FunctionNameClampMin,
FunctionNameClampMax,
FunctionNameAbsolute,
FunctionNameRunningDiff,
FunctionNameLog2,
FunctionNameLog10,
FunctionNameCumulativeSum,
FunctionNameEWMA3,
FunctionNameEWMA5,
FunctionNameEWMA7,
FunctionNameMedian3,
FunctionNameMedian5,
FunctionNameMedian7,
FunctionNameTimeShift,
FunctionNameAnomaly,
FunctionNameFillZero,
}
if slices.Contains(validFunctions, fn) {
return nil
}
// Format valid functions as comma-separated string
var validFunctionNames []string
for _, fn := range validFunctions {
validFunctionNames = append(validFunctionNames, fn.StringValue())
}
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"invalid function name: %s",
fn.StringValue(),
).WithAdditional(fmt.Sprintf("valid functions are: %s", strings.Join(validFunctionNames, ", ")))
}
// ApplyFunction applies the given function to the result data
func ApplyFunction(fn Function, result *TimeSeries) *TimeSeries {
// Extract the function name and arguments
@@ -112,6 +155,61 @@ func ApplyFunction(fn Function, result *TimeSeries) *TimeSeries {
return result
}
// ValidateArgs validates the arguments for the given function
func (fn Function) ValidateArgs() error {
// Extract the function name and arguments
name := fn.Name
args := fn.Args
switch name {
case FunctionNameCutOffMin, FunctionNameCutOffMax, FunctionNameClampMin, FunctionNameClampMax:
if len(args) == 0 {
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"threshold value is required for function %s",
name.StringValue(),
)
}
_, err := parseFloat64Arg(args[0].Value)
if err != nil {
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"threshold value must be a floating value for function %s",
name.StringValue(),
)
}
case FunctionNameEWMA3, FunctionNameEWMA5, FunctionNameEWMA7:
if len(args) == 0 {
return nil // alpha is optional for EWMA functions
}
_, err := parseFloat64Arg(args[0].Value)
if err != nil {
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"alpha value must be a floating value for function %s",
name.StringValue(),
)
}
case FunctionNameTimeShift:
if len(args) == 0 {
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"time shift value is required for function %s",
name.StringValue(),
)
}
_, err := parseFloat64Arg(args[0].Value)
if err != nil {
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"time shift value must be a floating value for function %s",
name.StringValue(),
)
}
}
return nil
}
// parseFloat64Arg parses an argument to float64
func parseFloat64Arg(value any) (float64, error) {
switch v := value.(type) {

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