Compare commits

...

18 Commits

Author SHA1 Message Date
vikrantgupta25
f95a8ec4f6 feat(authz): better naming for authz service and authz middleware 2026-05-08 18:59:37 +05:30
Vikrant Gupta
ddf81dff1f Merge branch 'main' into platform-pod/issues/2036 2026-05-08 18:59:06 +05:30
vikrantgupta25
b0f1b2b192 feat(authz): better naming for authz service and authz middleware 2026-05-08 18:52:48 +05:30
vikrantgupta25
9f743cfba8 feat(authz): move selectors to handler 2026-05-08 18:47:22 +05:30
Tushar Vats
504f6a4d04 fix: disallow group by timestamp for timeseries request (#11018)
* fix: disallow group by timestamp for timeseries request

* fix: added a unit test

* fix: use WithTimestampGroupByValidation instead

* fix: unexport the field

* fix: update doc string
2026-05-08 12:45:58 +00:00
Pandey
9e118ded1f feat(ruletypes): publish OpenAPI 3 discriminator on RuleThresholdData and EvaluationEnvelope (#11180)
* feat(ruletypes): publish OpenAPI 3 discriminator on RuleThresholdData and EvaluationEnvelope

Both types model `{kind, spec}` discriminated unions on the wire but
the generated OpenAPI lacked the `discriminator:` keyword, so
codegen tools (oapi-codegen, terraform-plugin-codegen-openapi) fell
back to opaque `Spec: any` and consumers had to hand-write
JSON-bridges instead of typed Expand/Flatten.

Add the discriminator declaration via a marker convention:

- Each parent type implements `jsonschema.Preparer` to set an
  `x-signoz-discriminator` extra property carrying `propertyName` and
  the per-kind `mapping`.
- A small `attachDiscriminators` pass in pkg/signoz/openapi.go runs
  after spec reflection, walks every component schema, promotes the
  marker into a real openapi3.Discriminator, and removes the marker
  so it doesn't leak into the rendered YAML.

The two-step is required because jsonschema-go.Schema has no
Discriminator field of its own and openapi-go only carries through
`x-`-prefixed extras unchanged. The wire shape is unchanged —
`{kind: "<value>", spec: <variant>}` is still what's sent and
received.

Adding a new variant: append to JSONSchemaOneOf and add a
mapping entry on PrepareJSONSchema.

* refactor(openapi): inline discriminator constant and tighten attachDiscriminators

* chore(ruletypes): trim discriminator comments

* fix(ruletypes): mark evaluation variant kind/spec required

* fix(ruletypes): keep envelope kind/spec out of the parent schema via json:"-"

* refactor(ruletypes): strip envelope parent properties in attachDiscriminators

Revert the json:"-" + custom MarshalJSON dance on the envelope
structs. Restore the original tags (json:"kind" / json:"spec"),
keep the discriminator marker, and clear the parent's redundant
properties / required block in attachDiscriminators after the
discriminator is promoted.

* style(openapi): add blank lines between logical blocks in attachDiscriminators

* style(ruletypes): add blank lines in PrepareJSONSchema

* fix(ruletypes): mark threshold variant kind/spec required
2026-05-08 12:26:22 +00:00
Naman Verma
89d394145d feat: replicate perses structs but with proper plugin references (#11031)
* feat: openapi spec generation

* test: script to generate test dashboard data in a sql db

* test: fixes in dashboard perf testing data generator

* test: perf test script for both sql flavours

* test: data column in perf tests should match real data

* test: much bigger json for data column

* chore: comment clean up

* chore: separate file for perses replicas

* test: more descriptive test file name

* chore: move plugin maps to correct file

* chore: comment cleanup

* test: add tests for spec wrappers

* chore: better file names

* chore: better file name

* chore: too many comments

* fix: js lint errors

* fix: dot at the end of a comment

* chore: better error messages

* fix: strict decode variable spec as well

* fix: remove textbox plugin from openapi spec

* chore: renames and code rearrangement

* chore: better comment to explain what restrictKindToLiteral does

* chore: cleaner comment

* chore: cleaner comment

* chore: cleaner comment

* chore: better method name

* chore: cleanup testing code

* chore: code movement

* chore: code movement

* chore: code movement

* chore: go lint fix (godot)

* chore: code movement

* chore: cleanup comments

* chore: better method name extractKindAndSpec

* test: test for drift detection mechanics

* chore: follow proper unmarshal json method structure

* chore: separate method for validation

* fix: remove extra spec from builder query marshalling

* fix: add allowed values in err messages

* fix: remove extra (un)marshal cycle

* fix: return 500 err if spec is nil for composite kind w/ code comment

* fix: no need for copying textboxvariablespec

* fix: wrap errors

* chore: no v2 subpackage

* fix: query-less panels not allowed

* fix: allow only 1 query in a panel

* test: unit test fixes
2026-05-08 12:12:32 +00:00
Yunus M
e99ac3dd76 fix: filter out warning and info level events (#11216)
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-05-08 09:10:06 +00:00
vikrantgupta25
7a94ee4986 feat(authz): revert role details changes 2026-05-06 22:54:55 +05:30
vikrantgupta25
5f10007c3e test(integration): fix test lint and remove contributing guide 2026-05-06 17:20:37 +05:30
vikrantgupta25
7e4b4a73e0 test(integration): add integration tests 2026-05-06 17:03:08 +05:30
vikrantgupta25
9cd7ccbde0 fix(types): move types to middleware to remove http import from types 2026-05-06 15:39:47 +05:30
vikrantgupta25
ad1521bfe8 fix(openapi): openapi changes for attach 2026-05-06 15:24:48 +05:30
vikrantgupta25
423c924314 fix(openapi): openapi changes for attach 2026-05-06 15:23:32 +05:30
vikrantgupta25
bb0aa813c0 feat(authz): role details page fixes 2026-05-06 14:24:14 +05:30
vikrantgupta25
cf6d34e2df feat(authz): add attach permissions to migration 2026-05-06 00:57:30 +05:30
vikrantgupta25
b27de15827 feat(authz): fix openapi spec 2026-05-06 00:09:48 +05:30
vikrantgupta25
953cb71a43 feat(authz): add resource-level FGA and attach permissions for service accounts
- Add CheckAll middleware (AND of OR groups) for multi-resource authz checks
- Switch SA role routes (SetRole, DeleteRole) to VerbAttach on ResourceServiceAccount
- Add RoleAttachSelectors on SA module for role-level VerbAttach resolution
- DeleteRole uses CheckAll (both checks at middleware from URL params)
- SetRole uses Check (entity) at middleware + module-level role attach check
- Add migration 078 to backfill FGA tuples for existing organizations
- Add authz contributing guide (docs/contributing/go/authz.md)
- Regenerate OpenAPI spec with scoped security schemes
2026-05-05 22:17:41 +05:30
56 changed files with 2404 additions and 579 deletions

View File

@@ -66,9 +66,10 @@ func runGenerateAuthz(_ context.Context) error {
registry := coretypes.NewRegistry()
allowedResources := map[string]bool{
coretypes.NewResourceRef(coretypes.ResourceServiceAccount).String(): true,
coretypes.NewResourceRef(coretypes.ResourceRole).String(): true,
coretypes.NewResourceRef(coretypes.ResourceMetaResourcesRole).String(): true,
coretypes.NewResourceRef(coretypes.ResourceServiceAccount).String(): true,
coretypes.NewResourceRef(coretypes.ResourceMetaResourcesServiceAccount).String(): true,
coretypes.NewResourceRef(coretypes.ResourceRole).String(): true,
coretypes.NewResourceRef(coretypes.ResourceMetaResourcesRole).String(): true,
}
allowedTypes := map[string]bool{}

View File

@@ -448,6 +448,7 @@ components:
- delete
- list
- assignee
- attach
type: string
AuthtypesRole:
properties:
@@ -4463,19 +4464,20 @@ components:
$ref: '#/components/schemas/RuletypesEvaluationKind'
spec:
$ref: '#/components/schemas/RuletypesCumulativeWindow'
type: object
RuletypesEvaluationEnvelope:
oneOf:
- $ref: '#/components/schemas/RuletypesEvaluationRolling'
- $ref: '#/components/schemas/RuletypesEvaluationCumulative'
properties:
kind:
$ref: '#/components/schemas/RuletypesEvaluationKind'
spec: {}
required:
- kind
- spec
type: object
RuletypesEvaluationEnvelope:
discriminator:
mapping:
cumulative: '#/components/schemas/RuletypesEvaluationCumulative'
rolling: '#/components/schemas/RuletypesEvaluationRolling'
propertyName: kind
oneOf:
- $ref: '#/components/schemas/RuletypesEvaluationRolling'
- $ref: '#/components/schemas/RuletypesEvaluationCumulative'
type: object
RuletypesEvaluationKind:
enum:
- rolling
@@ -4487,6 +4489,9 @@ components:
$ref: '#/components/schemas/RuletypesEvaluationKind'
spec:
$ref: '#/components/schemas/RuletypesRollingWindow'
required:
- kind
- spec
type: object
RuletypesGettableTestRule:
properties:
@@ -4794,15 +4799,12 @@ components:
- compositeQuery
type: object
RuletypesRuleThresholdData:
discriminator:
mapping:
basic: '#/components/schemas/RuletypesThresholdBasic'
propertyName: kind
oneOf:
- $ref: '#/components/schemas/RuletypesThresholdBasic'
properties:
kind:
$ref: '#/components/schemas/RuletypesThresholdKind'
spec: {}
required:
- kind
- spec
type: object
RuletypesRuleType:
enum:
@@ -4844,6 +4846,9 @@ components:
$ref: '#/components/schemas/RuletypesThresholdKind'
spec:
$ref: '#/components/schemas/RuletypesBasicRuleThresholds'
required:
- kind
- spec
type: object
RuletypesThresholdKind:
enum:
@@ -9490,9 +9495,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- serviceaccount:list
- tokenizer:
- ADMIN
- serviceaccount:list
summary: List service accounts
tags:
- serviceaccount
@@ -9552,9 +9557,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- serviceaccount:create
- tokenizer:
- ADMIN
- serviceaccount:create
summary: Create service account
tags:
- serviceaccount
@@ -9602,9 +9607,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- serviceaccount:delete
- tokenizer:
- ADMIN
- serviceaccount:delete
summary: Deletes a service account
tags:
- serviceaccount
@@ -9659,9 +9664,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- serviceaccount:read
- tokenizer:
- ADMIN
- serviceaccount:read
summary: Gets a service account
tags:
- serviceaccount
@@ -9719,9 +9724,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- serviceaccount:update
- tokenizer:
- ADMIN
- serviceaccount:update
summary: Updates a service account
tags:
- serviceaccount
@@ -9773,9 +9778,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- serviceaccount:read
- tokenizer:
- ADMIN
- serviceaccount:read
summary: List service account keys
tags:
- serviceaccount
@@ -9841,9 +9846,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- serviceaccount:update
- tokenizer:
- ADMIN
- serviceaccount:update
summary: Create a service account key
tags:
- serviceaccount
@@ -9896,9 +9901,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- serviceaccount:update
- tokenizer:
- ADMIN
- serviceaccount:update
summary: Revoke a service account key
tags:
- serviceaccount
@@ -9961,9 +9966,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- serviceaccount:update
- tokenizer:
- ADMIN
- serviceaccount:update
summary: Updates a service account key
tags:
- serviceaccount
@@ -10022,9 +10027,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- serviceaccount:read
- tokenizer:
- ADMIN
- serviceaccount:read
summary: Gets service account roles
tags:
- serviceaccount
@@ -10084,9 +10089,11 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- serviceaccount:attach
- role:attach
- tokenizer:
- ADMIN
- serviceaccount:attach
- role:attach
summary: Create service account role
tags:
- serviceaccount
@@ -10133,9 +10140,11 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- serviceaccount:attach
- role:attach
- tokenizer:
- ADMIN
- serviceaccount:attach
- role:attach
summary: Delete service account role
tags:
- serviceaccount

View File

@@ -327,6 +327,11 @@ function App(): JSX.Element {
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
beforeSend(event) {
// Drop the event if its level is 'warning' or 'info'
if (event.level === 'warning' || event.level === 'info') {
return null;
}
const sessionReplayUrl = posthog.get_session_replay_url?.({
withTimestamp: true,
});

View File

@@ -1839,6 +1839,7 @@ export enum AuthtypesRelationDTO {
delete = 'delete',
list = 'list',
assignee = 'assignee',
attach = 'attach',
}
export interface AuthtypesRoleDTO {
/**
@@ -6676,28 +6677,36 @@ export interface RuletypesCumulativeWindowDTO {
timezone: string;
}
export enum RuletypesEvaluationCumulativeDTOKind {
cumulative = 'cumulative',
}
export interface RuletypesEvaluationCumulativeDTO {
kind?: RuletypesEvaluationKindDTO;
spec?: RuletypesCumulativeWindowDTO;
/**
* @type string
* @enum cumulative
*/
kind: RuletypesEvaluationCumulativeDTOKind;
spec: RuletypesCumulativeWindowDTO;
}
export type RuletypesEvaluationEnvelopeDTO =
| (RuletypesEvaluationRollingDTO & {
kind: RuletypesEvaluationKindDTO;
spec: unknown;
})
| (RuletypesEvaluationCumulativeDTO & {
kind: RuletypesEvaluationKindDTO;
spec: unknown;
});
| RuletypesEvaluationRollingDTO
| RuletypesEvaluationCumulativeDTO;
export enum RuletypesEvaluationKindDTO {
rolling = 'rolling',
cumulative = 'cumulative',
}
export enum RuletypesEvaluationRollingDTOKind {
rolling = 'rolling',
}
export interface RuletypesEvaluationRollingDTO {
kind?: RuletypesEvaluationKindDTO;
spec?: RuletypesRollingWindowDTO;
/**
* @type string
* @enum rolling
*/
kind: RuletypesEvaluationRollingDTOKind;
spec: RuletypesRollingWindowDTO;
}
export interface RuletypesGettableTestRuleDTO {
@@ -7052,10 +7061,7 @@ export interface RuletypesRuleConditionDTO {
thresholds?: RuletypesRuleThresholdDataDTO;
}
export type RuletypesRuleThresholdDataDTO = RuletypesThresholdBasicDTO & {
kind: RuletypesThresholdKindDTO;
spec: unknown;
};
export type RuletypesRuleThresholdDataDTO = RuletypesThresholdBasicDTO;
export enum RuletypesRuleTypeDTO {
threshold_rule = 'threshold_rule',
@@ -7091,9 +7097,16 @@ export enum RuletypesSeasonalityDTO {
daily = 'daily',
weekly = 'weekly',
}
export enum RuletypesThresholdBasicDTOKind {
basic = 'basic',
}
export interface RuletypesThresholdBasicDTO {
kind?: RuletypesThresholdKindDTO;
spec?: RuletypesBasicRuleThresholdsDTO;
/**
* @type string
* @enum basic
*/
kind: RuletypesThresholdBasicDTOKind;
spec: RuletypesBasicRuleThresholdsDTO;
}
export enum RuletypesThresholdKindDTO {

View File

@@ -7,6 +7,10 @@ export default {
kind: 'role',
type: 'metaresources',
},
{
kind: 'serviceaccount',
type: 'metaresources',
},
{
kind: 'role',
type: 'role',

View File

@@ -10,7 +10,7 @@ import (
)
func (provider *provider) addAlertmanagerRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/channels", handler.New(provider.authZ.ViewAccess(provider.alertmanagerHandler.ListChannels), handler.OpenAPIDef{
if err := router.Handle("/api/v1/channels", handler.New(provider.authzMiddleware.ViewAccess(provider.alertmanagerHandler.ListChannels), handler.OpenAPIDef{
ID: "ListChannels",
Tags: []string{"channels"},
Summary: "List notification channels",
@@ -27,7 +27,7 @@ func (provider *provider) addAlertmanagerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/channels/{id}", handler.New(provider.authZ.ViewAccess(provider.alertmanagerHandler.GetChannelByID), handler.OpenAPIDef{
if err := router.Handle("/api/v1/channels/{id}", handler.New(provider.authzMiddleware.ViewAccess(provider.alertmanagerHandler.GetChannelByID), handler.OpenAPIDef{
ID: "GetChannelByID",
Tags: []string{"channels"},
Summary: "Get notification channel by ID",
@@ -44,7 +44,7 @@ func (provider *provider) addAlertmanagerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/channels", handler.New(provider.authZ.AdminAccess(provider.alertmanagerHandler.CreateChannel), handler.OpenAPIDef{
if err := router.Handle("/api/v1/channels", handler.New(provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.CreateChannel), handler.OpenAPIDef{
ID: "CreateChannel",
Tags: []string{"channels"},
Summary: "Create notification channel",
@@ -61,7 +61,7 @@ func (provider *provider) addAlertmanagerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/channels/{id}", handler.New(provider.authZ.AdminAccess(provider.alertmanagerHandler.UpdateChannelByID), handler.OpenAPIDef{
if err := router.Handle("/api/v1/channels/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.UpdateChannelByID), handler.OpenAPIDef{
ID: "UpdateChannelByID",
Tags: []string{"channels"},
Summary: "Update notification channel",
@@ -78,7 +78,7 @@ func (provider *provider) addAlertmanagerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/channels/{id}", handler.New(provider.authZ.AdminAccess(provider.alertmanagerHandler.DeleteChannelByID), handler.OpenAPIDef{
if err := router.Handle("/api/v1/channels/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.DeleteChannelByID), handler.OpenAPIDef{
ID: "DeleteChannelByID",
Tags: []string{"channels"},
Summary: "Delete notification channel",
@@ -95,7 +95,7 @@ func (provider *provider) addAlertmanagerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/channels/test", handler.New(provider.authZ.EditAccess(provider.alertmanagerHandler.TestReceiver), handler.OpenAPIDef{
if err := router.Handle("/api/v1/channels/test", handler.New(provider.authzMiddleware.EditAccess(provider.alertmanagerHandler.TestReceiver), handler.OpenAPIDef{
ID: "TestChannel",
Tags: []string{"channels"},
Summary: "Test notification channel",
@@ -112,7 +112,7 @@ func (provider *provider) addAlertmanagerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/testChannel", handler.New(provider.authZ.EditAccess(provider.alertmanagerHandler.TestReceiver), handler.OpenAPIDef{
if err := router.Handle("/api/v1/testChannel", handler.New(provider.authzMiddleware.EditAccess(provider.alertmanagerHandler.TestReceiver), handler.OpenAPIDef{
ID: "TestChannelDeprecated",
Tags: []string{"channels"},
Summary: "Test notification channel (deprecated)",
@@ -129,7 +129,7 @@ func (provider *provider) addAlertmanagerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/route_policies", handler.New(provider.authZ.ViewAccess(provider.alertmanagerHandler.GetAllRoutePolicies), handler.OpenAPIDef{
if err := router.Handle("/api/v1/route_policies", handler.New(provider.authzMiddleware.ViewAccess(provider.alertmanagerHandler.GetAllRoutePolicies), handler.OpenAPIDef{
ID: "GetAllRoutePolicies",
Tags: []string{"routepolicies"},
Summary: "List route policies",
@@ -146,7 +146,7 @@ func (provider *provider) addAlertmanagerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/route_policies/{id}", handler.New(provider.authZ.ViewAccess(provider.alertmanagerHandler.GetRoutePolicyByID), handler.OpenAPIDef{
if err := router.Handle("/api/v1/route_policies/{id}", handler.New(provider.authzMiddleware.ViewAccess(provider.alertmanagerHandler.GetRoutePolicyByID), handler.OpenAPIDef{
ID: "GetRoutePolicyByID",
Tags: []string{"routepolicies"},
Summary: "Get route policy by ID",
@@ -163,7 +163,7 @@ func (provider *provider) addAlertmanagerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/route_policies", handler.New(provider.authZ.AdminAccess(provider.alertmanagerHandler.CreateRoutePolicy), handler.OpenAPIDef{
if err := router.Handle("/api/v1/route_policies", handler.New(provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.CreateRoutePolicy), handler.OpenAPIDef{
ID: "CreateRoutePolicy",
Tags: []string{"routepolicies"},
Summary: "Create route policy",
@@ -180,7 +180,7 @@ func (provider *provider) addAlertmanagerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/route_policies/{id}", handler.New(provider.authZ.AdminAccess(provider.alertmanagerHandler.UpdateRoutePolicy), handler.OpenAPIDef{
if err := router.Handle("/api/v1/route_policies/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.UpdateRoutePolicy), handler.OpenAPIDef{
ID: "UpdateRoutePolicy",
Tags: []string{"routepolicies"},
Summary: "Update route policy",
@@ -197,7 +197,7 @@ func (provider *provider) addAlertmanagerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/route_policies/{id}", handler.New(provider.authZ.AdminAccess(provider.alertmanagerHandler.DeleteRoutePolicyByID), handler.OpenAPIDef{
if err := router.Handle("/api/v1/route_policies/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.DeleteRoutePolicyByID), handler.OpenAPIDef{
ID: "DeleteRoutePolicyByID",
Tags: []string{"routepolicies"},
Summary: "Delete route policy",
@@ -214,7 +214,7 @@ func (provider *provider) addAlertmanagerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/alerts", handler.New(provider.authZ.ViewAccess(provider.alertmanagerHandler.GetAlerts), handler.OpenAPIDef{
if err := router.Handle("/api/v1/alerts", handler.New(provider.authzMiddleware.ViewAccess(provider.alertmanagerHandler.GetAlerts), handler.OpenAPIDef{
ID: "GetAlerts",
Tags: []string{"alerts"},
Summary: "Get alerts",

View File

@@ -10,7 +10,7 @@ import (
)
func (provider *provider) addAuthDomainRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/domains", handler.New(provider.authZ.AdminAccess(provider.authDomainHandler.List), handler.OpenAPIDef{
if err := router.Handle("/api/v1/domains", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.List), handler.OpenAPIDef{
ID: "ListAuthDomains",
Tags: []string{"authdomains"},
Summary: "List all auth domains",
@@ -27,7 +27,7 @@ func (provider *provider) addAuthDomainRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/domains", handler.New(provider.authZ.AdminAccess(provider.authDomainHandler.Create), handler.OpenAPIDef{
if err := router.Handle("/api/v1/domains", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Create), handler.OpenAPIDef{
ID: "CreateAuthDomain",
Tags: []string{"authdomains"},
Summary: "Create auth domain",
@@ -44,7 +44,7 @@ func (provider *provider) addAuthDomainRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/domains/{id}", handler.New(provider.authZ.AdminAccess(provider.authDomainHandler.Get), handler.OpenAPIDef{
if err := router.Handle("/api/v1/domains/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Get), handler.OpenAPIDef{
ID: "GetAuthDomain",
Tags: []string{"authdomains"},
Summary: "Get auth domain by ID",
@@ -61,7 +61,7 @@ func (provider *provider) addAuthDomainRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/domains/{id}", handler.New(provider.authZ.AdminAccess(provider.authDomainHandler.Update), handler.OpenAPIDef{
if err := router.Handle("/api/v1/domains/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Update), handler.OpenAPIDef{
ID: "UpdateAuthDomain",
Tags: []string{"authdomains"},
Summary: "Update auth domain",
@@ -78,7 +78,7 @@ func (provider *provider) addAuthDomainRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/domains/{id}", handler.New(provider.authZ.AdminAccess(provider.authDomainHandler.Delete), handler.OpenAPIDef{
if err := router.Handle("/api/v1/domains/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Delete), handler.OpenAPIDef{
ID: "DeleteAuthDomain",
Tags: []string{"authdomains"},
Summary: "Delete auth domain",

View File

@@ -11,7 +11,7 @@ import (
func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/cloud_integrations/{cloud_provider}/credentials", handler.New(
provider.authZ.AdminAccess(provider.cloudIntegrationHandler.GetConnectionCredentials),
provider.authzMiddleware.AdminAccess(provider.cloudIntegrationHandler.GetConnectionCredentials),
handler.OpenAPIDef{
ID: "GetConnectionCredentials",
Tags: []string{"cloudintegration"},
@@ -31,7 +31,7 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v1/cloud_integrations/{cloud_provider}/accounts", handler.New(
provider.authZ.AdminAccess(provider.cloudIntegrationHandler.CreateAccount),
provider.authzMiddleware.AdminAccess(provider.cloudIntegrationHandler.CreateAccount),
handler.OpenAPIDef{
ID: "CreateAccount",
Tags: []string{"cloudintegration"},
@@ -51,7 +51,7 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v1/cloud_integrations/{cloud_provider}/accounts", handler.New(
provider.authZ.AdminAccess(provider.cloudIntegrationHandler.ListAccounts),
provider.authzMiddleware.AdminAccess(provider.cloudIntegrationHandler.ListAccounts),
handler.OpenAPIDef{
ID: "ListAccounts",
Tags: []string{"cloudintegration"},
@@ -71,7 +71,7 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v1/cloud_integrations/{cloud_provider}/accounts/{id}", handler.New(
provider.authZ.AdminAccess(provider.cloudIntegrationHandler.GetAccount),
provider.authzMiddleware.AdminAccess(provider.cloudIntegrationHandler.GetAccount),
handler.OpenAPIDef{
ID: "GetAccount",
Tags: []string{"cloudintegration"},
@@ -91,7 +91,7 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v1/cloud_integrations/{cloud_provider}/accounts/{id}", handler.New(
provider.authZ.AdminAccess(provider.cloudIntegrationHandler.UpdateAccount),
provider.authzMiddleware.AdminAccess(provider.cloudIntegrationHandler.UpdateAccount),
handler.OpenAPIDef{
ID: "UpdateAccount",
Tags: []string{"cloudintegration"},
@@ -111,7 +111,7 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v1/cloud_integrations/{cloud_provider}/accounts/{id}", handler.New(
provider.authZ.AdminAccess(provider.cloudIntegrationHandler.DisconnectAccount),
provider.authzMiddleware.AdminAccess(provider.cloudIntegrationHandler.DisconnectAccount),
handler.OpenAPIDef{
ID: "DisconnectAccount",
Tags: []string{"cloudintegration"},
@@ -131,7 +131,7 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v1/cloud_integrations/{cloud_provider}/services", handler.New(
provider.authZ.AdminAccess(provider.cloudIntegrationHandler.ListServicesMetadata),
provider.authzMiddleware.AdminAccess(provider.cloudIntegrationHandler.ListServicesMetadata),
handler.OpenAPIDef{
ID: "ListServicesMetadata",
Tags: []string{"cloudintegration"},
@@ -152,7 +152,7 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v1/cloud_integrations/{cloud_provider}/services/{service_id}", handler.New(
provider.authZ.AdminAccess(provider.cloudIntegrationHandler.GetService),
provider.authzMiddleware.AdminAccess(provider.cloudIntegrationHandler.GetService),
handler.OpenAPIDef{
ID: "GetService",
Tags: []string{"cloudintegration"},
@@ -173,7 +173,7 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v1/cloud_integrations/{cloud_provider}/accounts/{id}/services/{service_id}", handler.New(
provider.authZ.AdminAccess(provider.cloudIntegrationHandler.UpdateService),
provider.authzMiddleware.AdminAccess(provider.cloudIntegrationHandler.UpdateService),
handler.OpenAPIDef{
ID: "UpdateService",
Tags: []string{"cloudintegration"},
@@ -195,7 +195,7 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
// Agent check-in endpoint is kept same as older one to maintain backward compatibility with already deployed agents.
// In the future, this endpoint will be deprecated and a new endpoint will be introduced for consistency with above endpoints.
if err := router.Handle("/api/v1/cloud-integrations/{cloud_provider}/agent-check-in", handler.New(
provider.authZ.ViewAccess(provider.cloudIntegrationHandler.AgentCheckIn),
provider.authzMiddleware.ViewAccess(provider.cloudIntegrationHandler.AgentCheckIn),
handler.OpenAPIDef{
ID: "AgentCheckInDeprecated",
Tags: []string{"cloudintegration"},
@@ -215,7 +215,7 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v1/cloud_integrations/{cloud_provider}/accounts/check_in", handler.New(
provider.authZ.ViewAccess(provider.cloudIntegrationHandler.AgentCheckIn),
provider.authzMiddleware.ViewAccess(provider.cloudIntegrationHandler.AgentCheckIn),
handler.OpenAPIDef{
ID: "AgentCheckIn",
Tags: []string{"cloudintegration"},

View File

@@ -14,7 +14,7 @@ import (
)
func (provider *provider) addDashboardRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/dashboards/{id}/public", handler.New(provider.authZ.AdminAccess(provider.dashboardHandler.CreatePublic), handler.OpenAPIDef{
if err := router.Handle("/api/v1/dashboards/{id}/public", handler.New(provider.authzMiddleware.AdminAccess(provider.dashboardHandler.CreatePublic), handler.OpenAPIDef{
ID: "CreatePublicDashboard",
Tags: []string{"dashboard"},
Summary: "Create public dashboard",
@@ -31,7 +31,7 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/dashboards/{id}/public", handler.New(provider.authZ.AdminAccess(provider.dashboardHandler.GetPublic), handler.OpenAPIDef{
if err := router.Handle("/api/v1/dashboards/{id}/public", handler.New(provider.authzMiddleware.AdminAccess(provider.dashboardHandler.GetPublic), handler.OpenAPIDef{
ID: "GetPublicDashboard",
Tags: []string{"dashboard"},
Summary: "Get public dashboard",
@@ -48,7 +48,7 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/dashboards/{id}/public", handler.New(provider.authZ.AdminAccess(provider.dashboardHandler.UpdatePublic), handler.OpenAPIDef{
if err := router.Handle("/api/v1/dashboards/{id}/public", handler.New(provider.authzMiddleware.AdminAccess(provider.dashboardHandler.UpdatePublic), handler.OpenAPIDef{
ID: "UpdatePublicDashboard",
Tags: []string{"dashboard"},
Summary: "Update public dashboard",
@@ -65,7 +65,7 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/dashboards/{id}/public", handler.New(provider.authZ.AdminAccess(provider.dashboardHandler.DeletePublic), handler.OpenAPIDef{
if err := router.Handle("/api/v1/dashboards/{id}/public", handler.New(provider.authzMiddleware.AdminAccess(provider.dashboardHandler.DeletePublic), handler.OpenAPIDef{
ID: "DeletePublicDashboard",
Tags: []string{"dashboard"},
Summary: "Delete public dashboard",
@@ -82,7 +82,7 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/public/dashboards/{id}", handler.New(provider.authZ.CheckWithoutClaims(
if err := router.Handle("/api/v1/public/dashboards/{id}", handler.New(provider.authzMiddleware.CheckWithoutClaims(
provider.dashboardHandler.GetPublicData,
authtypes.Relation{Verb: coretypes.VerbRead},
coretypes.ResourceMetaResourcePublicDashboard,
@@ -110,7 +110,7 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/public/dashboards/{id}/widgets/{idx}/query_range", handler.New(provider.authZ.CheckWithoutClaims(
if err := router.Handle("/api/v1/public/dashboards/{id}/widgets/{idx}/query_range", handler.New(provider.authzMiddleware.CheckWithoutClaims(
provider.dashboardHandler.GetPublicWidgetQueryRange,
authtypes.Relation{Verb: coretypes.VerbRead},
coretypes.ResourceMetaResourcePublicDashboard,

View File

@@ -10,7 +10,7 @@ import (
)
func (provider *provider) addFieldsRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/fields/keys", handler.New(provider.authZ.ViewAccess(provider.fieldsHandler.GetFieldsKeys), handler.OpenAPIDef{
if err := router.Handle("/api/v1/fields/keys", handler.New(provider.authzMiddleware.ViewAccess(provider.fieldsHandler.GetFieldsKeys), handler.OpenAPIDef{
ID: "GetFieldsKeys",
Tags: []string{"fields"},
Summary: "Get field keys",
@@ -28,7 +28,7 @@ func (provider *provider) addFieldsRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/fields/values", handler.New(provider.authZ.ViewAccess(provider.fieldsHandler.GetFieldsValues), handler.OpenAPIDef{
if err := router.Handle("/api/v1/fields/values", handler.New(provider.authzMiddleware.ViewAccess(provider.fieldsHandler.GetFieldsValues), handler.OpenAPIDef{
ID: "GetFieldsValues",
Tags: []string{"fields"},
Summary: "Get field values",

View File

@@ -10,7 +10,7 @@ import (
)
func (provider *provider) addFlaggerRoutes(router *mux.Router) error {
if err := router.Handle("/api/v2/features", handler.New(provider.authZ.ViewAccess(provider.flaggerHandler.GetFeatures), handler.OpenAPIDef{
if err := router.Handle("/api/v2/features", handler.New(provider.authzMiddleware.ViewAccess(provider.flaggerHandler.GetFeatures), handler.OpenAPIDef{
ID: "GetFeatures",
Tags: []string{"features"},
Summary: "Get features",

View File

@@ -10,7 +10,7 @@ import (
)
func (provider *provider) addGatewayRoutes(router *mux.Router) error {
if err := router.Handle("/api/v2/gateway/ingestion_keys", handler.New(provider.authZ.EditAccess(provider.gatewayHandler.GetIngestionKeys), handler.OpenAPIDef{
if err := router.Handle("/api/v2/gateway/ingestion_keys", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.GetIngestionKeys), handler.OpenAPIDef{
ID: "GetIngestionKeys",
Tags: []string{"gateway"},
Summary: "Get ingestion keys for workspace",
@@ -28,7 +28,7 @@ func (provider *provider) addGatewayRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/gateway/ingestion_keys/search", handler.New(provider.authZ.EditAccess(provider.gatewayHandler.SearchIngestionKeys), handler.OpenAPIDef{
if err := router.Handle("/api/v2/gateway/ingestion_keys/search", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.SearchIngestionKeys), handler.OpenAPIDef{
ID: "SearchIngestionKeys",
Tags: []string{"gateway"},
Summary: "Search ingestion keys for workspace",
@@ -46,7 +46,7 @@ func (provider *provider) addGatewayRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/gateway/ingestion_keys", handler.New(provider.authZ.EditAccess(provider.gatewayHandler.CreateIngestionKey), handler.OpenAPIDef{
if err := router.Handle("/api/v2/gateway/ingestion_keys", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.CreateIngestionKey), handler.OpenAPIDef{
ID: "CreateIngestionKey",
Tags: []string{"gateway"},
Summary: "Create ingestion key for workspace",
@@ -63,7 +63,7 @@ func (provider *provider) addGatewayRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/gateway/ingestion_keys/{keyId}", handler.New(provider.authZ.EditAccess(provider.gatewayHandler.UpdateIngestionKey), handler.OpenAPIDef{
if err := router.Handle("/api/v2/gateway/ingestion_keys/{keyId}", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.UpdateIngestionKey), handler.OpenAPIDef{
ID: "UpdateIngestionKey",
Tags: []string{"gateway"},
Summary: "Update ingestion key for workspace",
@@ -80,7 +80,7 @@ func (provider *provider) addGatewayRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/gateway/ingestion_keys/{keyId}", handler.New(provider.authZ.EditAccess(provider.gatewayHandler.DeleteIngestionKey), handler.OpenAPIDef{
if err := router.Handle("/api/v2/gateway/ingestion_keys/{keyId}", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.DeleteIngestionKey), handler.OpenAPIDef{
ID: "DeleteIngestionKey",
Tags: []string{"gateway"},
Summary: "Delete ingestion key for workspace",
@@ -97,7 +97,7 @@ func (provider *provider) addGatewayRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/gateway/ingestion_keys/{keyId}/limits", handler.New(provider.authZ.EditAccess(provider.gatewayHandler.CreateIngestionKeyLimit), handler.OpenAPIDef{
if err := router.Handle("/api/v2/gateway/ingestion_keys/{keyId}/limits", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.CreateIngestionKeyLimit), handler.OpenAPIDef{
ID: "CreateIngestionKeyLimit",
Tags: []string{"gateway"},
Summary: "Create limit for the ingestion key",
@@ -114,7 +114,7 @@ func (provider *provider) addGatewayRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/gateway/ingestion_keys/limits/{limitId}", handler.New(provider.authZ.EditAccess(provider.gatewayHandler.UpdateIngestionKeyLimit), handler.OpenAPIDef{
if err := router.Handle("/api/v2/gateway/ingestion_keys/limits/{limitId}", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.UpdateIngestionKeyLimit), handler.OpenAPIDef{
ID: "UpdateIngestionKeyLimit",
Tags: []string{"gateway"},
Summary: "Update limit for the ingestion key",
@@ -131,7 +131,7 @@ func (provider *provider) addGatewayRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/gateway/ingestion_keys/limits/{limitId}", handler.New(provider.authZ.EditAccess(provider.gatewayHandler.DeleteIngestionKeyLimit), handler.OpenAPIDef{
if err := router.Handle("/api/v2/gateway/ingestion_keys/limits/{limitId}", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.DeleteIngestionKeyLimit), handler.OpenAPIDef{
ID: "DeleteIngestionKeyLimit",
Tags: []string{"gateway"},
Summary: "Delete limit for the ingestion key",

View File

@@ -9,7 +9,7 @@ import (
)
func (provider *provider) addGlobalRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/global/config", handler.New(provider.authZ.OpenAccess(provider.globalHandler.GetConfig), handler.OpenAPIDef{
if err := router.Handle("/api/v1/global/config", handler.New(provider.authzMiddleware.OpenAccess(provider.globalHandler.GetConfig), handler.OpenAPIDef{
ID: "GetGlobalConfig",
Tags: []string{"global"},
Summary: "Get global config",

View File

@@ -11,7 +11,7 @@ import (
func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
if err := router.Handle("/api/v2/infra_monitoring/hosts", handler.New(
provider.authZ.ViewAccess(provider.infraMonitoringHandler.ListHosts),
provider.authzMiddleware.ViewAccess(provider.infraMonitoringHandler.ListHosts),
handler.OpenAPIDef{
ID: "ListHosts",
Tags: []string{"inframonitoring"},
@@ -30,7 +30,7 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v2/infra_monitoring/pods", handler.New(
provider.authZ.ViewAccess(provider.infraMonitoringHandler.ListPods),
provider.authzMiddleware.ViewAccess(provider.infraMonitoringHandler.ListPods),
handler.OpenAPIDef{
ID: "ListPods",
Tags: []string{"inframonitoring"},
@@ -49,7 +49,7 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v2/infra_monitoring/nodes", handler.New(
provider.authZ.ViewAccess(provider.infraMonitoringHandler.ListNodes),
provider.authzMiddleware.ViewAccess(provider.infraMonitoringHandler.ListNodes),
handler.OpenAPIDef{
ID: "ListNodes",
Tags: []string{"inframonitoring"},

View File

@@ -11,7 +11,7 @@ import (
func (provider *provider) addLLMPricingRuleRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/llm_pricing_rules", handler.New(
provider.authZ.ViewAccess(provider.llmPricingRuleHandler.List),
provider.authzMiddleware.ViewAccess(provider.llmPricingRuleHandler.List),
handler.OpenAPIDef{
ID: "ListLLMPricingRules",
Tags: []string{"llmpricingrules"},
@@ -32,7 +32,7 @@ func (provider *provider) addLLMPricingRuleRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v1/llm_pricing_rules", handler.New(
provider.authZ.AdminAccess(provider.llmPricingRuleHandler.CreateOrUpdate),
provider.authzMiddleware.AdminAccess(provider.llmPricingRuleHandler.CreateOrUpdate),
handler.OpenAPIDef{
ID: "CreateOrUpdateLLMPricingRules",
Tags: []string{"llmpricingrules"},
@@ -50,7 +50,7 @@ func (provider *provider) addLLMPricingRuleRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v1/llm_pricing_rules/{id}", handler.New(
provider.authZ.ViewAccess(provider.llmPricingRuleHandler.Get),
provider.authzMiddleware.ViewAccess(provider.llmPricingRuleHandler.Get),
handler.OpenAPIDef{
ID: "GetLLMPricingRule",
Tags: []string{"llmpricingrules"},
@@ -70,7 +70,7 @@ func (provider *provider) addLLMPricingRuleRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v1/llm_pricing_rules/{id}", handler.New(
provider.authZ.AdminAccess(provider.llmPricingRuleHandler.Delete),
provider.authzMiddleware.AdminAccess(provider.llmPricingRuleHandler.Delete),
handler.OpenAPIDef{
ID: "DeleteLLMPricingRule",
Tags: []string{"llmpricingrules"},

View File

@@ -11,7 +11,7 @@ import (
func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
if err := router.Handle("/api/v2/metrics", handler.New(
provider.authZ.ViewAccess(provider.metricsExplorerHandler.ListMetrics),
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.ListMetrics),
handler.OpenAPIDef{
ID: "ListMetrics",
Tags: []string{"metrics"},
@@ -31,7 +31,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v2/metrics/stats", handler.New(
provider.authZ.ViewAccess(provider.metricsExplorerHandler.GetStats),
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetStats),
handler.OpenAPIDef{
ID: "GetMetricsStats",
Tags: []string{"metrics"},
@@ -50,7 +50,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v2/metrics/treemap", handler.New(
provider.authZ.ViewAccess(provider.metricsExplorerHandler.GetTreemap),
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetTreemap),
handler.OpenAPIDef{
ID: "GetMetricsTreemap",
Tags: []string{"metrics"},
@@ -69,7 +69,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v2/metrics/{metric_name}/attributes", handler.New(
provider.authZ.ViewAccess(provider.metricsExplorerHandler.GetMetricAttributes),
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetMetricAttributes),
handler.OpenAPIDef{
ID: "GetMetricAttributes",
Tags: []string{"metrics"},
@@ -89,7 +89,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v2/metrics/{metric_name}/metadata", handler.New(
provider.authZ.ViewAccess(provider.metricsExplorerHandler.GetMetricMetadata),
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetMetricMetadata),
handler.OpenAPIDef{
ID: "GetMetricMetadata",
Tags: []string{"metrics"},
@@ -108,7 +108,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v2/metrics/{metric_name}/metadata", handler.New(
provider.authZ.EditAccess(provider.metricsExplorerHandler.UpdateMetricMetadata),
provider.authzMiddleware.EditAccess(provider.metricsExplorerHandler.UpdateMetricMetadata),
handler.OpenAPIDef{
ID: "UpdateMetricMetadata",
Tags: []string{"metrics"},
@@ -127,7 +127,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v2/metrics/{metric_name}/highlights", handler.New(
provider.authZ.ViewAccess(provider.metricsExplorerHandler.GetMetricHighlights),
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetMetricHighlights),
handler.OpenAPIDef{
ID: "GetMetricHighlights",
Tags: []string{"metrics"},
@@ -146,7 +146,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v2/metrics/{metric_name}/alerts", handler.New(
provider.authZ.ViewAccess(provider.metricsExplorerHandler.GetMetricAlerts),
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetMetricAlerts),
handler.OpenAPIDef{
ID: "GetMetricAlerts",
Tags: []string{"metrics"},
@@ -165,7 +165,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v2/metrics/{metric_name}/dashboards", handler.New(
provider.authZ.ViewAccess(provider.metricsExplorerHandler.GetMetricDashboards),
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetMetricDashboards),
handler.OpenAPIDef{
ID: "GetMetricDashboards",
Tags: []string{"metrics"},
@@ -184,7 +184,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v2/metrics/inspect", handler.New(
provider.authZ.ViewAccess(provider.metricsExplorerHandler.InspectMetrics),
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.InspectMetrics),
handler.OpenAPIDef{
ID: "InspectMetrics",
Tags: []string{"metrics"},
@@ -203,7 +203,7 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v2/metrics/onboarding", handler.New(
provider.authZ.ViewAccess(provider.metricsExplorerHandler.GetOnboardingStatus),
provider.authzMiddleware.ViewAccess(provider.metricsExplorerHandler.GetOnboardingStatus),
handler.OpenAPIDef{
ID: "GetMetricsOnboardingStatus",
Tags: []string{"metrics"},

View File

@@ -9,7 +9,7 @@ import (
)
func (provider *provider) addOrgRoutes(router *mux.Router) error {
if err := router.Handle("/api/v2/orgs/me", handler.New(provider.authZ.AdminAccess(provider.orgHandler.Get), handler.OpenAPIDef{
if err := router.Handle("/api/v2/orgs/me", handler.New(provider.authzMiddleware.AdminAccess(provider.orgHandler.Get), handler.OpenAPIDef{
ID: "GetMyOrganization",
Tags: []string{"orgs"},
Summary: "Get my organization",
@@ -26,7 +26,7 @@ func (provider *provider) addOrgRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/orgs/me", handler.New(provider.authZ.AdminAccess(provider.orgHandler.Update), handler.OpenAPIDef{
if err := router.Handle("/api/v2/orgs/me", handler.New(provider.authzMiddleware.AdminAccess(provider.orgHandler.Update), handler.OpenAPIDef{
ID: "UpdateMyOrganization",
Tags: []string{"orgs"},
Summary: "Update my organization",

View File

@@ -10,7 +10,7 @@ import (
)
func (provider *provider) addPreferenceRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/user/preferences", handler.New(provider.authZ.ViewAccess(provider.preferenceHandler.ListByUser), handler.OpenAPIDef{
if err := router.Handle("/api/v1/user/preferences", handler.New(provider.authzMiddleware.ViewAccess(provider.preferenceHandler.ListByUser), handler.OpenAPIDef{
ID: "ListUserPreferences",
Tags: []string{"preferences"},
Summary: "List user preferences",
@@ -27,7 +27,7 @@ func (provider *provider) addPreferenceRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/user/preferences/{name}", handler.New(provider.authZ.ViewAccess(provider.preferenceHandler.GetByUser), handler.OpenAPIDef{
if err := router.Handle("/api/v1/user/preferences/{name}", handler.New(provider.authzMiddleware.ViewAccess(provider.preferenceHandler.GetByUser), handler.OpenAPIDef{
ID: "GetUserPreference",
Tags: []string{"preferences"},
Summary: "Get user preference",
@@ -44,7 +44,7 @@ func (provider *provider) addPreferenceRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/user/preferences/{name}", handler.New(provider.authZ.ViewAccess(provider.preferenceHandler.UpdateByUser), handler.OpenAPIDef{
if err := router.Handle("/api/v1/user/preferences/{name}", handler.New(provider.authzMiddleware.ViewAccess(provider.preferenceHandler.UpdateByUser), handler.OpenAPIDef{
ID: "UpdateUserPreference",
Tags: []string{"preferences"},
Summary: "Update user preference",
@@ -61,7 +61,7 @@ func (provider *provider) addPreferenceRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/org/preferences", handler.New(provider.authZ.AdminAccess(provider.preferenceHandler.ListByOrg), handler.OpenAPIDef{
if err := router.Handle("/api/v1/org/preferences", handler.New(provider.authzMiddleware.AdminAccess(provider.preferenceHandler.ListByOrg), handler.OpenAPIDef{
ID: "ListOrgPreferences",
Tags: []string{"preferences"},
Summary: "List org preferences",
@@ -78,7 +78,7 @@ func (provider *provider) addPreferenceRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/org/preferences/{name}", handler.New(provider.authZ.AdminAccess(provider.preferenceHandler.GetByOrg), handler.OpenAPIDef{
if err := router.Handle("/api/v1/org/preferences/{name}", handler.New(provider.authzMiddleware.AdminAccess(provider.preferenceHandler.GetByOrg), handler.OpenAPIDef{
ID: "GetOrgPreference",
Tags: []string{"preferences"},
Summary: "Get org preference",
@@ -95,7 +95,7 @@ func (provider *provider) addPreferenceRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/org/preferences/{name}", handler.New(provider.authZ.AdminAccess(provider.preferenceHandler.UpdateByOrg), handler.OpenAPIDef{
if err := router.Handle("/api/v1/org/preferences/{name}", handler.New(provider.authzMiddleware.AdminAccess(provider.preferenceHandler.UpdateByOrg), handler.OpenAPIDef{
ID: "UpdateOrgPreference",
Tags: []string{"preferences"},
Summary: "Update org preference",

View File

@@ -10,7 +10,7 @@ import (
)
func (provider *provider) addPromoteRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/logs/promote_paths", handler.New(provider.authZ.EditAccess(provider.promoteHandler.HandlePromoteAndIndexPaths), handler.OpenAPIDef{
if err := router.Handle("/api/v1/logs/promote_paths", handler.New(provider.authzMiddleware.EditAccess(provider.promoteHandler.HandlePromoteAndIndexPaths), handler.OpenAPIDef{
ID: "HandlePromoteAndIndexPaths",
Tags: []string{"logs"},
Summary: "Promote and index paths",
@@ -26,7 +26,7 @@ func (provider *provider) addPromoteRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/logs/promote_paths", handler.New(provider.authZ.ViewAccess(provider.promoteHandler.ListPromotedAndIndexedPaths), handler.OpenAPIDef{
if err := router.Handle("/api/v1/logs/promote_paths", handler.New(provider.authzMiddleware.ViewAccess(provider.promoteHandler.ListPromotedAndIndexedPaths), handler.OpenAPIDef{
ID: "ListPromotedAndIndexedPaths",
Tags: []string{"logs"},
Summary: "Promote and index paths",

View File

@@ -41,7 +41,8 @@ type provider struct {
config apiserver.Config
settings factory.ScopedProviderSettings
router *mux.Router
authZ *middleware.AuthZ
authzMiddleware *middleware.AuthZ
authzService authz.AuthZ
orgHandler organization.Handler
userHandler user.Handler
sessionHandler session.Handler
@@ -73,7 +74,7 @@ type provider struct {
func NewFactory(
orgGetter organization.Getter,
authz authz.AuthZ,
authzService authz.AuthZ,
orgHandler organization.Handler,
userHandler user.Handler,
sessionHandler session.Handler,
@@ -108,7 +109,7 @@ func NewFactory(
providerSettings,
config,
orgGetter,
authz,
authzService,
orgHandler,
userHandler,
sessionHandler,
@@ -145,7 +146,7 @@ func newProvider(
providerSettings factory.ProviderSettings,
config apiserver.Config,
orgGetter organization.Getter,
authz authz.AuthZ,
authzService authz.AuthZ,
orgHandler organization.Handler,
userHandler user.Handler,
sessionHandler session.Handler,
@@ -183,6 +184,7 @@ func newProvider(
router: router,
orgHandler: orgHandler,
userHandler: userHandler,
authzService: authzService,
sessionHandler: sessionHandler,
authDomainHandler: authDomainHandler,
preferenceHandler: preferenceHandler,
@@ -210,7 +212,7 @@ func newProvider(
llmPricingRuleHandler: llmPricingRuleHandler,
}
provider.authZ = middleware.NewAuthZ(settings.Logger(), orgGetter, authz)
provider.authzMiddleware = middleware.NewAuthZ(settings.Logger(), orgGetter, authzService)
if err := provider.AddToRouter(router); err != nil {
return nil, err
@@ -336,10 +338,7 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
}
func newSecuritySchemes(role types.Role) []handler.OpenAPISecurityScheme {
return []handler.OpenAPISecurityScheme{
{Name: authtypes.IdentNProviderAPIKey.StringValue(), Scopes: []string{role.String()}},
{Name: authtypes.IdentNProviderTokenizer.StringValue(), Scopes: []string{role.String()}},
}
return newScopedSecuritySchemes([]string{role.String()})
}
func newAnonymousSecuritySchemes(scopes []string) []handler.OpenAPISecurityScheme {
@@ -347,3 +346,10 @@ func newAnonymousSecuritySchemes(scopes []string) []handler.OpenAPISecuritySchem
{Name: authtypes.IdentNProviderAnonymous.StringValue(), Scopes: scopes},
}
}
func newScopedSecuritySchemes(scopes []string) []handler.OpenAPISecurityScheme {
return []handler.OpenAPISecurityScheme{
{Name: authtypes.IdentNProviderAPIKey.StringValue(), Scopes: scopes},
{Name: authtypes.IdentNProviderTokenizer.StringValue(), Scopes: scopes},
}
}

View File

@@ -10,7 +10,7 @@ import (
)
func (provider *provider) addQuerierRoutes(router *mux.Router) error {
if err := router.Handle("/api/v5/query_range", handler.New(provider.authZ.ViewAccess(provider.querierHandler.QueryRange), handler.OpenAPIDef{
if err := router.Handle("/api/v5/query_range", handler.New(provider.authzMiddleware.ViewAccess(provider.querierHandler.QueryRange), handler.OpenAPIDef{
ID: "QueryRangeV5",
Tags: []string{"querier"},
Summary: "Query range",
@@ -451,7 +451,7 @@ func (provider *provider) addQuerierRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v5/substitute_vars", handler.New(provider.authZ.ViewAccess(provider.querierHandler.ReplaceVariables), handler.OpenAPIDef{
if err := router.Handle("/api/v5/substitute_vars", handler.New(provider.authzMiddleware.ViewAccess(provider.querierHandler.ReplaceVariables), handler.OpenAPIDef{
ID: "ReplaceVariables",
Tags: []string{"querier"},
Summary: "Replace variables",

View File

@@ -12,7 +12,7 @@ import (
func (provider *provider) addRawDataExportRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/export_raw_data", handler.New(provider.authZ.ViewAccess(provider.rawDataExportHandler.ExportRawData), handler.OpenAPIDef{
if err := router.Handle("/api/v1/export_raw_data", handler.New(provider.authzMiddleware.ViewAccess(provider.rawDataExportHandler.ExportRawData), handler.OpenAPIDef{
ID: "HandleExportRawDataPOST",
Tags: []string{"logs", "traces"},
Summary: "Export raw data",

View File

@@ -57,7 +57,7 @@ func (handler *healthOpenAPIHandler) AuditDef() *pkghandler.AuditDef {
func (provider *provider) addRegistryRoutes(router *mux.Router) error {
if err := router.Handle("/api/v2/healthz", newHealthOpenAPIHandler(
provider.authZ.OpenAccess(provider.factoryHandler.Healthz),
provider.authzMiddleware.OpenAccess(provider.factoryHandler.Healthz),
"Healthz",
"Health check",
)).Methods(http.MethodGet).GetError(); err != nil {
@@ -65,14 +65,14 @@ func (provider *provider) addRegistryRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v2/readyz", newHealthOpenAPIHandler(
provider.authZ.OpenAccess(provider.factoryHandler.Readyz),
provider.authzMiddleware.OpenAccess(provider.factoryHandler.Readyz),
"Readyz",
"Readiness check",
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/livez", pkghandler.New(provider.authZ.OpenAccess(provider.factoryHandler.Livez),
if err := router.Handle("/api/v2/livez", pkghandler.New(provider.authzMiddleware.OpenAccess(provider.factoryHandler.Livez),
pkghandler.OpenAPIDef{
ID: "Livez",
Tags: []string{"health"},

View File

@@ -11,7 +11,7 @@ import (
)
func (provider *provider) addRoleRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/roles", handler.New(provider.authZ.AdminAccess(provider.authzHandler.Create), handler.OpenAPIDef{
if err := router.Handle("/api/v1/roles", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.Create), handler.OpenAPIDef{
ID: "CreateRole",
Tags: []string{"role"},
Summary: "Create role",
@@ -28,7 +28,7 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/roles", handler.New(provider.authZ.AdminAccess(provider.authzHandler.List), handler.OpenAPIDef{
if err := router.Handle("/api/v1/roles", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.List), handler.OpenAPIDef{
ID: "ListRoles",
Tags: []string{"role"},
Summary: "List roles",
@@ -45,7 +45,7 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authZ.AdminAccess(provider.authzHandler.Get), handler.OpenAPIDef{
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.Get), handler.OpenAPIDef{
ID: "GetRole",
Tags: []string{"role"},
Summary: "Get role",
@@ -62,7 +62,7 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/roles/{id}/relations/{relation}/objects", handler.New(provider.authZ.AdminAccess(provider.authzHandler.GetObjects), handler.OpenAPIDef{
if err := router.Handle("/api/v1/roles/{id}/relations/{relation}/objects", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.GetObjects), handler.OpenAPIDef{
ID: "GetObjects",
Tags: []string{"role"},
Summary: "Get objects for a role by relation",
@@ -79,7 +79,7 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authZ.AdminAccess(provider.authzHandler.Patch), handler.OpenAPIDef{
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.Patch), handler.OpenAPIDef{
ID: "PatchRole",
Tags: []string{"role"},
Summary: "Patch role",
@@ -96,7 +96,7 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/roles/{id}/relations/{relation}/objects", handler.New(provider.authZ.AdminAccess(provider.authzHandler.PatchObjects), handler.OpenAPIDef{
if err := router.Handle("/api/v1/roles/{id}/relations/{relation}/objects", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.PatchObjects), handler.OpenAPIDef{
ID: "PatchObjects",
Tags: []string{"role"},
Summary: "Patch objects for a role by relation",
@@ -113,7 +113,7 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authZ.AdminAccess(provider.authzHandler.Delete), handler.OpenAPIDef{
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.Delete), handler.OpenAPIDef{
ID: "DeleteRole",
Tags: []string{"role"},
Summary: "Delete role",

View File

@@ -10,7 +10,7 @@ import (
)
func (provider *provider) addRulerRoutes(router *mux.Router) error {
if err := router.Handle("/api/v2/rules", handler.New(provider.authZ.ViewAccess(provider.rulerHandler.ListRules), handler.OpenAPIDef{
if err := router.Handle("/api/v2/rules", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.ListRules), handler.OpenAPIDef{
ID: "ListRules",
Tags: []string{"rules"},
Summary: "List alert rules",
@@ -23,7 +23,7 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/rules/{id}", handler.New(provider.authZ.ViewAccess(provider.rulerHandler.GetRuleByID), handler.OpenAPIDef{
if err := router.Handle("/api/v2/rules/{id}", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.GetRuleByID), handler.OpenAPIDef{
ID: "GetRuleByID",
Tags: []string{"rules"},
Summary: "Get alert rule by ID",
@@ -37,7 +37,7 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/rules", handler.New(provider.authZ.EditAccess(provider.rulerHandler.CreateRule), handler.OpenAPIDef{
if err := router.Handle("/api/v2/rules", handler.New(provider.authzMiddleware.EditAccess(provider.rulerHandler.CreateRule), handler.OpenAPIDef{
ID: "CreateRule",
Tags: []string{"rules"},
Summary: "Create alert rule",
@@ -54,7 +54,7 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/rules/{id}", handler.New(provider.authZ.EditAccess(provider.rulerHandler.UpdateRuleByID), handler.OpenAPIDef{
if err := router.Handle("/api/v2/rules/{id}", handler.New(provider.authzMiddleware.EditAccess(provider.rulerHandler.UpdateRuleByID), handler.OpenAPIDef{
ID: "UpdateRuleByID",
Tags: []string{"rules"},
Summary: "Update alert rule",
@@ -69,7 +69,7 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/rules/{id}", handler.New(provider.authZ.EditAccess(provider.rulerHandler.DeleteRuleByID), handler.OpenAPIDef{
if err := router.Handle("/api/v2/rules/{id}", handler.New(provider.authzMiddleware.EditAccess(provider.rulerHandler.DeleteRuleByID), handler.OpenAPIDef{
ID: "DeleteRuleByID",
Tags: []string{"rules"},
Summary: "Delete alert rule",
@@ -81,7 +81,7 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/rules/{id}", handler.New(provider.authZ.EditAccess(provider.rulerHandler.PatchRuleByID), handler.OpenAPIDef{
if err := router.Handle("/api/v2/rules/{id}", handler.New(provider.authzMiddleware.EditAccess(provider.rulerHandler.PatchRuleByID), handler.OpenAPIDef{
ID: "PatchRuleByID",
Tags: []string{"rules"},
Summary: "Patch alert rule",
@@ -98,7 +98,7 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/rules/test", handler.New(provider.authZ.EditAccess(provider.rulerHandler.TestRule), handler.OpenAPIDef{
if err := router.Handle("/api/v2/rules/test", handler.New(provider.authzMiddleware.EditAccess(provider.rulerHandler.TestRule), handler.OpenAPIDef{
ID: "TestRule",
Tags: []string{"rules"},
Summary: "Test alert rule",
@@ -115,7 +115,7 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/downtime_schedules", handler.New(provider.authZ.ViewAccess(provider.rulerHandler.ListDowntimeSchedules), handler.OpenAPIDef{
if err := router.Handle("/api/v1/downtime_schedules", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.ListDowntimeSchedules), handler.OpenAPIDef{
ID: "ListDowntimeSchedules",
Tags: []string{"downtimeschedules"},
Summary: "List downtime schedules",
@@ -129,7 +129,7 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/downtime_schedules/{id}", handler.New(provider.authZ.ViewAccess(provider.rulerHandler.GetDowntimeScheduleByID), handler.OpenAPIDef{
if err := router.Handle("/api/v1/downtime_schedules/{id}", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.GetDowntimeScheduleByID), handler.OpenAPIDef{
ID: "GetDowntimeScheduleByID",
Tags: []string{"downtimeschedules"},
Summary: "Get downtime schedule by ID",
@@ -143,7 +143,7 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/downtime_schedules", handler.New(provider.authZ.EditAccess(provider.rulerHandler.CreateDowntimeSchedule), handler.OpenAPIDef{
if err := router.Handle("/api/v1/downtime_schedules", handler.New(provider.authzMiddleware.EditAccess(provider.rulerHandler.CreateDowntimeSchedule), handler.OpenAPIDef{
ID: "CreateDowntimeSchedule",
Tags: []string{"downtimeschedules"},
Summary: "Create downtime schedule",
@@ -159,7 +159,7 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/downtime_schedules/{id}", handler.New(provider.authZ.EditAccess(provider.rulerHandler.UpdateDowntimeScheduleByID), handler.OpenAPIDef{
if err := router.Handle("/api/v1/downtime_schedules/{id}", handler.New(provider.authzMiddleware.EditAccess(provider.rulerHandler.UpdateDowntimeScheduleByID), handler.OpenAPIDef{
ID: "UpdateDowntimeScheduleByID",
Tags: []string{"downtimeschedules"},
Summary: "Update downtime schedule",
@@ -173,7 +173,7 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/downtime_schedules/{id}", handler.New(provider.authZ.EditAccess(provider.rulerHandler.DeleteDowntimeScheduleByID), handler.OpenAPIDef{
if err := router.Handle("/api/v1/downtime_schedules/{id}", handler.New(provider.authzMiddleware.EditAccess(provider.rulerHandler.DeleteDowntimeScheduleByID), handler.OpenAPIDef{
ID: "DeleteDowntimeScheduleByID",
Tags: []string{"downtimeschedules"},
Summary: "Delete downtime schedule",

View File

@@ -13,7 +13,7 @@ import (
func (provider *provider) addRuleStateHistoryRoutes(router *mux.Router) error {
if err := router.Handle("/api/v2/rules/{id}/history/stats", handler.New(
provider.authZ.ViewAccess(provider.ruleStateHistoryHandler.GetRuleHistoryStats),
provider.authzMiddleware.ViewAccess(provider.ruleStateHistoryHandler.GetRuleHistoryStats),
handler.OpenAPIDef{
ID: "GetRuleHistoryStats",
Tags: []string{"rules"},
@@ -30,7 +30,7 @@ func (provider *provider) addRuleStateHistoryRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v2/rules/{id}/history/timeline", handler.New(
provider.authZ.ViewAccess(provider.ruleStateHistoryHandler.GetRuleHistoryTimeline),
provider.authzMiddleware.ViewAccess(provider.ruleStateHistoryHandler.GetRuleHistoryTimeline),
handler.OpenAPIDef{
ID: "GetRuleHistoryTimeline",
Tags: []string{"rules"},
@@ -47,7 +47,7 @@ func (provider *provider) addRuleStateHistoryRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v2/rules/{id}/history/top_contributors", handler.New(
provider.authZ.ViewAccess(provider.ruleStateHistoryHandler.GetRuleHistoryContributors),
provider.authzMiddleware.ViewAccess(provider.ruleStateHistoryHandler.GetRuleHistoryContributors),
handler.OpenAPIDef{
ID: "GetRuleHistoryTopContributors",
Tags: []string{"rules"},
@@ -64,7 +64,7 @@ func (provider *provider) addRuleStateHistoryRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v2/rules/{id}/history/filter_keys", handler.New(
provider.authZ.ViewAccess(provider.ruleStateHistoryHandler.GetRuleHistoryFilterKeys),
provider.authzMiddleware.ViewAccess(provider.ruleStateHistoryHandler.GetRuleHistoryFilterKeys),
handler.OpenAPIDef{
ID: "GetRuleHistoryFilterKeys",
Tags: []string{"rules"},
@@ -81,7 +81,7 @@ func (provider *provider) addRuleStateHistoryRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v2/rules/{id}/history/filter_values", handler.New(
provider.authZ.ViewAccess(provider.ruleStateHistoryHandler.GetRuleHistoryFilterValues),
provider.authzMiddleware.ViewAccess(provider.ruleStateHistoryHandler.GetRuleHistoryFilterValues),
handler.OpenAPIDef{
ID: "GetRuleHistoryFilterValues",
Tags: []string{"rules"},
@@ -98,7 +98,7 @@ func (provider *provider) addRuleStateHistoryRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v2/rules/{id}/history/overall_status", handler.New(
provider.authZ.ViewAccess(provider.ruleStateHistoryHandler.GetRuleHistoryOverallStatus),
provider.authzMiddleware.ViewAccess(provider.ruleStateHistoryHandler.GetRuleHistoryOverallStatus),
handler.OpenAPIDef{
ID: "GetRuleHistoryOverallStatus",
Tags: []string{"rules"},

View File

@@ -1,17 +1,25 @@
package signozapiserver
import (
"bytes"
"encoding/json"
"io"
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/serviceaccounttypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/service_accounts", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.Create), handler.OpenAPIDef{
if err := router.Handle("/api/v1/service_accounts", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.Create, authtypes.Relation{Verb: coretypes.VerbCreate}, coretypes.ResourceMetaResourcesServiceAccount, serviceAccountCollectionSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "CreateServiceAccount",
Tags: []string{"serviceaccount"},
Summary: "Create service account",
@@ -23,12 +31,14 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourcesServiceAccount.Scope(coretypes.VerbCreate)}),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.List), handler.OpenAPIDef{
if err := router.Handle("/api/v1/service_accounts", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.List, authtypes.Relation{Verb: coretypes.VerbList}, coretypes.ResourceMetaResourcesServiceAccount, serviceAccountCollectionSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "ListServiceAccounts",
Tags: []string{"serviceaccount"},
Summary: "List service accounts",
@@ -40,12 +50,12 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourcesServiceAccount.Scope(coretypes.VerbList)}),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/me", handler.New(provider.authZ.OpenAccess(provider.serviceAccountHandler.GetMe), handler.OpenAPIDef{
if err := router.Handle("/api/v1/service_accounts/me", handler.New(provider.authzMiddleware.OpenAccess(provider.serviceAccountHandler.GetMe), handler.OpenAPIDef{
ID: "GetMyServiceAccount",
Tags: []string{"serviceaccount"},
Summary: "Gets my service account",
@@ -62,7 +72,9 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.Get), handler.OpenAPIDef{
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.Get, authtypes.Relation{Verb: coretypes.VerbRead}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "GetServiceAccount",
Tags: []string{"serviceaccount"},
Summary: "Gets a service account",
@@ -74,12 +86,14 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbRead)}),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}/roles", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.GetRoles), handler.OpenAPIDef{
if err := router.Handle("/api/v1/service_accounts/{id}/roles", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.GetRoles, authtypes.Relation{Verb: coretypes.VerbRead}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "GetServiceAccountRoles",
Tags: []string{"serviceaccount"},
Summary: "Gets service account roles",
@@ -91,12 +105,19 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbRead)}),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}/roles", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.SetRole), handler.OpenAPIDef{
if err := router.Handle("/api/v1/service_accounts/{id}/roles", handler.New(provider.authzMiddleware.CheckAll(provider.serviceAccountHandler.SetRole, []middleware.AuthZCheckGroup{
{{Relation: authtypes.Relation{Verb: coretypes.VerbAttach}, Resource: coretypes.ResourceServiceAccount, SelectorCallback: serviceAccountInstanceSelectorCallback, Roles: []string{
authtypes.SigNozAdminRoleName,
}}},
{{Relation: authtypes.Relation{Verb: coretypes.VerbAttach}, Resource: coretypes.ResourceRole, SelectorCallback: provider.roleAttachSelectorFromBody, Roles: []string{
authtypes.SigNozAdminRoleName,
}}},
}), handler.OpenAPIDef{
ID: "CreateServiceAccountRole",
Tags: []string{"serviceaccount"},
Summary: "Create service account role",
@@ -108,12 +129,19 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbAttach), coretypes.ResourceRole.Scope(coretypes.VerbAttach)}),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}/roles/{rid}", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.DeleteRole), handler.OpenAPIDef{
if err := router.Handle("/api/v1/service_accounts/{id}/roles/{rid}", handler.New(provider.authzMiddleware.CheckAll(provider.serviceAccountHandler.DeleteRole, []middleware.AuthZCheckGroup{
{{Relation: authtypes.Relation{Verb: coretypes.VerbAttach}, Resource: coretypes.ResourceServiceAccount, SelectorCallback: serviceAccountInstanceSelectorCallback, Roles: []string{
authtypes.SigNozAdminRoleName,
}}},
{{Relation: authtypes.Relation{Verb: coretypes.VerbAttach}, Resource: coretypes.ResourceRole, SelectorCallback: provider.roleAttachSelectorFromPath, Roles: []string{
authtypes.SigNozAdminRoleName,
}}},
}), handler.OpenAPIDef{
ID: "DeleteServiceAccountRole",
Tags: []string{"serviceaccount"},
Summary: "Delete service account role",
@@ -125,12 +153,12 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbAttach), coretypes.ResourceRole.Scope(coretypes.VerbAttach)}),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/me", handler.New(provider.authZ.OpenAccess(provider.serviceAccountHandler.UpdateMe), handler.OpenAPIDef{
if err := router.Handle("/api/v1/service_accounts/me", handler.New(provider.authzMiddleware.OpenAccess(provider.serviceAccountHandler.UpdateMe), handler.OpenAPIDef{
ID: "UpdateMyServiceAccount",
Tags: []string{"serviceaccount"},
Summary: "Updates my service account",
@@ -147,7 +175,9 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.Update), handler.OpenAPIDef{
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.Update, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "UpdateServiceAccount",
Tags: []string{"serviceaccount"},
Summary: "Updates a service account",
@@ -159,12 +189,14 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbUpdate)}),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.Delete), handler.OpenAPIDef{
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.Delete, authtypes.Relation{Verb: coretypes.VerbDelete}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "DeleteServiceAccount",
Tags: []string{"serviceaccount"},
Summary: "Deletes a service account",
@@ -176,12 +208,14 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbDelete)}),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}/keys", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.CreateFactorAPIKey), handler.OpenAPIDef{
if err := router.Handle("/api/v1/service_accounts/{id}/keys", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.CreateFactorAPIKey, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "CreateServiceAccountKey",
Tags: []string{"serviceaccount"},
Summary: "Create a service account key",
@@ -193,12 +227,14 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbUpdate)}),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}/keys", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.ListFactorAPIKey), handler.OpenAPIDef{
if err := router.Handle("/api/v1/service_accounts/{id}/keys", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.ListFactorAPIKey, authtypes.Relation{Verb: coretypes.VerbRead}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "ListServiceAccountKeys",
Tags: []string{"serviceaccount"},
Summary: "List service account keys",
@@ -210,12 +246,14 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbRead)}),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}/keys/{fid}", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.UpdateFactorAPIKey), handler.OpenAPIDef{
if err := router.Handle("/api/v1/service_accounts/{id}/keys/{fid}", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.UpdateFactorAPIKey, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "UpdateServiceAccountKey",
Tags: []string{"serviceaccount"},
Summary: "Updates a service account key",
@@ -227,12 +265,14 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbUpdate)}),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}/keys/{fid}", handler.New(provider.authZ.AdminAccess(provider.serviceAccountHandler.RevokeFactorAPIKey), handler.OpenAPIDef{
if err := router.Handle("/api/v1/service_accounts/{id}/keys/{fid}", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.RevokeFactorAPIKey, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "RevokeServiceAccountKey",
Tags: []string{"serviceaccount"},
Summary: "Revoke a service account key",
@@ -244,10 +284,70 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbUpdate)}),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
return nil
}
func (provider *provider) roleAttachSelectorFromPath(req *http.Request, claims authtypes.Claims) ([]coretypes.Selector, error) {
roleID, err := valuer.NewUUID(mux.Vars(req)["rid"])
if err != nil {
return nil, err
}
role, err := provider.authzService.Get(req.Context(), valuer.MustNewUUID(claims.OrgID), roleID)
if err != nil {
return nil, err
}
return []coretypes.Selector{
coretypes.TypeRole.MustSelector(role.Name),
coretypes.TypeRole.MustSelector(coretypes.WildCardSelectorString),
}, nil
}
func (provider *provider) roleAttachSelectorFromBody(req *http.Request, claims authtypes.Claims) ([]coretypes.Selector, error) {
body, err := io.ReadAll(req.Body)
if err != nil {
return nil, err
}
req.Body = io.NopCloser(bytes.NewReader(body))
postableRole := new(serviceaccounttypes.PostableServiceAccountRole)
if err := json.Unmarshal(body, postableRole); err != nil {
return nil, err
}
role, err := provider.authzService.Get(req.Context(), valuer.MustNewUUID(claims.OrgID), postableRole.ID)
if err != nil {
return nil, err
}
return []coretypes.Selector{
coretypes.TypeRole.MustSelector(role.Name),
coretypes.TypeRole.MustSelector(coretypes.WildCardSelectorString),
}, nil
}
func serviceAccountCollectionSelectorCallback(_ *http.Request, _ authtypes.Claims) ([]coretypes.Selector, error) {
return []coretypes.Selector{
coretypes.TypeMetaResources.MustSelector(coretypes.WildCardSelectorString),
}, nil
}
func serviceAccountInstanceSelectorCallback(req *http.Request, _ authtypes.Claims) ([]coretypes.Selector, error) {
id := mux.Vars(req)["id"]
idSelector, err := coretypes.TypeServiceAccount.Selector(id)
if err != nil {
return nil, err
}
return []coretypes.Selector{
idSelector,
coretypes.TypeServiceAccount.MustSelector(coretypes.WildCardSelectorString),
}, nil
}

View File

@@ -9,7 +9,7 @@ import (
)
func (provider *provider) addSessionRoutes(router *mux.Router) error {
if err := router.Handle("/api/v2/sessions/email_password", handler.New(provider.authZ.OpenAccess(provider.sessionHandler.CreateSessionByEmailPassword), handler.OpenAPIDef{
if err := router.Handle("/api/v2/sessions/email_password", handler.New(provider.authzMiddleware.OpenAccess(provider.sessionHandler.CreateSessionByEmailPassword), handler.OpenAPIDef{
ID: "CreateSessionByEmailPassword",
Tags: []string{"sessions"},
Summary: "Create session by email and password",
@@ -26,7 +26,7 @@ func (provider *provider) addSessionRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/sessions/context", handler.New(provider.authZ.OpenAccess(provider.sessionHandler.GetSessionContext), handler.OpenAPIDef{
if err := router.Handle("/api/v2/sessions/context", handler.New(provider.authzMiddleware.OpenAccess(provider.sessionHandler.GetSessionContext), handler.OpenAPIDef{
ID: "GetSessionContext",
Tags: []string{"sessions"},
Summary: "Get session context",
@@ -43,7 +43,7 @@ func (provider *provider) addSessionRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/sessions/rotate", handler.New(provider.authZ.OpenAccess(provider.sessionHandler.RotateSession), handler.OpenAPIDef{
if err := router.Handle("/api/v2/sessions/rotate", handler.New(provider.authzMiddleware.OpenAccess(provider.sessionHandler.RotateSession), handler.OpenAPIDef{
ID: "RotateSession",
Tags: []string{"sessions"},
Summary: "Rotate session",
@@ -60,7 +60,7 @@ func (provider *provider) addSessionRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/sessions", handler.New(provider.authZ.OpenAccess(provider.sessionHandler.DeleteSession), handler.OpenAPIDef{
if err := router.Handle("/api/v2/sessions", handler.New(provider.authzMiddleware.OpenAccess(provider.sessionHandler.DeleteSession), handler.OpenAPIDef{
ID: "DeleteSession",
Tags: []string{"sessions"},
Summary: "Delete session",
@@ -77,7 +77,7 @@ func (provider *provider) addSessionRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/complete/google", handler.New(provider.authZ.OpenAccess(provider.sessionHandler.CreateSessionByGoogleCallback), handler.OpenAPIDef{
if err := router.Handle("/api/v1/complete/google", handler.New(provider.authzMiddleware.OpenAccess(provider.sessionHandler.CreateSessionByGoogleCallback), handler.OpenAPIDef{
ID: "CreateSessionByGoogleCallback",
Tags: []string{"sessions"},
Summary: "Create session by google callback",
@@ -94,7 +94,7 @@ func (provider *provider) addSessionRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/complete/saml", handler.New(provider.authZ.OpenAccess(provider.sessionHandler.CreateSessionBySAMLCallback), handler.OpenAPIDef{
if err := router.Handle("/api/v1/complete/saml", handler.New(provider.authzMiddleware.OpenAccess(provider.sessionHandler.CreateSessionBySAMLCallback), handler.OpenAPIDef{
ID: "CreateSessionBySAMLCallback",
Tags: []string{"sessions"},
Summary: "Create session by saml callback",
@@ -114,7 +114,7 @@ func (provider *provider) addSessionRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/complete/oidc", handler.New(provider.authZ.OpenAccess(provider.sessionHandler.CreateSessionByOIDCCallback), handler.OpenAPIDef{
if err := router.Handle("/api/v1/complete/oidc", handler.New(provider.authzMiddleware.OpenAccess(provider.sessionHandler.CreateSessionByOIDCCallback), handler.OpenAPIDef{
ID: "CreateSessionByOIDCCallback",
Tags: []string{"sessions"},
Summary: "Create session by oidc callback",

View File

@@ -11,7 +11,7 @@ import (
func (provider *provider) addSpanMapperRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/span_mapper_groups", handler.New(
provider.authZ.ViewAccess(provider.spanMapperHandler.ListGroups),
provider.authzMiddleware.ViewAccess(provider.spanMapperHandler.ListGroups),
handler.OpenAPIDef{
ID: "ListSpanMapperGroups",
Tags: []string{"spanmapper"},
@@ -32,7 +32,7 @@ func (provider *provider) addSpanMapperRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v1/span_mapper_groups", handler.New(
provider.authZ.AdminAccess(provider.spanMapperHandler.CreateGroup),
provider.authzMiddleware.AdminAccess(provider.spanMapperHandler.CreateGroup),
handler.OpenAPIDef{
ID: "CreateSpanMapperGroup",
Tags: []string{"spanmapper"},
@@ -52,7 +52,7 @@ func (provider *provider) addSpanMapperRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v1/span_mapper_groups/{groupId}", handler.New(
provider.authZ.AdminAccess(provider.spanMapperHandler.UpdateGroup),
provider.authzMiddleware.AdminAccess(provider.spanMapperHandler.UpdateGroup),
handler.OpenAPIDef{
ID: "UpdateSpanMapperGroup",
Tags: []string{"spanmapper"},
@@ -70,7 +70,7 @@ func (provider *provider) addSpanMapperRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v1/span_mapper_groups/{groupId}", handler.New(
provider.authZ.AdminAccess(provider.spanMapperHandler.DeleteGroup),
provider.authzMiddleware.AdminAccess(provider.spanMapperHandler.DeleteGroup),
handler.OpenAPIDef{
ID: "DeleteSpanMapperGroup",
Tags: []string{"spanmapper"},
@@ -90,7 +90,7 @@ func (provider *provider) addSpanMapperRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v1/span_mapper_groups/{groupId}/span_mappers", handler.New(
provider.authZ.ViewAccess(provider.spanMapperHandler.ListMappers),
provider.authzMiddleware.ViewAccess(provider.spanMapperHandler.ListMappers),
handler.OpenAPIDef{
ID: "ListSpanMappers",
Tags: []string{"spanmapper"},
@@ -110,7 +110,7 @@ func (provider *provider) addSpanMapperRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v1/span_mapper_groups/{groupId}/span_mappers", handler.New(
provider.authZ.AdminAccess(provider.spanMapperHandler.CreateMapper),
provider.authzMiddleware.AdminAccess(provider.spanMapperHandler.CreateMapper),
handler.OpenAPIDef{
ID: "CreateSpanMapper",
Tags: []string{"spanmapper"},
@@ -130,7 +130,7 @@ func (provider *provider) addSpanMapperRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v1/span_mapper_groups/{groupId}/span_mappers/{mapperId}", handler.New(
provider.authZ.AdminAccess(provider.spanMapperHandler.UpdateMapper),
provider.authzMiddleware.AdminAccess(provider.spanMapperHandler.UpdateMapper),
handler.OpenAPIDef{
ID: "UpdateSpanMapper",
Tags: []string{"spanmapper"},
@@ -148,7 +148,7 @@ func (provider *provider) addSpanMapperRoutes(router *mux.Router) error {
}
if err := router.Handle("/api/v1/span_mapper_groups/{groupId}/span_mappers/{mapperId}", handler.New(
provider.authZ.AdminAccess(provider.spanMapperHandler.DeleteMapper),
provider.authzMiddleware.AdminAccess(provider.spanMapperHandler.DeleteMapper),
handler.OpenAPIDef{
ID: "DeleteSpanMapper",
Tags: []string{"spanmapper"},

View File

@@ -11,7 +11,7 @@ import (
func (provider *provider) addTraceDetailRoutes(router *mux.Router) error {
if err := router.Handle("/api/v3/traces/{traceID}/waterfall", handler.New(
provider.authZ.ViewAccess(provider.traceDetailHandler.GetWaterfall),
provider.authzMiddleware.ViewAccess(provider.traceDetailHandler.GetWaterfall),
handler.OpenAPIDef{
ID: "GetWaterfall",
Tags: []string{"tracedetail"},

View File

@@ -10,7 +10,7 @@ import (
)
func (provider *provider) addUserRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/invite", handler.New(provider.authZ.AdminAccess(provider.userHandler.CreateInvite), handler.OpenAPIDef{
if err := router.Handle("/api/v1/invite", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.CreateInvite), handler.OpenAPIDef{
ID: "CreateInvite",
Tags: []string{"users"},
Summary: "Create invite",
@@ -27,7 +27,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/invite/bulk", handler.New(provider.authZ.AdminAccess(provider.userHandler.CreateBulkInvite), handler.OpenAPIDef{
if err := router.Handle("/api/v1/invite/bulk", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.CreateBulkInvite), handler.OpenAPIDef{
ID: "CreateBulkInvite",
Tags: []string{"users"},
Summary: "Create bulk invite",
@@ -43,7 +43,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/user", handler.New(provider.authZ.AdminAccess(provider.userHandler.ListUsersDeprecated), handler.OpenAPIDef{
if err := router.Handle("/api/v1/user", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.ListUsersDeprecated), handler.OpenAPIDef{
ID: "ListUsersDeprecated",
Tags: []string{"users"},
Summary: "List users",
@@ -60,7 +60,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/users", handler.New(provider.authZ.AdminAccess(provider.userHandler.ListUsers), handler.OpenAPIDef{
if err := router.Handle("/api/v2/users", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.ListUsers), handler.OpenAPIDef{
ID: "ListUsers",
Tags: []string{"users"},
Summary: "List users v2",
@@ -77,7 +77,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/user/me", handler.New(provider.authZ.OpenAccess(provider.userHandler.GetMyUserDeprecated), handler.OpenAPIDef{
if err := router.Handle("/api/v1/user/me", handler.New(provider.authzMiddleware.OpenAccess(provider.userHandler.GetMyUserDeprecated), handler.OpenAPIDef{
ID: "GetMyUserDeprecated",
Tags: []string{"users"},
Summary: "Get my user",
@@ -94,7 +94,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/users/me", handler.New(provider.authZ.OpenAccess(provider.userHandler.GetMyUser), handler.OpenAPIDef{
if err := router.Handle("/api/v2/users/me", handler.New(provider.authzMiddleware.OpenAccess(provider.userHandler.GetMyUser), handler.OpenAPIDef{
ID: "GetMyUser",
Tags: []string{"users"},
Summary: "Get my user v2",
@@ -111,7 +111,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/users/me", handler.New(provider.authZ.OpenAccess(provider.userHandler.UpdateMyUser), handler.OpenAPIDef{
if err := router.Handle("/api/v2/users/me", handler.New(provider.authzMiddleware.OpenAccess(provider.userHandler.UpdateMyUser), handler.OpenAPIDef{
ID: "UpdateMyUserV2",
Tags: []string{"users"},
Summary: "Update my user v2",
@@ -128,7 +128,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/user/{id}", handler.New(provider.authZ.SelfAccess(provider.userHandler.GetUserDeprecated), handler.OpenAPIDef{
if err := router.Handle("/api/v1/user/{id}", handler.New(provider.authzMiddleware.SelfAccess(provider.userHandler.GetUserDeprecated), handler.OpenAPIDef{
ID: "GetUserDeprecated",
Tags: []string{"users"},
Summary: "Get user",
@@ -145,7 +145,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/users/{id}", handler.New(provider.authZ.AdminAccess(provider.userHandler.GetUser), handler.OpenAPIDef{
if err := router.Handle("/api/v2/users/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.GetUser), handler.OpenAPIDef{
ID: "GetUser",
Tags: []string{"users"},
Summary: "Get user by user id",
@@ -162,7 +162,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/user/{id}", handler.New(provider.authZ.SelfAccess(provider.userHandler.UpdateUserDeprecated), handler.OpenAPIDef{
if err := router.Handle("/api/v1/user/{id}", handler.New(provider.authzMiddleware.SelfAccess(provider.userHandler.UpdateUserDeprecated), handler.OpenAPIDef{
ID: "UpdateUserDeprecated",
Tags: []string{"users"},
Summary: "Update user",
@@ -179,7 +179,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/users/{id}", handler.New(provider.authZ.AdminAccess(provider.userHandler.UpdateUser), handler.OpenAPIDef{
if err := router.Handle("/api/v2/users/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.UpdateUser), handler.OpenAPIDef{
ID: "UpdateUser",
Tags: []string{"users"},
Summary: "Update user v2",
@@ -196,7 +196,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/user/{id}", handler.New(provider.authZ.AdminAccess(provider.userHandler.DeleteUser), handler.OpenAPIDef{
if err := router.Handle("/api/v1/user/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.DeleteUser), handler.OpenAPIDef{
ID: "DeleteUser",
Tags: []string{"users"},
Summary: "Delete user",
@@ -213,7 +213,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/getResetPasswordToken/{id}", handler.New(provider.authZ.AdminAccess(provider.userHandler.GetResetPasswordTokenDeprecated), handler.OpenAPIDef{
if err := router.Handle("/api/v1/getResetPasswordToken/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.GetResetPasswordTokenDeprecated), handler.OpenAPIDef{
ID: "GetResetPasswordTokenDeprecated",
Tags: []string{"users"},
Summary: "Get reset password token",
@@ -230,7 +230,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/users/{id}/reset_password_tokens", handler.New(provider.authZ.AdminAccess(provider.userHandler.GetResetPasswordToken), handler.OpenAPIDef{
if err := router.Handle("/api/v2/users/{id}/reset_password_tokens", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.GetResetPasswordToken), handler.OpenAPIDef{
ID: "GetResetPasswordToken",
Tags: []string{"users"},
Summary: "Get reset password token for a user",
@@ -247,7 +247,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/users/{id}/reset_password_tokens", handler.New(provider.authZ.AdminAccess(provider.userHandler.CreateResetPasswordToken), handler.OpenAPIDef{
if err := router.Handle("/api/v2/users/{id}/reset_password_tokens", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.CreateResetPasswordToken), handler.OpenAPIDef{
ID: "CreateResetPasswordToken",
Tags: []string{"users"},
Summary: "Create or regenerate reset password token for a user",
@@ -264,7 +264,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/resetPassword", handler.New(provider.authZ.OpenAccess(provider.userHandler.ResetPassword), handler.OpenAPIDef{
if err := router.Handle("/api/v1/resetPassword", handler.New(provider.authzMiddleware.OpenAccess(provider.userHandler.ResetPassword), handler.OpenAPIDef{
ID: "ResetPassword",
Tags: []string{"users"},
Summary: "Reset password",
@@ -281,7 +281,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/users/me/factor_password", handler.New(provider.authZ.OpenAccess(provider.userHandler.ChangePassword), handler.OpenAPIDef{
if err := router.Handle("/api/v2/users/me/factor_password", handler.New(provider.authzMiddleware.OpenAccess(provider.userHandler.ChangePassword), handler.OpenAPIDef{
ID: "UpdateMyPassword",
Tags: []string{"users"},
Summary: "Updates my password",
@@ -298,7 +298,7 @@ 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{
if err := router.Handle("/api/v2/factor_password/forgot", handler.New(provider.authzMiddleware.OpenAccess(provider.userHandler.ForgotPassword), handler.OpenAPIDef{
ID: "ForgotPassword",
Tags: []string{"users"},
Summary: "Forgot password",
@@ -315,7 +315,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/users/{id}/roles", handler.New(provider.authZ.AdminAccess(provider.userHandler.GetRolesByUserID), handler.OpenAPIDef{
if err := router.Handle("/api/v2/users/{id}/roles", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.GetRolesByUserID), handler.OpenAPIDef{
ID: "GetRolesByUserID",
Tags: []string{"users"},
Summary: "Get user roles",
@@ -332,7 +332,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/users/{id}/roles", handler.New(provider.authZ.AdminAccess(provider.userHandler.SetRoleByUserID), handler.OpenAPIDef{
if err := router.Handle("/api/v2/users/{id}/roles", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.SetRoleByUserID), handler.OpenAPIDef{
ID: "SetRoleByUserID",
Tags: []string{"users"},
Summary: "Set user roles",
@@ -349,7 +349,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/users/{id}/roles/{roleId}", handler.New(provider.authZ.AdminAccess(provider.userHandler.RemoveUserRoleByRoleID), handler.OpenAPIDef{
if err := router.Handle("/api/v2/users/{id}/roles/{roleId}", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.RemoveUserRoleByRoleID), handler.OpenAPIDef{
ID: "RemoveUserRoleByUserIDAndRoleID",
Tags: []string{"users"},
Summary: "Remove a role from user",
@@ -366,7 +366,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/roles/{id}/users", handler.New(provider.authZ.AdminAccess(provider.userHandler.GetUsersByRoleID), handler.OpenAPIDef{
if err := router.Handle("/api/v2/roles/{id}/users", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.GetUsersByRoleID), handler.OpenAPIDef{
ID: "GetUsersByRoleID",
Tags: []string{"users"},
Summary: "Get users by role id",

View File

@@ -10,7 +10,7 @@ import (
)
func (provider *provider) addZeusRoutes(router *mux.Router) error {
if err := router.Handle("/api/v2/zeus/profiles", handler.New(provider.authZ.AdminAccess(provider.zeusHandler.PutProfile), handler.OpenAPIDef{
if err := router.Handle("/api/v2/zeus/profiles", handler.New(provider.authzMiddleware.AdminAccess(provider.zeusHandler.PutProfile), handler.OpenAPIDef{
ID: "PutProfile",
Tags: []string{"zeus"},
Summary: "Put profile in Zeus for a deployment.",
@@ -27,7 +27,7 @@ func (provider *provider) addZeusRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/zeus/hosts", handler.New(provider.authZ.ViewAccess(provider.zeusHandler.GetHosts), handler.OpenAPIDef{
if err := router.Handle("/api/v2/zeus/hosts", handler.New(provider.authzMiddleware.ViewAccess(provider.zeusHandler.GetHosts), handler.OpenAPIDef{
ID: "GetHosts",
Tags: []string{"zeus"},
Summary: "Get host info from Zeus.",
@@ -44,7 +44,7 @@ func (provider *provider) addZeusRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/zeus/hosts", handler.New(provider.authZ.AdminAccess(provider.zeusHandler.PutHost), handler.OpenAPIDef{
if err := router.Handle("/api/v2/zeus/hosts", handler.New(provider.authzMiddleware.AdminAccess(provider.zeusHandler.PutHost), handler.OpenAPIDef{
ID: "PutHost",
Tags: []string{"zeus"},
Summary: "Put host in Zeus for a deployment.",

View File

@@ -8,6 +8,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -18,6 +19,20 @@ const (
authzDeniedMessage string = "::AUTHZ-DENIED::"
)
type AuthZCheckDef struct {
Relation authtypes.Relation
Resource coretypes.Resource
SelectorCallback selectorCallbackWithClaimsFn
Roles []string
}
// AuthZCheckGroup is a set of checks OR'd together.
// At least one check in the group must pass for the group to pass.
type AuthZCheckGroup []AuthZCheckDef
type selectorCallbackWithClaimsFn func(*http.Request, authtypes.Claims) ([]coretypes.Selector, error)
type selectorCallbackWithoutClaimsFn func(*http.Request, []*types.Organization) ([]coretypes.Selector, valuer.UUID, error)
type AuthZ struct {
logger *slog.Logger
orgGetter organization.Getter
@@ -186,7 +201,7 @@ func (middleware *AuthZ) OpenAccess(next http.HandlerFunc) http.HandlerFunc {
})
}
func (middleware *AuthZ) Check(next http.HandlerFunc, relation authtypes.Relation, typeable coretypes.Resource, cb authtypes.SelectorCallbackWithClaimsFn, roles []string) http.HandlerFunc {
func (middleware *AuthZ) Check(next http.HandlerFunc, relation authtypes.Relation, typeable coretypes.Resource, cb selectorCallbackWithClaimsFn, roles []string) http.HandlerFunc {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
@@ -216,7 +231,61 @@ func (middleware *AuthZ) Check(next http.HandlerFunc, relation authtypes.Relatio
})
}
func (middleware *AuthZ) CheckWithoutClaims(next http.HandlerFunc, relation authtypes.Relation, typeable coretypes.Resource, cb authtypes.SelectorCallbackWithoutClaimsFn, roles []string) http.HandlerFunc {
// CheckAll verifies groups of permission checks.
// Within each group, checks are OR'd (any check passing = group passes).
// Across groups, results are AND'd (all groups must pass).
//
// This model expresses any combination:
// - Single check: []AuthZCheckGroup{{checkA}}
// - Pure AND: []AuthZCheckGroup{{checkA}, {checkB}}
// - Cross-resource OR: []AuthZCheckGroup{{checkA, checkB}}
// - Mixed (A OR B) AND C: []AuthZCheckGroup{{checkA, checkB}, {checkC}}
func (middleware *AuthZ) CheckAll(next http.HandlerFunc, groups []AuthZCheckGroup) http.HandlerFunc {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
for _, group := range groups {
groupPassed := false
var lastErr error
for _, check := range group {
selectors, err := check.SelectorCallback(req, claims)
if err != nil {
render.Error(rw, err)
return
}
roleSelectors := make([]coretypes.Selector, len(check.Roles))
for idx, role := range check.Roles {
roleSelectors[idx] = coretypes.TypeRole.MustSelector(role)
}
err = middleware.authzService.CheckWithTupleCreation(ctx, claims, orgID, check.Relation, check.Resource, selectors, roleSelectors)
if err == nil {
groupPassed = true
break
}
lastErr = err
}
if !groupPassed {
render.Error(rw, lastErr)
return
}
}
next(rw, req)
})
}
func (middleware *AuthZ) CheckWithoutClaims(next http.HandlerFunc, relation authtypes.Relation, typeable coretypes.Resource, cb selectorCallbackWithoutClaimsFn, roles []string) http.HandlerFunc {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
orgs, err := middleware.orgGetter.ListByOwnedKeyRange(ctx)

View File

@@ -44,6 +44,8 @@ import (
"gopkg.in/yaml.v2"
)
const signozDiscriminatorKey string = "x-signoz-discriminator"
type OpenAPI struct {
apiserver apiserver.APIServer
reflector *openapi3.Reflector
@@ -142,6 +144,8 @@ func (openapi *OpenAPI) CreateAndWrite(path string) error {
return err
}
attachDiscriminators(openapi.reflector.Spec)
// The library's MarshalYAML does a JSON round-trip that converts all numbers
// to float64, causing large integers (e.g. epoch millisecond timestamps) to
// render in scientific notation (1.6409952e+12).
@@ -199,3 +203,59 @@ func convertJSONNumbers(v interface{}) {
}
}
}
// attachDiscriminators promotes x-signoz-discriminator extensions
// into openapi3 Discriminator fields. Malformed markers are dropped.
func attachDiscriminators(spec *openapi3.Spec) {
if spec.Components == nil || spec.Components.Schemas == nil {
return
}
for name, entry := range spec.Components.Schemas.MapOfSchemaOrRefValues {
if entry.Schema == nil {
continue
}
raw, ok := entry.Schema.MapOfAnything[signozDiscriminatorKey]
if !ok {
continue
}
marker, ok := raw.(map[string]any)
if !ok {
continue
}
propertyName, ok := marker["propertyName"].(string)
if !ok || propertyName == "" {
continue
}
disc := openapi3.Discriminator{PropertyName: propertyName}
if rawMapping, ok := marker["mapping"]; ok {
if mapping, ok := rawMapping.(map[string]string); ok {
disc.Mapping = mapping
} else if mapping, ok := rawMapping.(map[string]any); ok {
converted := make(map[string]string, len(mapping))
for k, v := range mapping {
if s, ok := v.(string); ok {
converted[k] = s
}
}
disc.Mapping = converted
}
}
entry.Schema.Discriminator = &disc
delete(entry.Schema.MapOfAnything, signozDiscriminatorKey)
// The parent's reflected `properties` / `required` duplicate
// what the oneOf variants already declare, and orval intersects
// the two — turning a clean discriminated union DTO into a
// noisy union of intersections. Drop them here.
entry.Schema.Properties = nil
entry.Schema.Required = nil
spec.Components.Schemas.MapOfSchemaOrRefValues[name] = entry
}
}

View File

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

View File

@@ -0,0 +1,150 @@
package sqlmigration
import (
"context"
"time"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/oklog/ulid/v2"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
"github.com/uptrace/bun/migrate"
)
type addServiceAccountManagedRoleTransactions struct {
sqlstore sqlstore.SQLStore
}
func NewAddServiceAccountManagedRoleTransactionsFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("add_sa_managed_role_txn"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &addServiceAccountManagedRoleTransactions{sqlstore: sqlstore}, nil
})
}
func (migration *addServiceAccountManagedRoleTransactions) Register(migrations *migrate.Migrations) error {
return migrations.Register(migration.Up, migration.Down)
}
// managedRoleTuple describes a single FGA tuple to insert for a managed role.
type managedRoleTuple struct {
roleName string
objectType string // "metaresources" or "metaresource"
objectName string // "service-accounts" or "service-account"
relation string // "create", "list", "read", "update", "delete"
}
func (migration *addServiceAccountManagedRoleTransactions) Up(ctx context.Context, db *bun.DB) error {
// All tuples that need to be created for service account FGA managed role permissions.
tuples := []managedRoleTuple{
{authtypes.SigNozAdminRoleName, "role", "role", "attach"},
{authtypes.SigNozAdminRoleName, "serviceaccount", "serviceaccount", "attach"},
{authtypes.SigNozAdminRoleName, "metaresources", "serviceaccount", "create"},
{authtypes.SigNozAdminRoleName, "metaresources", "serviceaccount", "list"},
{authtypes.SigNozAdminRoleName, "serviceaccount", "serviceaccount", "read"},
{authtypes.SigNozAdminRoleName, "serviceaccount", "serviceaccount", "update"},
{authtypes.SigNozAdminRoleName, "serviceaccount", "serviceaccount", "delete"},
}
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
var storeID string
err = tx.QueryRowContext(ctx, `SELECT id FROM store WHERE name = ? LIMIT 1`, "signoz").Scan(&storeID)
if err != nil {
return err
}
// Fetch all orgs.
var orgIDs []string
rows, err := tx.QueryContext(ctx, `SELECT id FROM organizations`)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var orgID string
if err := rows.Scan(&orgID); err != nil {
return err
}
orgIDs = append(orgIDs, orgID)
}
isPG := migration.sqlstore.BunDB().Dialect().Name() == dialect.PG
for _, orgID := range orgIDs {
for _, tuple := range tuples {
entropy := ulid.DefaultEntropy()
now := time.Now().UTC()
tupleID := ulid.MustNew(ulid.Timestamp(now), entropy).String()
objectID := "organization/" + orgID + "/" + tuple.objectName + "/*"
roleSubject := "organization/" + orgID + "/role/" + tuple.roleName
if isPG {
user := "role:" + roleSubject + "#assignee"
result, err := tx.ExecContext(ctx, `
INSERT INTO tuple (store, object_type, object_id, relation, _user, user_type, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, object_type, object_id, relation, _user) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, user, "userset", tupleID, now,
)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
continue
}
_, err = tx.ExecContext(ctx, `
INSERT INTO changelog (store, object_type, object_id, relation, _user, operation, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, user, "TUPLE_OPERATION_WRITE", tupleID, now,
)
if err != nil {
return err
}
} else {
result, err := tx.ExecContext(ctx, `
INSERT INTO tuple (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, user_type, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", "userset", tupleID, now,
)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
continue
}
_, err = tx.ExecContext(ctx, `
INSERT INTO changelog (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, operation, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", 0, tupleID, now,
)
if err != nil {
return err
}
}
}
}
return tx.Commit()
}
func (migration *addServiceAccountManagedRoleTransactions) Down(context.Context, *bun.DB) error {
return nil
}

View File

@@ -176,7 +176,7 @@ func GetAdditionTuples(name string, orgID valuer.UUID, relation Relation, additi
transactionTuples := NewTuples(
resource,
MustNewSubject(
resource,
coretypes.NewResourceRole(),
name,
orgID,
&coretypes.VerbAssignee,
@@ -200,7 +200,7 @@ func GetDeletionTuples(name string, orgID valuer.UUID, relation Relation, deleti
transactionTuples := NewTuples(
resource,
MustNewSubject(
resource,
coretypes.NewResourceRole(),
name,
orgID,
&coretypes.VerbAssignee,

View File

@@ -1,18 +1,12 @@
package authtypes
import (
"net/http"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/valuer"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
)
type SelectorCallbackWithClaimsFn func(*http.Request, Claims) ([]coretypes.Selector, error)
type SelectorCallbackWithoutClaimsFn func(*http.Request, []*types.Organization) ([]coretypes.Selector, valuer.UUID, error)
var (
ErrCodeAuthZUnavailable = errors.MustNewCode("authz_unavailable")
ErrCodeAuthZForbidden = errors.MustNewCode("authz_forbidden")

View File

@@ -9,7 +9,6 @@ var Resources = []Resource{
ResourceMetaResourcesRole,
ResourceMetaResourcesOrganization,
ResourceMetaResourcesServiceAccount,
ResourceMetaResourcesServiceAccount,
ResourceMetaResourcesUser,
ResourceMetaResourceNotificationChannel,
ResourceMetaResourcesNotificationChannel,

View File

@@ -28,6 +28,8 @@ func NewVerb(verb string) (Verb, error) {
return VerbList, nil
case "assignee":
return VerbAssignee, nil
case "attach":
return VerbAttach, nil
default:
return Verb{}, errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidVerb, "verb %s is invalid, valid verbs are: %s", verb, Verb{}.Enum())
}
@@ -41,6 +43,7 @@ func (Verb) Enum() []any {
VerbDelete,
VerbList,
VerbAssignee,
VerbAttach,
}
}

View File

@@ -1,262 +0,0 @@
package dashboardtypes
import (
"bytes"
"encoding/json"
"fmt"
"slices"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
qb "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/go-playground/validator/v10"
v1 "github.com/perses/perses/pkg/model/api/v1"
"github.com/perses/perses/pkg/model/api/v1/common"
"github.com/perses/perses/pkg/model/api/v1/dashboard"
)
// StorableDashboardDataV2 wraps v1.DashboardSpec (Perses) with additional SigNoz-specific fields.
//
// We embed DashboardSpec (not v1.Dashboard) to avoid carrying Perses's Metadata
// (Name, Project, CreatedAt, UpdatedAt, Tags, Version) and Kind field. SigNoz
// manages identity (ID), timestamps (TimeAuditable), and multi-tenancy (OrgID)
// separately on StorableDashboardV2/DashboardV2.
//
// The following v1 request fields map to locations inside v1.DashboardSpec:
// - title → Display.Name (common.Display)
// - description → Display.Description (common.Display)
//
// Fields that have no Perses equivalent will be added in this wrapper (like image, uploadGrafana, etc.)
type StorableDashboardDataV2 = v1.DashboardSpec
// UnmarshalAndValidateDashboardV2JSON unmarshals the JSON into a StorableDashboardDataV2
// (= PostableDashboardV2 = UpdatableDashboardV2) and validates plugin kinds and specs.
func UnmarshalAndValidateDashboardV2JSON(data []byte) (*StorableDashboardDataV2, error) {
var d StorableDashboardDataV2
// Note: DashboardSpec has a custom UnmarshalJSON which prevents
// DisallowUnknownFields from working at the top level. Unknown
// fields in plugin specs are still rejected by validateAndNormalizePluginSpec.
if err := json.Unmarshal(data, &d); err != nil {
return nil, err
}
if err := validateDashboardV2(d); err != nil {
return nil, err
}
return &d, nil
}
// Plugin kind → spec type factory. Each value is a pointer to the zero value of the
// expected spec struct. validatePluginSpec marshals plugin.Spec back to JSON and
// unmarshals into the typed struct to catch field-level errors.
var (
panelPluginSpecs = map[PanelPluginKind]func() any{
PanelKindTimeSeries: func() any { return new(TimeSeriesPanelSpec) },
PanelKindBarChart: func() any { return new(BarChartPanelSpec) },
PanelKindNumber: func() any { return new(NumberPanelSpec) },
PanelKindPieChart: func() any { return new(PieChartPanelSpec) },
PanelKindTable: func() any { return new(TablePanelSpec) },
PanelKindHistogram: func() any { return new(HistogramPanelSpec) },
PanelKindList: func() any { return new(ListPanelSpec) },
}
queryPluginSpecs = map[QueryPluginKind]func() any{
QueryKindBuilder: func() any { return new(BuilderQuerySpec) },
QueryKindComposite: func() any { return new(CompositeQuerySpec) },
QueryKindFormula: func() any { return new(FormulaSpec) },
QueryKindPromQL: func() any { return new(PromQLQuerySpec) },
QueryKindClickHouseSQL: func() any { return new(ClickHouseSQLQuerySpec) },
QueryKindTraceOperator: func() any { return new(TraceOperatorSpec) },
}
variablePluginSpecs = map[VariablePluginKind]func() any{
VariableKindDynamic: func() any { return new(DynamicVariableSpec) },
VariableKindQuery: func() any { return new(QueryVariableSpec) },
VariableKindCustom: func() any { return new(CustomVariableSpec) },
VariableKindTextbox: func() any { return new(TextboxVariableSpec) },
}
datasourcePluginSpecs = map[DatasourcePluginKind]func() any{
DatasourceKindSigNoz: func() any { return new(struct{}) },
}
// allowedQueryKinds maps each panel plugin kind to the query plugin
// kinds it supports. Composite sub-query types are mapped to these
// same kind strings via compositeSubQueryTypeToPluginKind.
allowedQueryKinds = map[PanelPluginKind][]QueryPluginKind{
PanelKindTimeSeries: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
PanelKindBarChart: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
PanelKindNumber: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
PanelKindHistogram: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
PanelKindPieChart: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindClickHouseSQL},
PanelKindTable: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindClickHouseSQL},
PanelKindList: {QueryKindBuilder},
}
// compositeSubQueryTypeToPluginKind maps CompositeQuery sub-query type
// strings to the equivalent top-level query plugin kind for validation.
compositeSubQueryTypeToPluginKind = map[qb.QueryType]QueryPluginKind{
qb.QueryTypeBuilder: QueryKindBuilder,
qb.QueryTypeFormula: QueryKindFormula,
qb.QueryTypeTraceOperator: QueryKindTraceOperator,
qb.QueryTypePromQL: QueryKindPromQL,
qb.QueryTypeClickHouseSQL: QueryKindClickHouseSQL,
}
)
func validateDashboardV2(d StorableDashboardDataV2) error {
for name, ds := range d.Datasources {
if err := validateDatasourcePlugin(&ds.Plugin, fmt.Sprintf("spec.datasources.%s.plugin", name)); err != nil {
return err
}
}
for i, v := range d.Variables {
if err := validateVariablePlugin(v, fmt.Sprintf("spec.variables[%d]", i)); err != nil {
return err
}
}
for key, panel := range d.Panels {
if panel == nil {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "spec.panels.%s: panel must not be null", key)
}
path := fmt.Sprintf("spec.panels.%s", key)
if err := validatePanelPlugin(&panel.Spec.Plugin, path+".spec.plugin"); err != nil {
return err
}
panelKind := PanelPluginKind(panel.Spec.Plugin.Kind)
allowed := allowedQueryKinds[panelKind]
for qi := range panel.Spec.Queries {
queryPath := fmt.Sprintf("%s.spec.queries[%d].spec.plugin", path, qi)
if err := validateQueryPlugin(&panel.Spec.Queries[qi].Spec.Plugin, queryPath); err != nil {
return err
}
if err := validateQueryAllowedForPanel(panel.Spec.Queries[qi].Spec.Plugin, allowed, panelKind, queryPath); err != nil {
return err
}
}
}
return nil
}
func validateDatasourcePlugin(plugin *common.Plugin, path string) error {
kind := DatasourcePluginKind(plugin.Kind)
factory, ok := datasourcePluginSpecs[kind]
if !ok {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput,
"%s: unknown datasource plugin kind %q; allowed values: %s", path, kind, formatEnum(kind.Enum()))
}
return validateAndNormalizePluginSpec(plugin, factory, path)
}
func validateVariablePlugin(v dashboard.Variable, path string) error {
switch spec := v.Spec.(type) {
case *dashboard.ListVariableSpec:
pluginPath := path + ".spec.plugin"
kind := VariablePluginKind(spec.Plugin.Kind)
factory, ok := variablePluginSpecs[kind]
if !ok {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput,
"%s: unknown variable plugin kind %q; allowed values: %s", pluginPath, kind, formatEnum(kind.Enum()))
}
return validateAndNormalizePluginSpec(&spec.Plugin, factory, pluginPath)
case *dashboard.TextVariableSpec:
// TextVariables have no plugin, nothing to validate.
return nil
default:
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "%s: unsupported variable kind %q", path, v.Kind)
}
}
func validatePanelPlugin(plugin *common.Plugin, path string) error {
kind := PanelPluginKind(plugin.Kind)
factory, ok := panelPluginSpecs[kind]
if !ok {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput,
"%s: unknown panel plugin kind %q; allowed values: %s", path, kind, formatEnum(kind.Enum()))
}
return validateAndNormalizePluginSpec(plugin, factory, path)
}
func validateQueryPlugin(plugin *common.Plugin, path string) error {
kind := QueryPluginKind(plugin.Kind)
factory, ok := queryPluginSpecs[kind]
if !ok {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput,
"%s: unknown query plugin kind %q; allowed values: %s", path, kind, formatEnum(kind.Enum()))
}
return validateAndNormalizePluginSpec(plugin, factory, path)
}
func formatEnum(values []any) string {
parts := make([]string, len(values))
for i, v := range values {
parts[i] = fmt.Sprintf("`%v`", v)
}
return strings.Join(parts, ", ")
}
// validateAndNormalizePluginSpec validates the plugin spec and writes the typed
// struct (with defaults) back into plugin.Spec so that DB storage and API
// responses contain normalized values.
func validateAndNormalizePluginSpec(plugin *common.Plugin, factory func() any, path string) error {
if plugin.Kind == "" {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "%s: plugin kind is required", path)
}
if plugin.Spec == nil {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "%s: plugin spec is required", path)
}
// Re-marshal the spec and unmarshal into the typed struct.
specJSON, err := json.Marshal(plugin.Spec)
if err != nil {
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "%s.spec", path)
}
target := factory()
decoder := json.NewDecoder(bytes.NewReader(specJSON))
decoder.DisallowUnknownFields()
if err := decoder.Decode(target); err != nil {
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "%s.spec", path)
}
if err := validator.New().Struct(target); err != nil {
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "%s.spec", path)
}
// Write the typed struct back so defaults are included.
plugin.Spec = target
return nil
}
// validateQueryAllowedForPanel checks that the query plugin kind is permitted
// for the given panel. For composite queries it recurses into sub-queries.
func validateQueryAllowedForPanel(plugin common.Plugin, allowed []QueryPluginKind, panelKind PanelPluginKind, path string) error {
queryKind := QueryPluginKind(plugin.Kind)
if !slices.Contains(allowed, queryKind) {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput,
"%s: query kind %q is not supported by panel kind %q", path, queryKind, panelKind)
}
// For composite queries, validate each sub-query type.
if queryKind == QueryKindComposite && plugin.Spec != nil {
specJSON, err := json.Marshal(plugin.Spec)
if err != nil {
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "%s.spec", path)
}
var composite struct {
Queries []struct {
Type qb.QueryType `json:"type"`
} `json:"queries"`
}
if err := json.Unmarshal(specJSON, &composite); err != nil {
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "%s.spec", path)
}
for si, sub := range composite.Queries {
pluginKind, ok := compositeSubQueryTypeToPluginKind[sub.Type]
if !ok {
continue
}
if !slices.Contains(allowed, pluginKind) {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput,
"%s.spec.queries[%d]: sub-query type %q is not supported by panel kind %q",
path, si, sub.Type, panelKind)
}
}
}
return nil
}

View File

@@ -0,0 +1,107 @@
package dashboardtypes
import (
"bytes"
"encoding/json"
"fmt"
"slices"
"github.com/SigNoz/signoz/pkg/errors"
qb "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
v1 "github.com/perses/perses/pkg/model/api/v1"
"github.com/perses/perses/pkg/model/api/v1/common"
)
// DashboardData is the SigNoz dashboard v2 spec shape. It mirrors
// v1.DashboardSpec (Perses) field-for-field, except every common.Plugin
// occurrence is replaced with a typed SigNoz plugin whose OpenAPI schema is a
// per-site discriminated oneOf.
type DashboardData struct {
Display *common.Display `json:"display,omitempty"`
Datasources map[string]*DatasourceSpec `json:"datasources,omitempty"`
Variables []Variable `json:"variables,omitempty"`
Panels map[string]*Panel `json:"panels"`
Layouts []Layout `json:"layouts"`
Duration common.DurationString `json:"duration"`
RefreshInterval common.DurationString `json:"refreshInterval,omitempty"`
Links []v1.Link `json:"links,omitempty"`
}
// ══════════════════════════════════════════════
// Unmarshal + validate entry point
// ══════════════════════════════════════════════
func (d *DashboardData) UnmarshalJSON(data []byte) error {
dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields()
type alias DashboardData
var tmp alias
if err := dec.Decode(&tmp); err != nil {
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid dashboard spec")
}
*d = DashboardData(tmp)
return d.Validate()
}
// ══════════════════════════════════════════════
// Cross-field validation
// ══════════════════════════════════════════════
func (d *DashboardData) Validate() error {
for key, panel := range d.Panels {
if panel == nil {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "spec.panels.%s: panel must not be null", key)
}
path := fmt.Sprintf("spec.panels.%s", key)
panelKind := panel.Spec.Plugin.Kind
if len(panel.Spec.Queries) != 1 {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "%s.spec.queries: panel must have one query", path)
}
allowed := allowedQueryKinds[panelKind]
for qi, q := range panel.Spec.Queries {
queryPath := fmt.Sprintf("%s.spec.queries[%d].spec.plugin", path, qi)
if err := validateQueryAllowedForPanel(q.Spec.Plugin, allowed, panelKind, queryPath); err != nil {
return err
}
}
}
return nil
}
func validateQueryAllowedForPanel(plugin QueryPlugin, allowed []QueryPluginKind, panelKind PanelPluginKind, path string) error {
if !slices.Contains(allowed, plugin.Kind) {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput,
"%s: query kind %q is not supported by panel kind %q", path, plugin.Kind, panelKind)
}
if plugin.Kind != QueryKindComposite {
return nil
}
composite, ok := plugin.Spec.(*CompositeQuerySpec)
if !ok || composite == nil {
// Unreachable via UnmarshalJSON; reaching here means a Go caller broke the Kind/Spec pairing.
return errors.NewInternalf(errors.CodeInternal, "%s: composite query plugin has unexpected spec type %T", path, plugin.Spec)
}
for si, sub := range composite.Queries {
subKind, ok := compositeSubQueryTypeToPluginKind[sub.Type]
if !ok {
continue
}
if !slices.Contains(allowed, subKind) {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput,
"%s.spec.queries[%d]: sub-query type %q is not supported by panel kind %q",
path, si, sub.Type, panelKind)
}
}
return nil
}
var (
compositeSubQueryTypeToPluginKind = map[qb.QueryType]QueryPluginKind{
qb.QueryTypeBuilder: QueryKindBuilder,
qb.QueryTypeFormula: QueryKindFormula,
qb.QueryTypeTraceOperator: QueryKindTraceOperator,
qb.QueryTypePromQL: QueryKindPromQL,
qb.QueryTypeClickHouseSQL: QueryKindClickHouseSQL,
}
)

View File

@@ -7,33 +7,75 @@ import (
"testing"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func unmarshalDashboard(data []byte) (*DashboardData, error) {
var d DashboardData
if err := json.Unmarshal(data, &d); err != nil {
return nil, err
}
return &d, nil
}
func TestValidateBigExample(t *testing.T) {
data, err := os.ReadFile("testdata/perses.json")
require.NoError(t, err, "reading example file")
_, err = UnmarshalAndValidateDashboardV2JSON(data)
_, err = unmarshalDashboard(data)
require.NoError(t, err, "expected valid dashboard")
}
func TestValidateDashboardWithSections(t *testing.T) {
data, err := os.ReadFile("testdata/perses_with_sections.json")
require.NoError(t, err, "reading example file")
_, err = UnmarshalAndValidateDashboardV2JSON(data)
_, err = unmarshalDashboard(data)
require.NoError(t, err, "expected valid dashboard")
}
func TestInvalidateNotAJSON(t *testing.T) {
_, err := UnmarshalAndValidateDashboardV2JSON([]byte("not json"))
_, err := unmarshalDashboard([]byte("not json"))
require.Error(t, err, "expected error for invalid JSON")
}
// TestUnmarshalErrorPreservesNestedMessage guards the wrap on dec.Decode in
// DashboardData.UnmarshalJSON. The wrap stamps a consistent type/code on
// decode failures, but must not smother the rich messages produced by nested
// UnmarshalJSON methods (panel/query/variable/datasource plugin envelopes).
func TestUnmarshalErrorPreservesNestedMessage(t *testing.T) {
data := []byte(`{
"panels": {
"p1": {
"kind": "Panel",
"spec": {
"plugin": {"kind": "NonExistentPanel", "spec": {}}
}
}
},
"layouts": []
}`)
_, err := unmarshalDashboard(data)
require.Error(t, err)
require.Contains(t, err.Error(), "unknown panel plugin kind",
"outer wrap should not smother the inner UnmarshalJSON message")
require.Contains(t, err.Error(), `"NonExistentPanel"`,
"the offending value should still appear in the error")
require.Contains(t, err.Error(), "allowed values:",
"the allowed-values hint should still appear in the error")
assert.True(t, errors.Ast(err, errors.TypeInvalidInput),
"outer wrap should classify the error as TypeInvalidInput")
assert.True(t, errors.Asc(err, ErrCodeDashboardInvalidInput),
"outer wrap should stamp ErrCodeDashboardInvalidInput")
}
func TestValidateEmptySpec(t *testing.T) {
// no variables no panels
data := []byte(`{}`)
_, err := UnmarshalAndValidateDashboardV2JSON(data)
_, err := unmarshalDashboard(data)
require.NoError(t, err, "expected valid")
}
@@ -59,17 +101,13 @@ func TestValidateOnlyVariables(t *testing.T) {
"kind": "TextVariable",
"spec": {
"name": "mytext",
"value": "default",
"plugin": {
"kind": "signoz/TextboxVariable",
"spec": {}
}
"value": "default"
}
}
],
"layouts": []
}`)
_, err := UnmarshalAndValidateDashboardV2JSON(data)
_, err := unmarshalDashboard(data)
require.NoError(t, err, "expected valid")
}
@@ -148,7 +186,7 @@ func TestInvalidateUnknownPluginKind(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := UnmarshalAndValidateDashboardV2JSON([]byte(tt.data))
_, err := unmarshalDashboard([]byte(tt.data))
require.Error(t, err, "expected error containing %q, got nil", tt.wantContain)
require.Contains(t, err.Error(), tt.wantContain, "error should mention %q", tt.wantContain)
})
@@ -169,7 +207,7 @@ func TestInvalidateOneInvalidPanel(t *testing.T) {
},
"layouts": []
}`)
_, err := UnmarshalAndValidateDashboardV2JSON(data)
_, err := unmarshalDashboard(data)
require.Error(t, err, "expected error for invalid panel plugin kind")
require.Contains(t, err.Error(), "FakePanel", "error should mention FakePanel")
}
@@ -245,7 +283,7 @@ func TestRejectUnknownFieldsInPluginSpec(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := UnmarshalAndValidateDashboardV2JSON([]byte(tt.data))
_, err := unmarshalDashboard([]byte(tt.data))
require.Error(t, err, "expected error for unknown field")
require.Contains(t, err.Error(), tt.wantContain, "error should mention %q", tt.wantContain)
})
@@ -323,7 +361,7 @@ func TestInvalidateWrongFieldTypeInPluginSpec(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := UnmarshalAndValidateDashboardV2JSON([]byte(tt.data))
_, err := unmarshalDashboard([]byte(tt.data))
require.Error(t, err, "expected validation error")
if tt.wantContain != "" {
require.Contains(t, err.Error(), tt.wantContain, "error should mention %q", tt.wantContain)
@@ -531,13 +569,69 @@ func TestInvalidateBadPanelSpecValues(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := UnmarshalAndValidateDashboardV2JSON([]byte(tt.data))
_, err := unmarshalDashboard([]byte(tt.data))
require.Error(t, err, "expected error containing %q, got nil", tt.wantContain)
require.Contains(t, err.Error(), tt.wantContain, "error should mention %q", tt.wantContain)
})
}
}
func TestInvalidatePanelWithoutQueries(t *testing.T) {
data := []byte(`{
"panels": {
"p1": {
"kind": "Panel",
"spec": {"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}}}
}
},
"layouts": []
}`)
_, err := unmarshalDashboard(data)
require.Error(t, err, "expected panel-without-queries to be rejected")
require.Contains(t, err.Error(), "panel must have one query")
}
func TestInvalidatePanelWithEmptyQueriesArray(t *testing.T) {
data := []byte(`{
"panels": {
"p1": {
"kind": "Panel",
"spec": {
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}},
"queries": []
}
}
},
"layouts": []
}`)
_, err := unmarshalDashboard(data)
require.Error(t, err, "expected panel with explicit empty queries array to be rejected")
require.Contains(t, err.Error(), "panel must have one query")
}
// Rendering multiple data sources in a single panel is supported via
// signoz/CompositeQuery, not by listing multiple top-level queries.
func TestInvalidatePanelWithMultipleDirectQueries(t *testing.T) {
data := []byte(`{
"panels": {
"p1": {
"kind": "Panel",
"spec": {
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}},
"queries": [
{"kind": "TimeSeriesQuery", "spec": {"plugin": {"kind": "signoz/BuilderQuery", "spec": {"name": "A", "signal": "metrics"}}}},
{"kind": "TimeSeriesQuery", "spec": {"plugin": {"kind": "signoz/BuilderQuery", "spec": {"name": "B", "signal": "metrics"}}}}
]
}
}
},
"layouts": []
}`)
_, err := unmarshalDashboard(data)
require.Error(t, err, "expected panel with two top-level queries to be rejected")
require.Contains(t, err.Error(), "panel must have one query")
}
func TestValidateRequiredFields(t *testing.T) {
wrapVariable := func(pluginKind, pluginSpec string) string {
return `{
@@ -626,7 +720,7 @@ func TestValidateRequiredFields(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := UnmarshalAndValidateDashboardV2JSON([]byte(tt.data))
_, err := unmarshalDashboard([]byte(tt.data))
require.Error(t, err, "expected error containing %q, got nil", tt.wantContain)
require.Contains(t, err.Error(), tt.wantContain, "error should mention %q", tt.wantContain)
})
@@ -642,13 +736,14 @@ func TestTimeSeriesPanelDefaults(t *testing.T) {
"plugin": {
"kind": "signoz/TimeSeriesPanel",
"spec": {}
}
},
"queries": [{"kind": "TimeSeriesQuery", "spec": {"plugin": {"kind": "signoz/PromQLQuery", "spec": {"name": "A", "query": "up"}}}}]
}
}
},
"layouts": []
}`)
d, err := UnmarshalAndValidateDashboardV2JSON(data)
d, err := unmarshalDashboard(data)
require.NoError(t, err, "unmarshal and validate failed")
// After validation+normalization, the plugin spec should be a typed struct.
@@ -689,13 +784,14 @@ func TestNumberPanelDefaults(t *testing.T) {
"plugin": {
"kind": "signoz/NumberPanel",
"spec": {"thresholds": [{"value": 100, "color": "Red"}]}
}
},
"queries": [{"kind": "TimeSeriesQuery", "spec": {"plugin": {"kind": "signoz/PromQLQuery", "spec": {"name": "A", "query": "up"}}}}]
}
}
},
"layouts": []
}`)
d, err := UnmarshalAndValidateDashboardV2JSON(data)
d, err := unmarshalDashboard(data)
require.NoError(t, err, "unmarshal and validate failed")
require.IsType(t, &NumberPanelSpec{}, d.Panels["p1"].Spec.Plugin.Spec)
@@ -716,6 +812,30 @@ func TestNumberPanelDefaults(t *testing.T) {
"expected stored/response JSON to contain operator:>, got: %s", outputStr)
}
// TestPersesFixtureStorageRoundTrip exercises the typed → map[string]any →
// typed cycle that the create/get path performs against the kitchen-sink
// fixture. Catches plugin specs whose UnmarshalJSON expects a different shape
// than the default MarshalJSON emits.
func TestPersesFixtureStorageRoundTrip(t *testing.T) {
raw, err := os.ReadFile("testdata/perses.json")
require.NoError(t, err)
var data DashboardData
require.NoError(t, json.Unmarshal(raw, &data), "initial unmarshal")
marshaled, err := json.Marshal(data)
require.NoError(t, err, "marshal typed → JSON")
var asMap map[string]any
require.NoError(t, json.Unmarshal(marshaled, &asMap), "JSON → map (storage shape)")
remarshaled, err := json.Marshal(asMap)
require.NoError(t, err, "map → JSON (read-back shape)")
var roundtripped DashboardData
require.NoError(t, json.Unmarshal(remarshaled, &roundtripped), "JSON → typed (the failure mode)")
}
// TestStorageRoundTrip simulates the future DB store/load cycle:
// marshal the normalized dashboard to JSON (what would be written to DB),
// then unmarshal it back (what would be read from DB), and verify defaults survive.
@@ -728,7 +848,8 @@ func TestStorageRoundTrip(t *testing.T) {
"plugin": {
"kind": "signoz/TimeSeriesPanel",
"spec": {}
}
},
"queries": [{"kind": "TimeSeriesQuery", "spec": {"plugin": {"kind": "signoz/PromQLQuery", "spec": {"name": "A", "query": "up"}}}}]
}
},
"p2": {
@@ -737,7 +858,8 @@ func TestStorageRoundTrip(t *testing.T) {
"plugin": {
"kind": "signoz/NumberPanel",
"spec": {"thresholds": [{"value": 100, "color": "Red"}]}
}
},
"queries": [{"kind": "TimeSeriesQuery", "spec": {"plugin": {"kind": "signoz/PromQLQuery", "spec": {"name": "A", "query": "up"}}}}]
}
}
},
@@ -745,7 +867,7 @@ func TestStorageRoundTrip(t *testing.T) {
}`)
// Step 1: Unmarshal + validate + normalize (what the API handler does).
d, err := UnmarshalAndValidateDashboardV2JSON(input)
d, err := unmarshalDashboard(input)
require.NoError(t, err, "unmarshal and validate failed")
// Step 1.5: Verify struct fields have correct defaults (extra validation before storing).
@@ -765,7 +887,7 @@ func TestStorageRoundTrip(t *testing.T) {
require.NoError(t, err, "marshal for storage failed")
// Step 3: Unmarshal from JSON (simulates reading from DB).
loaded, err := UnmarshalAndValidateDashboardV2JSON(stored)
loaded, err := unmarshalDashboard(stored)
require.NoError(t, err, "unmarshal from storage failed")
// Step 3.5: Verify struct fields have correct defaults after loading (before returning in API).
@@ -878,7 +1000,7 @@ func TestPanelTypeQueryTypeCompatibility(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
_, err := UnmarshalAndValidateDashboardV2JSON(tc.data)
_, err := unmarshalDashboard(tc.data)
if tc.wantErr {
require.Error(t, err)
} else {

View File

@@ -0,0 +1,170 @@
package dashboardtypes
// TestDashboardDataMatchesPerses asserts that DashboardData
// and every nested SigNoz-owned type cover the JSON field set of their Perses
// counterpart.
import (
"reflect"
"sort"
"strings"
"testing"
v1 "github.com/perses/perses/pkg/model/api/v1"
"github.com/perses/perses/pkg/model/api/v1/dashboard"
"github.com/stretchr/testify/assert"
)
func TestDashboardDataMatchesPerses(t *testing.T) {
cases := []struct {
name string
ours reflect.Type
perses reflect.Type
}{
{"DashboardSpec", typeOf[DashboardData](), typeOf[v1.DashboardSpec]()},
{"Panel", typeOf[Panel](), typeOf[v1.Panel]()},
{"PanelSpec", typeOf[PanelSpec](), typeOf[v1.PanelSpec]()},
{"Query", typeOf[Query](), typeOf[v1.Query]()},
{"QuerySpec", typeOf[QuerySpec](), typeOf[v1.QuerySpec]()},
{"DatasourceSpec", typeOf[DatasourceSpec](), typeOf[v1.DatasourceSpec]()},
{"Variable", typeOf[Variable](), typeOf[dashboard.Variable]()},
{"ListVariableSpec", typeOf[ListVariableSpec](), typeOf[dashboard.ListVariableSpec]()},
{"Layout", typeOf[Layout](), typeOf[dashboard.Layout]()},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
missing, extra := drift(c.ours, c.perses)
assert.Empty(t, missing,
"DashboardData (%s) is missing json fields present on Perses %s — upstream likely added or renamed a field",
c.ours.Name(), c.perses.Name())
assert.Empty(t, extra,
"DashboardData (%s) has json fields absent on Perses %s — upstream likely removed a field or we added one without the counterpart",
c.ours.Name(), c.perses.Name())
})
}
}
func TestDriftDetectionMechanics(t *testing.T) {
t.Run("upstream added a field", func(t *testing.T) {
type ours struct {
Name string `json:"name"`
}
type perses struct {
Name string `json:"name"`
Description string `json:"description"`
}
missing, extra := drift(typeOf[ours](), typeOf[perses]())
assert.Equal(t, []string{"description"}, missing, "missing fires: upstream has a field we don't")
assert.Empty(t, extra)
})
t.Run("upstream removed a field", func(t *testing.T) {
type ours struct {
Name string `json:"name"`
Description string `json:"description"`
}
type perses struct {
Name string `json:"name"`
}
missing, extra := drift(typeOf[ours](), typeOf[perses]())
assert.Empty(t, missing)
assert.Equal(t, []string{"description"}, extra, "extra fires: we kept a field upstream removed")
})
t.Run("upstream renamed a field", func(t *testing.T) {
type ours struct {
Name string `json:"name"`
}
type perses struct {
Name string `json:"title"`
}
missing, extra := drift(typeOf[ours](), typeOf[perses]())
assert.Equal(t, []string{"title"}, missing, "missing fires for the new name")
assert.Equal(t, []string{"name"}, extra, "extra fires for the old name — both fire on a rename")
})
t.Run("we added a field upstream does not have", func(t *testing.T) {
type ours struct {
Name string `json:"name"`
Internal string `json:"internal"`
}
type perses struct {
Name string `json:"name"`
}
missing, extra := drift(typeOf[ours](), typeOf[perses]())
assert.Empty(t, missing)
assert.Equal(t, []string{"internal"}, extra, "extra fires: we added a field with no upstream counterpart")
})
t.Run("embedded struct flattens — drift inside the embed is caught", func(t *testing.T) {
type embedded struct {
Display string `json:"display"`
NewBit string `json:"newBit"` // upstream added this inside the embed
}
type ours struct {
Display string `json:"display"`
Name string `json:"name"`
}
type perses struct {
embedded `json:",inline"`
Name string `json:"name"`
}
missing, extra := drift(typeOf[ours](), typeOf[perses]())
assert.Equal(t, []string{"newBit"}, missing, "field added inside an inlined embed surfaces at the parent level")
assert.Empty(t, extra)
})
}
func drift(ours, perses reflect.Type) (missing, extra []string) {
o, p := jsonFields(ours), jsonFields(perses)
return sortedDiff(p, o), sortedDiff(o, p)
}
// jsonFields returns the set of json tag names for a struct, flattening
// anonymous embedded fields (matching encoding/json behavior).
func jsonFields(t reflect.Type) map[string]struct{} {
out := map[string]struct{}{}
if t.Kind() != reflect.Struct {
return out
}
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
// Skip unexported fields (e.g., dashboard.ListVariableSpec has an
// unexported `variableSpec` interface tag).
if !f.IsExported() && !f.Anonymous {
continue
}
tag := f.Tag.Get("json")
name := strings.Split(tag, ",")[0]
// Anonymous embed with empty json name (no tag, or `json:",inline"` /
// `json:",omitempty"`-style options-only tag) is flattened by encoding/json.
if f.Anonymous && name == "" {
for k := range jsonFields(f.Type) {
out[k] = struct{}{}
}
continue
}
if tag == "-" || name == "" {
continue
}
out[name] = struct{}{}
}
return out
}
// sortedDiff returns keys in a but not in b, sorted.
func sortedDiff(a, b map[string]struct{}) []string {
var diff []string
for k := range a {
if _, ok := b[k]; !ok {
diff = append(diff, k)
}
}
sort.Strings(diff)
return diff
}
func typeOf[T any]() reflect.Type { return reflect.TypeOf((*T)(nil)).Elem() }

View File

@@ -0,0 +1,312 @@
package dashboardtypes
import (
"bytes"
"encoding/json"
"maps"
"slices"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/go-playground/validator/v10"
"github.com/swaggest/jsonschema-go"
)
// ══════════════════════════════════════════════
// Panel plugin
// ══════════════════════════════════════════════
type PanelPlugin struct {
Kind PanelPluginKind `json:"kind"`
Spec any `json:"spec"`
}
// PrepareJSONSchema drops the reflected struct shape (type: object, properties)
// from the envelope so that only the JSONSchemaOneOf result binds.
func (PanelPlugin) PrepareJSONSchema(s *jsonschema.Schema) error {
return clearOneOfParentShape(s)
}
func (p *PanelPlugin) UnmarshalJSON(data []byte) error {
kind, specJSON, err := extractKindAndSpec(data)
if err != nil {
return err
}
factory, ok := panelPluginSpecs[PanelPluginKind(kind)]
if !ok {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "unknown panel plugin kind %q; allowed values: %s", kind, allowedValuesForKind(slices.Sorted(maps.Keys(panelPluginSpecs))))
}
spec, err := decodeSpec(specJSON, factory(), kind)
if err != nil {
return err
}
p.Kind = PanelPluginKind(kind)
p.Spec = spec
return nil
}
func (PanelPlugin) JSONSchemaOneOf() []any {
return []any{
PanelPluginVariant[TimeSeriesPanelSpec]{Kind: string(PanelKindTimeSeries)},
PanelPluginVariant[BarChartPanelSpec]{Kind: string(PanelKindBarChart)},
PanelPluginVariant[NumberPanelSpec]{Kind: string(PanelKindNumber)},
PanelPluginVariant[PieChartPanelSpec]{Kind: string(PanelKindPieChart)},
PanelPluginVariant[TablePanelSpec]{Kind: string(PanelKindTable)},
PanelPluginVariant[HistogramPanelSpec]{Kind: string(PanelKindHistogram)},
PanelPluginVariant[ListPanelSpec]{Kind: string(PanelKindList)},
}
}
type PanelPluginVariant[S any] struct {
Kind string `json:"kind" required:"true"`
Spec S `json:"spec" required:"true"`
}
func (v PanelPluginVariant[S]) PrepareJSONSchema(s *jsonschema.Schema) error {
return restrictKindToOneValue(s, v.Kind)
}
// ══════════════════════════════════════════════
// Query plugin
// ══════════════════════════════════════════════
type QueryPlugin struct {
Kind QueryPluginKind `json:"kind"`
Spec any `json:"spec"`
}
func (QueryPlugin) PrepareJSONSchema(s *jsonschema.Schema) error {
return clearOneOfParentShape(s)
}
func (p *QueryPlugin) UnmarshalJSON(data []byte) error {
kind, specJSON, err := extractKindAndSpec(data)
if err != nil {
return err
}
factory, ok := queryPluginSpecs[QueryPluginKind(kind)]
if !ok {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "unknown query plugin kind %q; allowed values: %s", kind, allowedValuesForKind(slices.Sorted(maps.Keys(queryPluginSpecs))))
}
spec, err := decodeSpec(specJSON, factory(), kind)
if err != nil {
return err
}
p.Kind = QueryPluginKind(kind)
p.Spec = spec
return nil
}
func (QueryPlugin) JSONSchemaOneOf() []any {
return []any{
QueryPluginVariant[BuilderQuerySpec]{Kind: string(QueryKindBuilder)},
QueryPluginVariant[CompositeQuerySpec]{Kind: string(QueryKindComposite)},
QueryPluginVariant[FormulaSpec]{Kind: string(QueryKindFormula)},
QueryPluginVariant[PromQLQuerySpec]{Kind: string(QueryKindPromQL)},
QueryPluginVariant[ClickHouseSQLQuerySpec]{Kind: string(QueryKindClickHouseSQL)},
QueryPluginVariant[TraceOperatorSpec]{Kind: string(QueryKindTraceOperator)},
}
}
type QueryPluginVariant[S any] struct {
Kind string `json:"kind" required:"true"`
Spec S `json:"spec" required:"true"`
}
func (v QueryPluginVariant[S]) PrepareJSONSchema(s *jsonschema.Schema) error {
return restrictKindToOneValue(s, v.Kind)
}
// ══════════════════════════════════════════════
// Variable plugin
// ══════════════════════════════════════════════
type VariablePlugin struct {
Kind VariablePluginKind `json:"kind"`
Spec any `json:"spec"`
}
func (VariablePlugin) PrepareJSONSchema(s *jsonschema.Schema) error {
return clearOneOfParentShape(s)
}
func (p *VariablePlugin) UnmarshalJSON(data []byte) error {
kind, specJSON, err := extractKindAndSpec(data)
if err != nil {
return err
}
factory, ok := variablePluginSpecs[VariablePluginKind(kind)]
if !ok {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "unknown variable plugin kind %q; allowed values: %s", kind, allowedValuesForKind(slices.Sorted(maps.Keys(variablePluginSpecs))))
}
spec, err := decodeSpec(specJSON, factory(), kind)
if err != nil {
return err
}
p.Kind = VariablePluginKind(kind)
p.Spec = spec
return nil
}
func (VariablePlugin) JSONSchemaOneOf() []any {
return []any{
VariablePluginVariant[DynamicVariableSpec]{Kind: string(VariableKindDynamic)},
VariablePluginVariant[QueryVariableSpec]{Kind: string(VariableKindQuery)},
VariablePluginVariant[CustomVariableSpec]{Kind: string(VariableKindCustom)},
}
}
type VariablePluginVariant[S any] struct {
Kind string `json:"kind" required:"true"`
Spec S `json:"spec" required:"true"`
}
func (v VariablePluginVariant[S]) PrepareJSONSchema(s *jsonschema.Schema) error {
return restrictKindToOneValue(s, v.Kind)
}
// ══════════════════════════════════════════════
// Datasource plugin
// ══════════════════════════════════════════════
type DatasourcePlugin struct {
Kind DatasourcePluginKind `json:"kind"`
Spec any `json:"spec"`
}
func (DatasourcePlugin) PrepareJSONSchema(s *jsonschema.Schema) error {
return clearOneOfParentShape(s)
}
func (p *DatasourcePlugin) UnmarshalJSON(data []byte) error {
kind, specJSON, err := extractKindAndSpec(data)
if err != nil {
return err
}
factory, ok := datasourcePluginSpecs[DatasourcePluginKind(kind)]
if !ok {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "unknown datasource plugin kind %q; allowed values: %s", kind, allowedValuesForKind(slices.Sorted(maps.Keys(datasourcePluginSpecs))))
}
spec, err := decodeSpec(specJSON, factory(), kind)
if err != nil {
return err
}
p.Kind = DatasourcePluginKind(kind)
p.Spec = spec
return nil
}
func (DatasourcePlugin) JSONSchemaOneOf() []any {
return []any{
DatasourcePluginVariant[struct{}]{Kind: string(DatasourceKindSigNoz)},
}
}
type DatasourcePluginVariant[S any] struct {
Kind string `json:"kind" required:"true"`
Spec S `json:"spec" required:"true"`
}
func (v DatasourcePluginVariant[S]) PrepareJSONSchema(s *jsonschema.Schema) error {
return restrictKindToOneValue(s, v.Kind)
}
// ══════════════════════════════════════════════
// Helpers
// ══════════════════════════════════════════════
var (
panelPluginSpecs = map[PanelPluginKind]func() any{
PanelKindTimeSeries: func() any { return new(TimeSeriesPanelSpec) },
PanelKindBarChart: func() any { return new(BarChartPanelSpec) },
PanelKindNumber: func() any { return new(NumberPanelSpec) },
PanelKindPieChart: func() any { return new(PieChartPanelSpec) },
PanelKindTable: func() any { return new(TablePanelSpec) },
PanelKindHistogram: func() any { return new(HistogramPanelSpec) },
PanelKindList: func() any { return new(ListPanelSpec) },
}
queryPluginSpecs = map[QueryPluginKind]func() any{
QueryKindBuilder: func() any { return new(BuilderQuerySpec) },
QueryKindComposite: func() any { return new(CompositeQuerySpec) },
QueryKindFormula: func() any { return new(FormulaSpec) },
QueryKindPromQL: func() any { return new(PromQLQuerySpec) },
QueryKindClickHouseSQL: func() any { return new(ClickHouseSQLQuerySpec) },
QueryKindTraceOperator: func() any { return new(TraceOperatorSpec) },
}
variablePluginSpecs = map[VariablePluginKind]func() any{
VariableKindDynamic: func() any { return new(DynamicVariableSpec) },
VariableKindQuery: func() any { return new(QueryVariableSpec) },
VariableKindCustom: func() any { return new(CustomVariableSpec) },
}
datasourcePluginSpecs = map[DatasourcePluginKind]func() any{
DatasourceKindSigNoz: func() any { return new(struct{}) },
}
allowedQueryKinds = map[PanelPluginKind][]QueryPluginKind{
PanelKindTimeSeries: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
PanelKindBarChart: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
PanelKindNumber: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
PanelKindHistogram: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
PanelKindPieChart: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindClickHouseSQL},
PanelKindTable: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindClickHouseSQL},
PanelKindList: {QueryKindBuilder},
}
)
func allowedValuesForKind[K ~string](kinds []K) string {
parts := make([]string, len(kinds))
for i, k := range kinds {
parts[i] = "`" + string(k) + "`"
}
return strings.Join(parts, ", ")
}
// extractKindAndSpec parses a {"kind": "...", "spec": {...}} envelope and returns
// kind and the raw spec bytes for typed decoding.
func extractKindAndSpec(data []byte) (string, []byte, error) {
var head struct {
Kind string `json:"kind"`
Spec json.RawMessage `json:"spec"`
}
if err := json.Unmarshal(data, &head); err != nil {
return "", nil, errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid plugin envelope")
}
return head.Kind, head.Spec, nil
}
// decodeSpec strict-decodes a spec JSON into target and runs struct-tag validation (go-playground/validator).
func decodeSpec(specJSON []byte, target any, kind string) (any, error) {
if len(specJSON) == 0 {
return nil, errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "kind %q: spec is required", kind)
}
dec := json.NewDecoder(bytes.NewReader(specJSON))
dec.DisallowUnknownFields()
if err := dec.Decode(target); err != nil {
return nil, errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "kind %q: invalid spec JSON", kind)
}
if err := validator.New().Struct(target); err != nil {
return nil, errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "kind %q: spec failed validation", kind)
}
return target, nil
}
// clearOneOfParentShape drops Type and Properties on a schema that also has a JSONSchemaOneOf.
func clearOneOfParentShape(s *jsonschema.Schema) error {
s.Type = nil
s.Properties = nil
return nil
}
// restrictKindToOneValue ensures that the schema only allows one Kind value for a type.
// For eg. PanelPluginVariant[TimeSeriesPanelSpec]{Kind: string(PanelKindTimeSeries)} should
// only allow "signoz/TimeSeriesPanel" in its kind field.
func restrictKindToOneValue(schema *jsonschema.Schema, kind string) error {
kindProp, ok := schema.Properties["kind"]
if !ok || kindProp.TypeObject == nil {
return errors.NewInternalf(errors.CodeInternal, "variant schema missing `kind` property")
}
kindProp.TypeObject.WithEnum(kind)
schema.Properties["kind"] = kindProp
return nil
}

View File

@@ -0,0 +1,182 @@
package dashboardtypes
import (
"maps"
"slices"
"github.com/SigNoz/signoz/pkg/errors"
v1 "github.com/perses/perses/pkg/model/api/v1"
"github.com/perses/perses/pkg/model/api/v1/common"
"github.com/perses/perses/pkg/model/api/v1/dashboard"
"github.com/perses/perses/pkg/model/api/v1/variable"
"github.com/swaggest/jsonschema-go"
)
// ══════════════════════════════════════════════
// Datasource
// ══════════════════════════════════════════════
type DatasourceSpec struct {
Display *common.Display `json:"display,omitempty"`
Default bool `json:"default"`
Plugin DatasourcePlugin `json:"plugin"`
}
// ══════════════════════════════════════════════
// Panel
// ══════════════════════════════════════════════
type Panel struct {
Kind string `json:"kind"`
Spec PanelSpec `json:"spec"`
}
type PanelSpec struct {
Display *v1.PanelDisplay `json:"display,omitempty"`
Plugin PanelPlugin `json:"plugin"`
Queries []Query `json:"queries,omitempty"`
Links []v1.Link `json:"links,omitempty"`
}
// ══════════════════════════════════════════════
// Query
// ══════════════════════════════════════════════
type Query struct {
Kind string `json:"kind"`
Spec QuerySpec `json:"spec"`
}
type QuerySpec struct {
Name string `json:"name,omitempty"`
Plugin QueryPlugin `json:"plugin"`
}
// ══════════════════════════════════════════════
// Variable
// ══════════════════════════════════════════════
// Variable is the list/text sum type. Spec is set to *ListVariableSpec or
// *dashboard.TextVariableSpec by UnmarshalJSON based on Kind. The schema is a
// discriminated oneOf (see JSONSchemaOneOf).
type Variable struct {
Kind variable.Kind `json:"kind"`
Spec any `json:"spec"`
}
func (Variable) PrepareJSONSchema(s *jsonschema.Schema) error {
return clearOneOfParentShape(s)
}
func (v *Variable) UnmarshalJSON(data []byte) error {
kind, specJSON, err := extractKindAndSpec(data)
if err != nil {
return err
}
switch kind {
case string(variable.KindList):
spec, err := decodeSpec(specJSON, new(ListVariableSpec), kind)
if err != nil {
return err
}
v.Kind = variable.KindList
v.Spec = spec
case string(variable.KindText):
spec, err := decodeSpec(specJSON, new(dashboard.TextVariableSpec), kind)
if err != nil {
return err
}
v.Kind = variable.KindText
v.Spec = spec
default:
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "unknown variable kind %q; allowed values: %s", kind, allowedValuesForKind([]variable.Kind{variable.KindList, variable.KindText}))
}
return nil
}
func (Variable) JSONSchemaOneOf() []any {
return []any{
VariableEnvelope[ListVariableSpec]{Kind: string(variable.KindList)},
VariableEnvelope[dashboard.TextVariableSpec]{Kind: string(variable.KindText)},
}
}
type VariableEnvelope[S any] struct {
Kind string `json:"kind" required:"true"`
Spec S `json:"spec" required:"true"`
}
func (v VariableEnvelope[S]) PrepareJSONSchema(s *jsonschema.Schema) error {
return restrictKindToOneValue(s, v.Kind)
}
// ListVariableSpec mirrors dashboard.ListVariableSpec (variable.ListSpec
// fields + Name) but with a typed VariablePlugin replacing common.Plugin.
type ListVariableSpec struct {
Display *variable.Display `json:"display,omitempty"`
DefaultValue *variable.DefaultValue `json:"defaultValue,omitempty"`
AllowAllValue bool `json:"allowAllValue"`
AllowMultiple bool `json:"allowMultiple"`
CustomAllValue string `json:"customAllValue,omitempty"`
CapturingRegexp string `json:"capturingRegexp,omitempty"`
Sort *variable.Sort `json:"sort,omitempty"`
Plugin VariablePlugin `json:"plugin"`
Name string `json:"name"`
}
// ══════════════════════════════════════════════
// Layout
// ══════════════════════════════════════════════
// Layout is the dashboard layout sum type. Spec is populated by UnmarshalJSON
// with the concrete layout spec struct (today only dashboard.GridLayoutSpec)
// based on Kind. No plugin is involved, so we reuse the Perses spec types as
// leaf imports.
type Layout struct {
Kind dashboard.LayoutKind `json:"kind"`
Spec any `json:"spec"`
}
// layoutSpecs is the layout sum type factory. Perses only defines
// KindGridLayout today; adding a new kind upstream surfaces as an
// "unknown layout kind" runtime error here until we add it.
var layoutSpecs = map[dashboard.LayoutKind]func() any{
dashboard.KindGridLayout: func() any { return new(dashboard.GridLayoutSpec) },
}
func (Layout) PrepareJSONSchema(s *jsonschema.Schema) error {
return clearOneOfParentShape(s)
}
func (l *Layout) UnmarshalJSON(data []byte) error {
kind, specJSON, err := extractKindAndSpec(data)
if err != nil {
return err
}
factory, ok := layoutSpecs[dashboard.LayoutKind(kind)]
if !ok {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "unknown layout kind %q; allowed values: %s", kind, allowedValuesForKind(slices.Sorted(maps.Keys(layoutSpecs))))
}
spec, err := decodeSpec(specJSON, factory(), kind)
if err != nil {
return err
}
l.Kind = dashboard.LayoutKind(kind)
l.Spec = spec
return nil
}
func (Layout) JSONSchemaOneOf() []any {
return []any{
LayoutEnvelope[dashboard.GridLayoutSpec]{Kind: string(dashboard.KindGridLayout)},
}
}
type LayoutEnvelope[S any] struct {
Kind string `json:"kind" required:"true"`
Spec S `json:"spec" required:"true"`
}
func (v LayoutEnvelope[S]) PrepareJSONSchema(s *jsonschema.Schema) error {
return restrictKindToOneValue(s, v.Kind)
}

View File

@@ -8,6 +8,7 @@ import (
qb "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/swaggest/jsonschema-go"
)
// ══════════════════════════════════════════════
@@ -20,11 +21,10 @@ const (
VariableKindDynamic VariablePluginKind = "signoz/DynamicVariable"
VariableKindQuery VariablePluginKind = "signoz/QueryVariable"
VariableKindCustom VariablePluginKind = "signoz/CustomVariable"
VariableKindTextbox VariablePluginKind = "signoz/TextboxVariable"
)
func (VariablePluginKind) Enum() []any {
return []any{VariableKindDynamic, VariableKindQuery, VariableKindCustom, VariableKindTextbox}
return []any{VariableKindDynamic, VariableKindQuery, VariableKindCustom}
}
type DynamicVariableSpec struct {
@@ -42,8 +42,6 @@ type CustomVariableSpec struct {
CustomValue string `json:"customValue" validate:"required" required:"true"`
}
type TextboxVariableSpec struct{}
// ══════════════════════════════════════════════
// SigNoz query plugin specs — aliased from querybuildertypesv5
// ══════════════════════════════════════════════
@@ -87,6 +85,30 @@ func (b *BuilderQuerySpec) UnmarshalJSON(data []byte) error {
return nil
}
// MarshalJSON delegates to the inner Spec so the on-wire shape matches what
// UnmarshalJSON expects (a flat builder-query payload with `signal` at the top
// level). Without this, Go's default would wrap it as {"Spec": {...}} and the
// signal-dispatch on read would fail.
func (b BuilderQuerySpec) MarshalJSON() ([]byte, error) {
return json.Marshal(b.Spec)
}
// PrepareJSONSchema drops the reflected struct shape so only the
// JSONSchemaOneOf result binds.
func (BuilderQuerySpec) PrepareJSONSchema(s *jsonschema.Schema) error {
return clearOneOfParentShape(s)
}
// JSONSchemaOneOf exposes the three signal-dispatched shapes a builder query
// can take. Mirrors qb.UnmarshalBuilderQueryBySignal's runtime dispatch.
func (BuilderQuerySpec) JSONSchemaOneOf() []any {
return []any{
qb.QueryBuilderQuery[qb.LogAggregation]{},
qb.QueryBuilderQuery[qb.MetricAggregation]{},
qb.QueryBuilderQuery[qb.TraceAggregation]{},
}
}
// ══════════════════════════════════════════════
// SigNoz panel plugin specs
// ══════════════════════════════════════════════

View File

@@ -76,11 +76,7 @@
"display": {
"name": "textboxvar"
},
"value": "defaultvaluegoeshere",
"plugin": {
"kind": "signoz/TextboxVariable",
"spec": {}
}
"value": "defaultvaluegoeshere"
}
}
],

View File

@@ -47,12 +47,13 @@ const (
type ValidationOption func(*validationConfig)
type validationConfig struct {
skipLimitOffsetValidation bool
skipAggregationValidation bool
skipHavingValidation bool
skipAggregationOrderBy bool
skipSelectFieldValidation bool
skipGroupByValidation bool
skipLimitOffsetValidation bool
skipAggregationValidation bool
skipHavingValidation bool
skipAggregationOrderBy bool
skipSelectFieldValidation bool
skipGroupByValidation bool
withTimestampGroupByValidation bool
}
func applyValidationOptions(opts []ValidationOption) validationConfig {
@@ -111,6 +112,13 @@ func WithSkipGroupByValidation() ValidationOption {
}
}
// WithTimestampGroupByValidation enables validation to disallow grouping by timestamp field.
func WithTimestampGroupByValidation() ValidationOption {
return func(cfg *validationConfig) {
cfg.withTimestampGroupByValidation = true
}
}
// Validate performs preliminary validation on QueryBuilderQuery.
func (q *QueryBuilderQuery[T]) Validate(opts ...ValidationOption) error {
cfg := applyValidationOptions(opts)
@@ -177,6 +185,11 @@ func (q *QueryBuilderQuery[T]) validateGroupBy(cfg validationConfig) error {
errors.CodeInvalidInput, "invalid empty key name for group by at index %d", idx,
)
}
if cfg.withTimestampGroupByValidation && item.Name == "timestamp" {
return errors.NewInvalidInputf(
errors.CodeInvalidInput, "group by on timestamp is not allowed",
)
}
}
return nil
}
@@ -665,7 +678,9 @@ func validateQueryEnvelope(envelope QueryEnvelope, opts ...ValidationOption) err
func GetValidationOptions(requestType RequestType) []ValidationOption {
switch requestType {
case RequestTypeTimeSeries, RequestTypeScalar:
case RequestTypeTimeSeries:
return []ValidationOption{WithSkipSelectFieldValidation(), WithTimestampGroupByValidation()}
case RequestTypeScalar:
return []ValidationOption{WithSkipSelectFieldValidation()}
case RequestTypeRaw, RequestTypeRawStream, RequestTypeTrace:
return []ValidationOption{WithSkipAggregationValidation(), WithSkipHavingValidation(), WithSkipAggregationOrderBy(), WithSkipGroupByValidation()}

View File

@@ -695,6 +695,59 @@ func TestQueryRangeRequest_ValidateCompositeQuery(t *testing.T) {
wantErr: true,
errMsg: "raw request type is not supported for metric queries",
},
{
name: "timeseries request with group by timestamp should return error",
request: QueryRangeRequest{
Start: 1640995200000,
End: 1640998800000,
RequestType: RequestTypeTimeSeries,
CompositeQuery: CompositeQuery{
Queries: []QueryEnvelope{
{
Type: QueryTypeBuilder,
Spec: QueryBuilderQuery[LogAggregation]{
Name: "A",
Signal: telemetrytypes.SignalLogs,
Aggregations: []LogAggregation{
{Expression: "count()"},
},
GroupBy: []GroupByKey{
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "timestamp"}},
},
},
},
},
},
},
wantErr: true,
errMsg: "group by on timestamp is not allowed",
},
{
name: "scalar request with group by timestamp should pass",
request: QueryRangeRequest{
Start: 1640995200000,
End: 1640998800000,
RequestType: RequestTypeScalar,
CompositeQuery: CompositeQuery{
Queries: []QueryEnvelope{
{
Type: QueryTypeBuilder,
Spec: QueryBuilderQuery[LogAggregation]{
Name: "A",
Signal: telemetrytypes.SignalLogs,
Aggregations: []LogAggregation{
{Expression: "count()"},
},
GroupBy: []GroupByKey{
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "timestamp"}},
},
},
},
},
},
},
wantErr: false,
},
{
name: "raw request with log query without aggregations should pass",
request: QueryRangeRequest{

View File

@@ -250,17 +250,20 @@ type EvaluationEnvelope struct {
// evaluationRolling is the OpenAPI schema for an EvaluationEnvelope with kind=rolling.
type evaluationRolling struct {
Kind EvaluationKind `json:"kind" description:"The kind of evaluation."`
Spec RollingWindow `json:"spec" description:"The rolling window evaluation specification."`
Kind EvaluationKind `json:"kind" description:"The kind of evaluation." required:"true"`
Spec RollingWindow `json:"spec" description:"The rolling window evaluation specification." required:"true"`
}
// evaluationCumulative is the OpenAPI schema for an EvaluationEnvelope with kind=cumulative.
type evaluationCumulative struct {
Kind EvaluationKind `json:"kind" description:"The kind of evaluation."`
Spec CumulativeWindow `json:"spec" description:"The cumulative window evaluation specification."`
Kind EvaluationKind `json:"kind" description:"The kind of evaluation." required:"true"`
Spec CumulativeWindow `json:"spec" description:"The cumulative window evaluation specification." required:"true"`
}
var _ jsonschema.OneOfExposer = EvaluationEnvelope{}
var (
_ jsonschema.OneOfExposer = EvaluationEnvelope{}
_ jsonschema.Preparer = EvaluationEnvelope{}
)
// JSONSchemaOneOf returns the oneOf variants for the EvaluationEnvelope discriminated union.
// Each variant represents a different evaluation kind with its corresponding spec schema.
@@ -271,6 +274,22 @@ func (EvaluationEnvelope) JSONSchemaOneOf() []any {
}
}
func (EvaluationEnvelope) PrepareJSONSchema(schema *jsonschema.Schema) error {
if schema.ExtraProperties == nil {
schema.ExtraProperties = map[string]any{}
}
schema.ExtraProperties["x-signoz-discriminator"] = map[string]any{
"propertyName": "kind",
"mapping": map[string]string{
"rolling": "#/components/schemas/RuletypesEvaluationRolling",
"cumulative": "#/components/schemas/RuletypesEvaluationCumulative",
},
}
return nil
}
func (e *EvaluationEnvelope) UnmarshalJSON(data []byte) error {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {

View File

@@ -36,11 +36,14 @@ type RuleThresholdData struct {
// thresholdBasic is the OpenAPI schema for a RuleThresholdData with kind=basic.
type thresholdBasic struct {
Kind ThresholdKind `json:"kind" description:"The kind of threshold."`
Spec BasicRuleThresholds `json:"spec" description:"The basic threshold specification (array of thresholds)."`
Kind ThresholdKind `json:"kind" description:"The kind of threshold." required:"true"`
Spec BasicRuleThresholds `json:"spec" description:"The basic threshold specification (array of thresholds)." required:"true"`
}
var _ jsonschema.OneOfExposer = RuleThresholdData{}
var (
_ jsonschema.OneOfExposer = RuleThresholdData{}
_ jsonschema.Preparer = RuleThresholdData{}
)
// JSONSchemaOneOf returns the oneOf variants for the RuleThresholdData discriminated union.
// Each variant represents a different threshold kind with its corresponding spec schema.
@@ -50,6 +53,24 @@ func (RuleThresholdData) JSONSchemaOneOf() []any {
}
}
// PrepareJSONSchema marks the schema with x-signoz-discriminator;
// signoz.attachDiscriminators promotes it to a real OpenAPI 3
// discriminator after reflection.
func (RuleThresholdData) PrepareJSONSchema(schema *jsonschema.Schema) error {
if schema.ExtraProperties == nil {
schema.ExtraProperties = map[string]any{}
}
schema.ExtraProperties["x-signoz-discriminator"] = map[string]any{
"propertyName": "kind",
"mapping": map[string]string{
"basic": "#/components/schemas/RuletypesThresholdBasic",
},
}
return nil
}
func (r *RuleThresholdData) UnmarshalJSON(data []byte) error {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {

View File

@@ -25,6 +25,8 @@ pytest_plugins = [
"fixtures.cloudintegrations",
"fixtures.jsontypes",
"fixtures.seeder",
"fixtures.serviceaccount",
"fixtures.role",
]

76
tests/fixtures/role.py vendored Normal file
View File

@@ -0,0 +1,76 @@
"""Fixtures and helpers for role tests."""
from http import HTTPStatus
import requests
from fixtures import types
from fixtures.logger import setup_logger
logger = setup_logger(__name__)
ROLES_BASE = "/api/v1/roles"
def find_role_by_name(signoz: types.SigNoz, token: str, name: str) -> str:
"""Find a role by name from the roles endpoint and return its UUID."""
resp = requests.get(
signoz.self.host_configs["8080"].get(ROLES_BASE),
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.OK, resp.text
roles = resp.json()["data"]
role = next(r for r in roles if r["name"] == name)
return role["id"]
def create_custom_role(signoz: types.SigNoz, token: str, name: str) -> str:
"""Create a custom role and return its ID."""
resp = requests.post(
signoz.self.host_configs["8080"].get(ROLES_BASE),
json={"name": name},
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.CREATED, resp.text
return resp.json()["data"]["id"]
def delete_custom_role(signoz: types.SigNoz, token: str, role_id: str) -> None:
"""Delete a custom role."""
resp = requests.delete(
signoz.self.host_configs["8080"].get(f"{ROLES_BASE}/{role_id}"),
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.NO_CONTENT, resp.text
def patch_role_objects(
signoz: types.SigNoz,
token: str,
role_id: str,
relation: str,
additions=None,
deletions=None,
) -> None:
"""PATCH /api/v1/roles/{id}/relations/{relation}/objects."""
body = {}
if additions is not None:
body["additions"] = additions
if deletions is not None:
body["deletions"] = deletions
resp = requests.patch(
signoz.self.host_configs["8080"].get(f"{ROLES_BASE}/{role_id}/relations/{relation}/objects"),
json=body,
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.NO_CONTENT, f"PatchObjects {relation} failed: {resp.text}"
def object_group(type_name: str, kind_name: str, selectors: list[str]) -> dict:
"""Build an ObjectGroup dict for PatchObjects."""
return {"resource": {"type": type_name, "kind": kind_name}, "selectors": selectors}

View File

@@ -6,24 +6,11 @@ import requests
from fixtures import types
from fixtures.logger import setup_logger
from fixtures.role import ROLES_BASE, find_role_by_name # noqa: F401 — re-export for existing callers
logger = setup_logger(__name__)
SERVICE_ACCOUNT_BASE = "/api/v1/service_accounts"
ROLES_BASE = "/api/v1/roles"
def find_role_by_name(signoz: types.SigNoz, token: str, name: str) -> str:
"""Find a role by name from the roles endpoint and return its UUID."""
resp = requests.get(
signoz.self.host_configs["8080"].get(ROLES_BASE),
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.OK, resp.text
roles = resp.json()["data"]
role = next(r for r in roles if r["name"] == name)
return role["id"]
def create_service_account(signoz: types.SigNoz, token: str, name: str, role: str = "signoz-viewer") -> str:
@@ -75,6 +62,17 @@ def delete_service_account(signoz: types.SigNoz, token: str, service_account_id:
assert resp.status_code == HTTPStatus.NO_CONTENT, resp.text
def get_first_key_id(signoz: types.SigNoz, token: str, service_account_id: str) -> str:
"""Return the ID of the first API key for a service account."""
resp = requests.get(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/keys"),
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.OK, resp.text
return resp.json()["data"][0]["id"]
def find_service_account_by_name(signoz: types.SigNoz, token: str, name: str) -> dict:
"""Find a service account by name from the list endpoint."""
list_resp = requests.get(

View File

@@ -0,0 +1,578 @@
"""Tests for resource-level FGA on service account endpoints.
Validates that a custom role with specific SA permissions gets exactly
the access it was granted, and that SA role assignment requires BOTH
serviceaccount:attach AND role:attach.
"""
from collections.abc import Callable
from http import HTTPStatus
import requests
from wiremock.resources.mappings import Mapping
from fixtures import types
from fixtures.auth import (
USER_ADMIN_EMAIL,
USER_ADMIN_PASSWORD,
add_license,
change_user_role,
create_active_user,
find_user_by_email,
)
from fixtures.role import (
create_custom_role,
delete_custom_role,
find_role_by_name,
object_group,
patch_role_objects,
)
from fixtures.serviceaccount import (
SERVICE_ACCOUNT_BASE,
create_service_account,
find_service_account_by_name,
get_first_key_id,
)
SA_FGA_CUSTOM_ROLE_NAME = "sa-fga-readonly"
SA_FGA_CUSTOM_USER_EMAIL = "customrole+safga@integration.test"
SA_FGA_CUSTOM_USER_PASSWORD = "password123Z$"
SA_FGA_TARGET_SA_NAME = "sa-fga-target"
# ---------------------------------------------------------------------------
# 1. Apply license (required for custom role CRUD)
# ---------------------------------------------------------------------------
def test_apply_license(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
make_http_mocks: Callable[[types.TestContainerDocker, list[Mapping]], None],
get_token: Callable[[str, str], str],
) -> None:
add_license(signoz, make_http_mocks, get_token)
# ---------------------------------------------------------------------------
# 2. Create custom role + user
# ---------------------------------------------------------------------------
def test_create_custom_role_readonly_sa(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
# Create the custom role.
role_id = create_custom_role(signoz, admin_token, SA_FGA_CUSTOM_ROLE_NAME)
# Grant read on serviceaccount instances.
patch_role_objects(
signoz,
admin_token,
role_id,
"read",
additions=[
object_group("serviceaccount", "serviceaccount", ["*"]),
],
)
# Grant list on serviceaccount collection.
patch_role_objects(
signoz,
admin_token,
role_id,
"list",
additions=[
object_group("metaresources", "serviceaccount", ["*"]),
],
)
# Create the custom-role user: invite as VIEWER, activate, change role.
user_id = create_active_user(
signoz,
admin_token,
email=SA_FGA_CUSTOM_USER_EMAIL,
role="VIEWER",
password=SA_FGA_CUSTOM_USER_PASSWORD,
name="sa-fga-test-user",
)
change_user_role(signoz, admin_token, user_id, "signoz-viewer", SA_FGA_CUSTOM_ROLE_NAME)
# Create a target SA (with role + key) for the custom user to operate on.
sa_id = create_service_account(signoz, admin_token, SA_FGA_TARGET_SA_NAME, role="signoz-viewer")
# Create a key on the target SA.
key_resp = requests.post(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{sa_id}/keys"),
json={"name": "fga-key", "expiresAt": 0},
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert key_resp.status_code == HTTPStatus.CREATED, key_resp.text
# ---------------------------------------------------------------------------
# 3. Read-only access: allowed operations
# ---------------------------------------------------------------------------
def test_readonly_role_allowed_operations(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
token = get_token(SA_FGA_CUSTOM_USER_EMAIL, SA_FGA_CUSTOM_USER_PASSWORD)
sa_id = find_service_account_by_name(signoz, get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD), SA_FGA_TARGET_SA_NAME)["id"]
# List SAs.
resp = requests.get(
signoz.self.host_configs["8080"].get(SERVICE_ACCOUNT_BASE),
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.OK, f"list SAs: {resp.text}"
# Get SA.
resp = requests.get(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{sa_id}"),
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.OK, f"get SA: {resp.text}"
# Get SA roles.
resp = requests.get(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{sa_id}/roles"),
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.OK, f"get SA roles: {resp.text}"
# List SA keys.
resp = requests.get(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{sa_id}/keys"),
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.OK, f"list SA keys: {resp.text}"
# ---------------------------------------------------------------------------
# 4. Read-only access: forbidden operations
# ---------------------------------------------------------------------------
def test_readonly_role_forbidden_operations(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
token = get_token(SA_FGA_CUSTOM_USER_EMAIL, SA_FGA_CUSTOM_USER_PASSWORD)
sa_id = find_service_account_by_name(signoz, admin_token, SA_FGA_TARGET_SA_NAME)["id"]
viewer_role_id = find_role_by_name(signoz, admin_token, "signoz-viewer")
key_id = get_first_key_id(signoz, admin_token, sa_id)
# Create SA — forbidden.
resp = requests.post(
signoz.self.host_configs["8080"].get(SERVICE_ACCOUNT_BASE),
json={"name": "sa-fga-should-fail"},
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.FORBIDDEN, f"create SA: expected 403, got {resp.status_code}: {resp.text}"
# Update SA — forbidden.
resp = requests.put(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{sa_id}"),
json={"name": "sa-fga-renamed"},
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.FORBIDDEN, f"update SA: expected 403, got {resp.status_code}: {resp.text}"
# Delete SA — forbidden.
resp = requests.delete(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{sa_id}"),
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.FORBIDDEN, f"delete SA: expected 403, got {resp.status_code}: {resp.text}"
# Assign role to SA — forbidden.
resp = requests.post(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{sa_id}/roles"),
json={"id": viewer_role_id},
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.FORBIDDEN, f"assign SA role: expected 403, got {resp.status_code}: {resp.text}"
# Remove role from SA — forbidden.
resp = requests.delete(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{sa_id}/roles/{viewer_role_id}"),
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.FORBIDDEN, f"remove SA role: expected 403, got {resp.status_code}: {resp.text}"
# Create key — forbidden (needs update).
resp = requests.post(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{sa_id}/keys"),
json={"name": "fga-key-fail", "expiresAt": 0},
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.FORBIDDEN, f"create key: expected 403, got {resp.status_code}: {resp.text}"
# Revoke key — forbidden (needs update).
resp = requests.delete(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{sa_id}/keys/{key_id}"),
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.FORBIDDEN, f"revoke key: expected 403, got {resp.status_code}: {resp.text}"
# ---------------------------------------------------------------------------
# 5. Grant write permissions, verify access opens up
# ---------------------------------------------------------------------------
def test_patch_role_add_write_permissions(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
role_id = find_role_by_name(signoz, admin_token, SA_FGA_CUSTOM_ROLE_NAME)
sa_id = find_service_account_by_name(signoz, admin_token, SA_FGA_TARGET_SA_NAME)["id"]
viewer_role_id = find_role_by_name(signoz, admin_token, "signoz-viewer")
# Grant create on collection.
patch_role_objects(
signoz,
admin_token,
role_id,
"create",
additions=[
object_group("metaresources", "serviceaccount", ["*"]),
],
)
# Grant update on instances.
patch_role_objects(
signoz,
admin_token,
role_id,
"update",
additions=[
object_group("serviceaccount", "serviceaccount", ["*"]),
],
)
# Grant delete on instances.
patch_role_objects(
signoz,
admin_token,
role_id,
"delete",
additions=[
object_group("serviceaccount", "serviceaccount", ["*"]),
],
)
custom_token = get_token(SA_FGA_CUSTOM_USER_EMAIL, SA_FGA_CUSTOM_USER_PASSWORD)
# Create SA — now allowed.
resp = requests.post(
signoz.self.host_configs["8080"].get(SERVICE_ACCOUNT_BASE),
json={"name": "sa-fga-write-test"},
headers={"Authorization": f"Bearer {custom_token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.CREATED, f"create SA: {resp.text}"
new_sa_id = resp.json()["data"]["id"]
# Update SA — now allowed.
resp = requests.put(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{new_sa_id}"),
json={"name": "sa-fga-write-renamed"},
headers={"Authorization": f"Bearer {custom_token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.NO_CONTENT, f"update SA: {resp.text}"
# Create key — now allowed (update permission covers key create).
key_resp = requests.post(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{new_sa_id}/keys"),
json={"name": "fga-write-key", "expiresAt": 0},
headers={"Authorization": f"Bearer {custom_token}"},
timeout=5,
)
assert key_resp.status_code == HTTPStatus.CREATED, f"create key: {key_resp.text}"
new_key_id = key_resp.json()["data"]["id"]
# Revoke key — now allowed (update permission covers key revoke).
resp = requests.delete(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{new_sa_id}/keys/{new_key_id}"),
headers={"Authorization": f"Bearer {custom_token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.NO_CONTENT, f"revoke key: {resp.text}"
# Delete SA — now allowed.
resp = requests.delete(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{new_sa_id}"),
headers={"Authorization": f"Bearer {custom_token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.NO_CONTENT, f"delete SA: {resp.text}"
# Role assignment still forbidden (no attach).
resp = requests.post(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{sa_id}/roles"),
json={"id": viewer_role_id},
headers={"Authorization": f"Bearer {custom_token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.FORBIDDEN, f"assign SA role: expected 403, got {resp.status_code}: {resp.text}"
resp = requests.delete(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{sa_id}/roles/{viewer_role_id}"),
headers={"Authorization": f"Bearer {custom_token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.FORBIDDEN, f"remove SA role: expected 403, got {resp.status_code}: {resp.text}"
# ---------------------------------------------------------------------------
# 6. Dual-attach: SA attach only (no role attach) → forbidden
# ---------------------------------------------------------------------------
def test_attach_with_only_sa_attach_forbidden(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
role_id = find_role_by_name(signoz, admin_token, SA_FGA_CUSTOM_ROLE_NAME)
sa_id = find_service_account_by_name(signoz, admin_token, SA_FGA_TARGET_SA_NAME)["id"]
viewer_role_id = find_role_by_name(signoz, admin_token, "signoz-viewer")
# Grant attach on serviceaccount only.
patch_role_objects(
signoz,
admin_token,
role_id,
"attach",
additions=[
object_group("serviceaccount", "serviceaccount", ["*"]),
],
)
custom_token = get_token(SA_FGA_CUSTOM_USER_EMAIL, SA_FGA_CUSTOM_USER_PASSWORD)
# Assign role — forbidden (has SA attach, missing role attach).
resp = requests.post(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{sa_id}/roles"),
json={"id": viewer_role_id},
headers={"Authorization": f"Bearer {custom_token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.FORBIDDEN, f"assign with only SA attach: expected 403, got {resp.status_code}: {resp.text}"
# Remove role — forbidden (CheckAll: role attach group fails).
resp = requests.delete(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{sa_id}/roles/{viewer_role_id}"),
headers={"Authorization": f"Bearer {custom_token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.FORBIDDEN, f"remove with only SA attach: expected 403, got {resp.status_code}: {resp.text}"
# ---------------------------------------------------------------------------
# 7. Dual-attach: role attach only (no SA attach) → forbidden
# ---------------------------------------------------------------------------
def test_attach_with_only_role_attach_forbidden(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
role_id = find_role_by_name(signoz, admin_token, SA_FGA_CUSTOM_ROLE_NAME)
sa_id = find_service_account_by_name(signoz, admin_token, SA_FGA_TARGET_SA_NAME)["id"]
viewer_role_id = find_role_by_name(signoz, admin_token, "signoz-viewer")
# Remove SA attach, grant role attach.
patch_role_objects(
signoz,
admin_token,
role_id,
"attach",
additions=[object_group("role", "role", ["*"])],
deletions=[object_group("serviceaccount", "serviceaccount", ["*"])],
)
custom_token = get_token(SA_FGA_CUSTOM_USER_EMAIL, SA_FGA_CUSTOM_USER_PASSWORD)
# Assign role — forbidden (middleware SA attach check fails).
resp = requests.post(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{sa_id}/roles"),
json={"id": viewer_role_id},
headers={"Authorization": f"Bearer {custom_token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.FORBIDDEN, f"assign with only role attach: expected 403, got {resp.status_code}: {resp.text}"
# Remove role — forbidden (CheckAll: SA attach group fails).
resp = requests.delete(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{sa_id}/roles/{viewer_role_id}"),
headers={"Authorization": f"Bearer {custom_token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.FORBIDDEN, f"remove with only role attach: expected 403, got {resp.status_code}: {resp.text}"
# ---------------------------------------------------------------------------
# 8. Dual-attach: both SA + role attach → succeeds
# ---------------------------------------------------------------------------
def test_attach_with_both_permissions_succeeds(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
role_id = find_role_by_name(signoz, admin_token, SA_FGA_CUSTOM_ROLE_NAME)
sa_id = find_service_account_by_name(signoz, admin_token, SA_FGA_TARGET_SA_NAME)["id"]
# Add back SA attach (role attach already present from previous test).
patch_role_objects(
signoz,
admin_token,
role_id,
"attach",
additions=[
object_group("serviceaccount", "serviceaccount", ["*"]),
],
)
custom_token = get_token(SA_FGA_CUSTOM_USER_EMAIL, SA_FGA_CUSTOM_USER_PASSWORD)
# The target SA currently has signoz-viewer assigned. Assign a different role.
editor_role_id = find_role_by_name(signoz, admin_token, "signoz-editor")
# Assign editor role — should succeed (both SA attach + role attach).
resp = requests.post(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{sa_id}/roles"),
json={"id": editor_role_id},
headers={"Authorization": f"Bearer {custom_token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.NO_CONTENT, f"assign with both attach: {resp.text}"
# Remove the editor role — should succeed (CheckAll: both groups pass).
resp = requests.delete(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{sa_id}/roles/{editor_role_id}"),
headers={"Authorization": f"Bearer {custom_token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.NO_CONTENT, f"remove with both attach: {resp.text}"
# ---------------------------------------------------------------------------
# 9. Revoke read/list → verify access lost
# ---------------------------------------------------------------------------
def test_remove_read_permissions_revokes_access(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
role_id = find_role_by_name(signoz, admin_token, SA_FGA_CUSTOM_ROLE_NAME)
sa_id = find_service_account_by_name(signoz, admin_token, SA_FGA_TARGET_SA_NAME)["id"]
# Revoke read.
patch_role_objects(
signoz,
admin_token,
role_id,
"read",
deletions=[
object_group("serviceaccount", "serviceaccount", ["*"]),
],
)
# Revoke list.
patch_role_objects(
signoz,
admin_token,
role_id,
"list",
deletions=[
object_group("metaresources", "serviceaccount", ["*"]),
],
)
custom_token = get_token(SA_FGA_CUSTOM_USER_EMAIL, SA_FGA_CUSTOM_USER_PASSWORD)
# List SAs — forbidden.
resp = requests.get(
signoz.self.host_configs["8080"].get(SERVICE_ACCOUNT_BASE),
headers={"Authorization": f"Bearer {custom_token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.FORBIDDEN, f"list SAs after revoke: expected 403, got {resp.status_code}: {resp.text}"
# Get SA — forbidden.
resp = requests.get(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{sa_id}"),
headers={"Authorization": f"Bearer {custom_token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.FORBIDDEN, f"get SA after revoke: expected 403, got {resp.status_code}: {resp.text}"
# ---------------------------------------------------------------------------
# 10. Clean up: delete custom role
# ---------------------------------------------------------------------------
def test_delete_custom_role_cleanup(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
role_id = find_role_by_name(signoz, admin_token, SA_FGA_CUSTOM_ROLE_NAME)
user = find_user_by_email(signoz, admin_token, SA_FGA_CUSTOM_USER_EMAIL)
# Remove the custom role from the user first — role deletion requires no assignees.
resp = requests.get(
signoz.self.host_configs["8080"].get(f"/api/v2/users/{user['id']}/roles"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.OK, resp.text
roles = resp.json()["data"]
custom_entry = next((r for r in roles if r["name"] == SA_FGA_CUSTOM_ROLE_NAME), None)
if custom_entry is not None:
resp = requests.delete(
signoz.self.host_configs["8080"].get(f"/api/v2/users/{user['id']}/roles/{custom_entry['id']}"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.NO_CONTENT, f"remove role from user: {resp.text}"
delete_custom_role(signoz, admin_token, role_id)