Compare commits

..

14 Commits

Author SHA1 Message Date
SagarRajput-7
44f1332169 Merge branch 'main' into platform-tests-refactor 2026-05-14 17:17:10 +05:30
SagarRajput-7
6736957074 chore(test): fixes in roledetailspage test 2026-05-14 17:15:51 +05:30
SagarRajput-7
4878757c4c Merge branch 'main' into platform-tests-refactor 2026-05-14 17:00:29 +05:30
Vinicius Lourenço
4f263304f0 chore(husky): ensure can run without xterm-256color (#11297)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
2026-05-14 11:22:07 +00:00
Vinicius Lourenço
2680f7163f chore(signozhq): bump to 0.0.19 (#11294) 2026-05-14 11:21:41 +00:00
Vikrant Gupta
edb30f29c1 feat(authz): introduce detach relationship (#11298)
* feat(authz): introduce detach relationship

* feat(authz): attach and detach for parent child heirarchy

* feat(authz): fix the openapi spec generated schemas

* feat(authz): add integration tests

* feat(authz): add telemetry metaresource

* feat(authz): fix the http response and integration tests

* feat(authz): generate frontend openapi schema

* feat(authz): remove unwanted tuples
2026-05-14 11:06:17 +00:00
Vikrant Gupta
d6f4b051e6 chore(authz): add support for wildcard selectors (#11310) 2026-05-14 11:05:37 +00:00
Ashwin Bhatkal
7bc6ce7551 chore: pin pnpm version in package.json and enforce via preinstall (#11303)
* test: fix pnpm version in package json and npmrc

* test: remove packageManager as it shows wrong error message
2026-05-14 10:59:05 +00:00
Nikhil Mantri
3b9ee4901e feat(infra-monitoring): v2 jobs list api (#11148)
* chore: baseline setup

* chore: endpoint detail update

* chore: added logic for hosts v3 api

* fix: bug fix

* chore: disk usage

* chore: added validate function

* chore: added some unit tests

* chore: return status as a string

* chore: yarn generate api

* chore: removed isSendingK8sAgentsMetricsCode

* chore: moved funcs

* chore: added validation on order by

* chore: added pods list logic

* chore: updated openapi yml

* chore: updated spec

* chore: pods api meta start time

* chore: nil pointer check

* chore: nil pointer dereference fix in req.Filter

* chore: added temporalities of metrics

* chore: added pods metrics temporality

* chore: unified composite key function

* chore: code improvements

* chore: added pods list api updates

* chore: hostStatusNone added for clarity that this field can be left empty as well in payload

* chore: yarn generate api

* chore: return errors from getMetadata and lint fix

* chore: return errors from getMetadata and lint fix

* chore: added hostName logic

* chore: modified getMetadata query

* chore: add type for response and files rearrange

* chore: warnings added passing from queryResponse warning to host lists response struct

* chore: added better metrics existence check

* chore: added a TODO remark

* chore: added required metrics check

* chore: distributed samples table to local table change for get metadata

* chore: frontend fix

* chore: endpoint correction

* chore: endpoint modification openapi

* chore: escape backtick to prevent sql injection

* chore: rearrage

* chore: improvements

* chore: validate order by to validate function

* chore: improved description

* chore: added TODOs and made filterByStatus a part of filter struct

* chore: ignore empty string hosts in get active hosts

* feat(infra-monitoring): v2 hosts list - return counts of active & inactive hosts for custom group by attributes (#10956)

* chore: add functionality for showing active and inactive counts in custom group by

* chore: bug fix

* chore: added subquery for active and total count

* chore: ignore empty string hosts in get active hosts

* fix: sinceUnixMilli for determining active hosts compute once per request

* chore: refactor code

* chore: rename HostsList -> ListHosts

* chore: rearrangement

* chore: inframonitoring types renaming

* chore: added types package

* chore: file structure further breakdown for clarity

* chore: comments correction

* chore: removed temporalities

* chore: pods code restructuring

* chore: comments resolve

* chore: added json tag required: true

* chore: removed pod metric temporalities

* chore: removed internal server error

* chore: added status unauthorized

* chore: remove a defensive nil map check, the function ensure non-nil map when err nil

* chore: cleanup and rename

* chore: make sort stable in case of tiebreaker by comparing composite group by keys

* chore: regen api client for inframonitoring

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: added phase counts feature

* chore: added queries for pod phase counts in custom group by

* chore: added required tags

* chore: added support for pod phase unknown

* chore: removed pods - order by phase

* chore: improved api description to document -1 as no data in numeric fields

* fix: rebase fixes

* chore: added unknown phase count

* fix: isPodUIDInGroupBy in buildPodRecords

* chore: 3 cte --> 2 cte

* chore: pod phase with local table of time series as counts

* chore: comment correction

* chore: corrected comment

* chore: value column for samples table added

* chore: removed query G for phase counts

* chore: rename variable

* chore: added PodPhaseNum constants to types

* feat(infra-monitoring): v2 pods list apis - phase counts when custom grouping (#11088)

* chore: added phase counts feature

* chore: added queries for pod phase counts in custom group by

* chore: added unknown phase count

* fix: isPodUIDInGroupBy in buildPodRecords

* chore: 3 cte --> 2 cte

* chore: pod phase with local table of time series as counts

* chore: comment correction

* chore: corrected comment

* chore: value column for samples table added

* chore: removed query G for phase counts

* chore: rename variable

* chore: added PodPhaseNum constants to types

* chore: nodes list v2 full blown

* chore: metadata fix

* chore: updated comment

* chore: namespaces code

* chore: v2 nodes api

* chore: rename

* chore: v2 clusters list api

* chore: namespaces code

* chore: rename

* chore: review clusters PR

* chore: pvcs code added

* chore: updated endpoint and spec

* chore: pvcs todo

* chore: added condition

* chore: added filter

* chore: added code for deployments

* chore: query nit

* chore: statefulsets code added

* chore: base filter added

* chore: added base deployments change

* chore: added base condition

* chore: v2 jobs list api added

* chore: added pod phase counts

* chore: for pods and nodes, replace none with no_data

* chore: node and pod counts structs added

* chore: namespace record uses PodCountsByPhase

* chore: cluster record uses PodCountsByPhase, NodeCountsByReadiness

* chore: deployment record uses PodCountsByPhase

* chore: statefulset record uses PodCountsByPhase

* chore: job record uses PodCountsByPhase

* chore: metrics existence check

* chore: statefulset metrics added

* chore: added jobs metrics

* chore: added metrics

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Ashwin Bhatkal <ashwin96@gmail.com>
2026-05-14 10:49:21 +00:00
SagarRajput-7
d35db1b02b chore(test): refactored custom domain and generel settings tests 2026-05-14 06:55:07 +05:30
SagarRajput-7
0556e67739 chore(test): refactored memebrs and sa tests 2026-05-14 06:36:46 +05:30
SagarRajput-7
79ea20b371 chore(test): fixes in roledetailspage test 2026-05-14 05:45:13 +05:30
SagarRajput-7
2bbd11c181 chore(test): fix, unskip and refactor platform surfaces tests 2026-05-14 05:38:06 +05:30
SagarRajput-7
0f9891d1c1 chore(test): fix, unskip and refactor platform surfaces tests 2026-05-14 04:33:46 +05:30
65 changed files with 2762 additions and 1009 deletions

View File

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

View File

@@ -449,6 +449,7 @@ components:
- list
- assignee
- attach
- detach
type: string
AuthtypesRole:
properties:
@@ -2206,7 +2207,7 @@ components:
- role
- organization
- metaresource
- metaresources
- telemetryresource
type: string
DashboardtypesDashboard:
properties:
@@ -2728,6 +2729,82 @@ components:
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesJobRecord:
properties:
activePods:
type: integer
desiredSuccessfulPods:
type: integer
failedPods:
type: integer
jobCPU:
format: double
type: number
jobCPULimit:
format: double
type: number
jobCPURequest:
format: double
type: number
jobMemory:
format: double
type: number
jobMemoryLimit:
format: double
type: number
jobMemoryRequest:
format: double
type: number
jobName:
type: string
meta:
additionalProperties:
type: string
nullable: true
type: object
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
successfulPods:
type: integer
required:
- jobName
- jobCPU
- jobCPURequest
- jobCPULimit
- jobMemory
- jobMemoryRequest
- jobMemoryLimit
- desiredSuccessfulPods
- activePods
- failedPods
- successfulPods
- podCountsByPhase
- meta
type: object
InframonitoringtypesJobs:
properties:
endTimeBeforeRetention:
type: boolean
records:
items:
$ref: '#/components/schemas/InframonitoringtypesJobRecord'
nullable: true
type: array
requiredMetricsCheck:
$ref: '#/components/schemas/InframonitoringtypesRequiredMetricsCheck'
total:
type: integer
type:
$ref: '#/components/schemas/InframonitoringtypesResponseType'
warning:
$ref: '#/components/schemas/Querybuildertypesv5QueryWarnData'
required:
- type
- records
- total
- requiredMetricsCheck
- endTimeBeforeRetention
type: object
InframonitoringtypesNamespaceRecord:
properties:
meta:
@@ -3031,6 +3108,32 @@ components:
- end
- limit
type: object
InframonitoringtypesPostableJobs:
properties:
end:
format: int64
type: integer
filter:
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
nullable: true
type: array
limit:
type: integer
offset:
type: integer
orderBy:
$ref: '#/components/schemas/Querybuildertypesv5OrderBy'
start:
format: int64
type: integer
required:
- start
- end
- limit
type: object
InframonitoringtypesPostableNamespaces:
properties:
end:
@@ -9194,9 +9297,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- role:list
- tokenizer:
- ADMIN
- role:list
summary: List roles
tags:
- role
@@ -9268,9 +9371,9 @@ paths:
description: Not Implemented
security:
- api_key:
- ADMIN
- role:create
- tokenizer:
- ADMIN
- role:create
summary: Create role
tags:
- role
@@ -9330,9 +9433,9 @@ paths:
description: Not Implemented
security:
- api_key:
- ADMIN
- role:delete
- tokenizer:
- ADMIN
- role:delete
summary: Delete role
tags:
- role
@@ -9381,9 +9484,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- role:read
- tokenizer:
- ADMIN
- role:read
summary: Get role
tags:
- role
@@ -9447,9 +9550,9 @@ paths:
description: Not Implemented
security:
- api_key:
- ADMIN
- role:update
- tokenizer:
- ADMIN
- role:update
summary: Patch role
tags:
- role
@@ -9525,9 +9628,9 @@ paths:
description: Not Implemented
security:
- api_key:
- ADMIN
- role:read
- tokenizer:
- ADMIN
- role:read
summary: Get objects for a role by relation
tags:
- role
@@ -9603,9 +9706,9 @@ paths:
description: Not Implemented
security:
- api_key:
- ADMIN
- role:update
- tokenizer:
- ADMIN
- role:update
summary: Patch objects for a role by relation
tags:
- role
@@ -10209,9 +10312,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- serviceaccount:read
- factor-api-key:list
- tokenizer:
- serviceaccount:read
- factor-api-key:list
summary: List service account keys
tags:
- serviceaccount
@@ -10277,9 +10380,11 @@ paths:
description: Internal Server Error
security:
- api_key:
- serviceaccount:update
- factor-api-key:create
- serviceaccount:attach
- tokenizer:
- serviceaccount:update
- factor-api-key:create
- serviceaccount:attach
summary: Create a service account key
tags:
- serviceaccount
@@ -10332,9 +10437,11 @@ paths:
description: Internal Server Error
security:
- api_key:
- serviceaccount:update
- factor-api-key:delete
- serviceaccount:detach
- tokenizer:
- serviceaccount:update
- factor-api-key:delete
- serviceaccount:detach
summary: Revoke a service account key
tags:
- serviceaccount
@@ -10397,9 +10504,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- serviceaccount:update
- factor-api-key:update
- tokenizer:
- serviceaccount:update
- factor-api-key:update
summary: Updates a service account key
tags:
- serviceaccount
@@ -10571,11 +10678,11 @@ paths:
description: Internal Server Error
security:
- api_key:
- serviceaccount:attach
- role:attach
- serviceaccount:detach
- role:detach
- tokenizer:
- serviceaccount:attach
- role:attach
- serviceaccount:detach
- role:detach
summary: Delete service account role
tags:
- serviceaccount
@@ -12311,6 +12418,84 @@ paths:
summary: List Hosts for Infra Monitoring
tags:
- inframonitoring
/api/v2/infra_monitoring/jobs:
post:
deprecated: false
description: 'Returns a paginated list of Kubernetes Jobs with key aggregated
pod metrics: CPU usage and memory working set summed across pods owned by
the job, plus average CPU/memory request and limit utilization (jobCPURequest,
jobCPULimit, jobMemoryRequest, jobMemoryLimit). Each row also reports the
latest known job-level counters from kube-state-metrics: desiredSuccessfulPods
(k8s.job.desired_successful_pods, the target completion count), activePods
(k8s.job.active_pods), failedPods (k8s.job.failed_pods, cumulative across
the lifetime of the job), and successfulPods (k8s.job.successful_pods, cumulative).
It also reports per-group podCountsByPhase ({ pending, running, succeeded,
failed, unknown } from each pod''s latest k8s.pod.phase value); note podCountsByPhase.failed
(current pod-phase) is distinct from failedPods (cumulative job kube-state-metric).
Each job includes metadata attributes (k8s.job.name, k8s.namespace.name, k8s.cluster.name).
The response type is ''list'' for the default k8s.job.name grouping or ''grouped_list''
for custom groupBy keys; in both modes every row aggregates pods owned by
jobs in the group. Supports filtering via a filter expression, custom groupBy,
ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit
/ desired_successful_pods / active_pods / failed_pods / successful_pods, and
pagination via offset/limit. Also reports missing required metrics and whether
the requested time range falls before the data retention boundary. Numeric
metric fields (jobCPU, jobCPURequest, jobCPULimit, jobMemory, jobMemoryRequest,
jobMemoryLimit, desiredSuccessfulPods, activePods, failedPods, successfulPods)
return -1 as a sentinel when no data is available for that field.'
operationId: ListJobs
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/InframonitoringtypesPostableJobs'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/InframonitoringtypesJobs'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: List Jobs for Infra Monitoring
tags:
- inframonitoring
/api/v2/infra_monitoring/namespaces:
post:
deprecated: false

View File

@@ -87,7 +87,7 @@ func (provider *provider) BatchCheck(ctx context.Context, tupleReq map[string]*o
}
func (provider *provider) CheckTransactions(ctx context.Context, subject string, orgID valuer.UUID, transactions []*authtypes.Transaction) ([]*authtypes.TransactionWithAuthorization, error) {
tuples, err := authtypes.NewTuplesFromTransactions(transactions, subject, orgID)
tuples, correlations, err := authtypes.NewTuplesFromTransactionsWithCorrelations(transactions, subject, orgID)
if err != nil {
return nil, err
}
@@ -99,10 +99,21 @@ func (provider *provider) CheckTransactions(ctx context.Context, subject string,
results := make([]*authtypes.TransactionWithAuthorization, len(transactions))
for i, txn := range transactions {
result := batchResults[txn.ID.StringValue()]
txnID := txn.ID.StringValue()
authorized := batchResults[txnID].Authorized
if !authorized {
for _, correlationID := range correlations[txnID] {
if result, exists := batchResults[correlationID]; exists && result.Authorized {
authorized = true
break
}
}
}
results[i] = &authtypes.TransactionWithAuthorization{
Transaction: txn,
Authorized: result.Authorized,
Authorized: authorized,
}
}
return results, nil

View File

@@ -7,17 +7,27 @@ type organization
type user
relations
define create: [user, serviceaccount, role#assignee]
define list: [user, serviceaccount, role#assignee]
define read: [user, serviceaccount, role#assignee]
define update: [user, serviceaccount, role#assignee]
define delete: [user, serviceaccount, role#assignee]
define attach: [user, serviceaccount, role#assignee]
define detach: [user, serviceaccount, role#assignee]
type serviceaccount
relations
define create: [user, serviceaccount, role#assignee]
define list: [user, serviceaccount, role#assignee]
define read: [user, serviceaccount, role#assignee]
define update: [user, serviceaccount, role#assignee]
define delete: [user, serviceaccount, role#assignee]
define attach: [user, serviceaccount, role#assignee]
define detach: [user, serviceaccount, role#assignee]
type anonymous
@@ -25,25 +35,28 @@ type role
relations
define assignee: [user, serviceaccount, anonymous]
define create: [user, serviceaccount, role#assignee]
define list: [user, serviceaccount, role#assignee]
define read: [user, serviceaccount, role#assignee]
define update: [user, serviceaccount, role#assignee]
define delete: [user, serviceaccount, role#assignee]
define attach: [user, serviceaccount, role#assignee]
type metaresources
define attach: [user, serviceaccount, role#assignee]
define detach: [user, serviceaccount, role#assignee]
type metaresource
relations
define create: [user, serviceaccount, role#assignee]
define list: [user, serviceaccount, role#assignee]
type metaresource
relations
define read: [user, serviceaccount, anonymous, role#assignee]
define update: [user, serviceaccount, role#assignee]
define delete: [user, serviceaccount, role#assignee]
define read: [user, serviceaccount, anonymous, role#assignee]
define update: [user, serviceaccount, role#assignee]
define delete: [user, serviceaccount, role#assignee]
define block: [user, serviceaccount, role#assignee]
define block: [user, serviceaccount, role#assignee]
type telemetryresource
relations
define read: [user, serviceaccount, role#assignee]
define read: [user, serviceaccount, role#assignee]

View File

@@ -5,9 +5,15 @@ cd frontend && pnpm run commitlint --edit $1
branch="$(git rev-parse --abbrev-ref HEAD)"
color_red="$(tput setaf 1)"
bold="$(tput bold)"
reset="$(tput sgr0)"
if [ -n "$TERM" ] && [ "$TERM" != "dumb" ]; then
color_red="$(tput setaf 1)"
bold="$(tput bold)"
reset="$(tput sgr0)"
else
color_red=""
bold=""
reset=""
fi
if [ "$branch" = "main" ]; then
echo "${color_red}${bold}You can't commit directly to the main branch${reset}"

View File

@@ -1,4 +1,5 @@
registry = 'https://registry.npmjs.org/'
engine-strict=true
public-hoist-pattern[]=@commitlint*
public-hoist-pattern[]=commitlint

View File

@@ -4,6 +4,7 @@
"description": "",
"type": "module",
"scripts": {
"preinstall": "npx only-allow pnpm",
"i18n:generate-hash": "node ./i18-generate-hash.cjs",
"dev": "vite",
"build": "vite build",
@@ -26,7 +27,8 @@
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh"
},
"engines": {
"node": ">=22.0.0"
"node": ">=22.0.0",
"pnpm": ">=10.0.0 <11.0.0"
},
"author": "",
"license": "ISC",
@@ -51,7 +53,7 @@
"@signozhq/design-tokens": "2.1.4",
"@signozhq/icons": "0.4.0",
"@signozhq/resizable": "0.0.2",
"@signozhq/ui": "0.0.18",
"@signozhq/ui": "0.0.19",
"@tanstack/react-table": "8.21.3",
"@tanstack/react-virtual": "3.13.22",
"@uiw/codemirror-theme-copilot": "4.23.11",

View File

@@ -89,8 +89,8 @@ importers:
specifier: 0.0.2
version: 0.0.2(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@signozhq/ui':
specifier: 0.0.18
version: 0.0.18(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.27.0(react@18.2.0))(react@18.2.0)
specifier: 0.0.19
version: 0.0.19(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.27.0(react@18.2.0))(react@18.2.0)
'@tanstack/react-table':
specifier: 8.21.3
version: 8.21.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -1907,89 +1907,105 @@ packages:
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
@@ -2344,48 +2360,56 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-arm64-musl@0.47.0':
resolution: {integrity: sha512-IxtQC/sbBi4ubbY+MdwdanRWrG9InQJVZqyMsBa5IUaQcnSg86gQme574HxXMC1p4bo4YhV99zQ+wNnGCvEgzw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxfmt/binding-linux-ppc64-gnu@0.47.0':
resolution: {integrity: sha512-EWXEhOMbWO0q6eJSbu0QLkU8cKi0ljlYLngeDs2Ocu/pm1rrLwyQiYzlFbdnMRURI4w9ndr1sI9rSbhlJ5o23Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-riscv64-gnu@0.47.0':
resolution: {integrity: sha512-tZrjS11TUiDuEpRaqdk8K9F9xETRyKXfuZKmdeW+Gj7coBnm7+8sBEfyt033EAFEQSlkniAXvBLh+Qja2ioGBQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-riscv64-musl@0.47.0':
resolution: {integrity: sha512-KBFy+2CFKUCZzYwX2ZOPQKck1vjQbz+hextuc19G4r0WRJwadfAeuQMQRQvB+Ivc8brlbOVg7et8K7E467440g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@oxfmt/binding-linux-s390x-gnu@0.47.0':
resolution: {integrity: sha512-REUPFKVGSiK99B+9eaPhluEVglzaoj/SMykNC5SUiV2RSsBfV5lWN7Y0iCIc251Wz3GaeAGZsJ/zj3gjarxdFg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-x64-gnu@0.47.0':
resolution: {integrity: sha512-KVftVSVEDeIfRW3TIeLe3aNI/iY4m1fu5mDwHcisKMZSCMKLkrhFsjowC7o9RoqNPxbbglm2+/6KAKBIts2t0Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-x64-musl@0.47.0':
resolution: {integrity: sha512-DTsmGEaA2860Aq5VUyDO8/MT9NFxwVL93RnRYmpMwK6DsSkThmvEpqoUDDljziEpAedMRG19SCogrNbINSbLUQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxfmt/binding-openharmony-arm64@0.47.0':
resolution: {integrity: sha512-8r5BDro7fLOBoq1JXHLVSs55OlrxQhEso4HVo0TcY7OXJUPYfjPoOaYL5us+yIwqyP9rQwN+rxuiNFSmaxSuOQ==}
@@ -2488,48 +2512,56 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-arm64-musl@1.62.0':
resolution: {integrity: sha512-8eCy3FCDuWUM5hWujAv6heMvfZPbcCOU3SdQUAkixZLu5bSzOkNfirJiLGoQFO943xceOKkiQRMQNzH++jM3WA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxlint/binding-linux-ppc64-gnu@1.62.0':
resolution: {integrity: sha512-NjQ7K7tpTPDe9J+yq8p/s/J0E7lRCkK2uDBDqvT4XIT6f4Z0tlnr59OBg/WcrmVHER1AbrcfyxhGTXgcG8ytWg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-riscv64-gnu@1.62.0':
resolution: {integrity: sha512-oKZed9gmSwze29dEt3/Wnsv6l/Ygw/FUst+8Kfpv2SGeS/glEoTGZAMQw37SVyzFV76UTHJN2snGgxK2t2+8ow==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-riscv64-musl@1.62.0':
resolution: {integrity: sha512-gBjBxQ+9lGpAYq+ELqw0w8QXsBnkZclFc7GRX2r0LnEVn3ZTEqeIKpKcGjucmp76Q53bvJD0i4qBWBhcfhSfGA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@oxlint/binding-linux-s390x-gnu@1.62.0':
resolution: {integrity: sha512-Ew2Kxs9EQ9/mbAIJ2hvocMC0wsOu6YKzStI2eFBDt+Td5O8seVC/oxgRIHqCcl5sf5ratA1nozQBAuv7tphkHg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-x64-gnu@1.62.0':
resolution: {integrity: sha512-5z25jcAA0gfKyVwz71A0VXgaPlocPoTAxhlv/hgoK6tlCrfoNuw7haWbDHvGMfjXhdic4EqVXGRv5XsTqFnbRQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-x64-musl@1.62.0':
resolution: {integrity: sha512-IWpHmMB6ZDllPvqWDkG6AmXrN7JF5e/c4g/0PuURsmlK+vHoYZPB70rr4u1bn3I4LsKCSpqqfveyx6UCOC8wdg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxlint/binding-openharmony-arm64@1.62.0':
resolution: {integrity: sha512-fjlSxxrD5pA594vkyikCS9MnPRjQawW6/BLgyTYkO+73wwPlYjkcZ7LSd974l0Q2zkHQmu4DPvJFLYA7o8xrxQ==}
@@ -2584,36 +2616,42 @@ packages:
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm-musl@2.5.1':
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-arm64-glibc@2.5.1':
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm64-musl@2.5.1':
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-x64-glibc@2.5.1':
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-x64-musl@2.5.1':
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@parcel/watcher-win32-arm64@2.5.1':
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
@@ -3474,24 +3512,28 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.53':
resolution: {integrity: sha512-bGe5EBB8FVjHBR1mOLOPEFg1Lp3//7geqWkU5NIhxe+yH0W8FVrQ6WRYOap4SUTKdklD/dC4qPLREkMMQ855FA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.53':
resolution: {integrity: sha512-qL+63WKVQs1CMvFedlPt0U9PiEKJOAL/bsHMKUDS6Vp2Q+YAv/QLPu8rcvkfIMvQ0FPU2WL0aX4eWwF6e/GAnA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.0.0-beta.53':
resolution: {integrity: sha512-VGl9JIGjoJh3H8Mb+7xnVqODajBmrdOOb9lxWXdcmxyI+zjB2sux69br0hZJDTyLJfvBoYm439zPACYbCjGRmw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@rolldown/binding-openharmony-arm64@1.0.0-beta.53':
resolution: {integrity: sha512-B4iIserJXuSnNzA5xBLFUIjTfhNy7d9sq4FUMQY3GhQWGVhS2RWWzzDnkSU6MUt7/aHUrep0CdQfXUJI9D3W7A==}
@@ -3644,8 +3686,8 @@ packages:
peerDependencies:
react: ^18.2.0
'@signozhq/ui@0.0.18':
resolution: {integrity: sha512-1p3ALh76kafiz5yX7ReNKVcHDt2od7CcZD/Vx9i2adTwTeynkLJcEfVoXoJD3oh1kKTleooOiOjRyxlA7VzmSA==}
'@signozhq/ui@0.0.19':
resolution: {integrity: sha512-2q6aRxN/PR4PlR2xJZAREEuvLPiDFggfFKzCW2Z5vHVVbrgnvZHWD1jPUuwszfEg0ceH3UvkwqceO7wN4uRJAA==}
peerDependencies:
'@signozhq/icons': 0.3.0
react: ^18.2.0
@@ -4266,41 +4308,49 @@ packages:
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
cpu: [x64]
os: [linux]
libc: [musl]
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
@@ -4367,7 +4417,7 @@ packages:
resolution: {integrity: sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==}
engines: {node: ^20.19.0 || >=22.12.0}
peerDependencies:
vite: npm:rolldown-vite@7.3.1
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
'@webassemblyjs/ast@1.14.1':
resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==}
@@ -7194,24 +7244,28 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.31.1:
resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.31.1:
resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.31.1:
resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.31.1:
resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==}
@@ -10239,7 +10293,7 @@ packages:
oxlint: '>=1'
stylelint: '>=16'
typescript: '*'
vite: npm:rolldown-vite@7.3.1
vite: '>=5.4.21'
vls: '*'
vti: '*'
vue-tsc: ~2.2.10 || ^3.0.0
@@ -10268,12 +10322,12 @@ packages:
vite-plugin-compression@0.5.1:
resolution: {integrity: sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==}
peerDependencies:
vite: npm:rolldown-vite@7.3.1
vite: '>=2.0.0'
vite-plugin-html@3.2.2:
resolution: {integrity: sha512-vb9C9kcdzcIo/Oc3CLZVS03dL5pDlOFuhGlZYDCJ840BhWl/0nGeZWf3Qy7NlOayscY4Cm/QRgULCQkEZige5Q==}
peerDependencies:
vite: npm:rolldown-vite@7.3.1
vite: '>=2.0.0'
vite-plugin-image-optimizer@2.0.3:
resolution: {integrity: sha512-1vrFOTcpSvv6DCY7h8UXab4wqMAjTJB/ndOzG/Kmj1oDOuPF6mbjkNQoGzzCEYeWGe7qU93jc8oQqvoJ57al3A==}
@@ -10281,7 +10335,7 @@ packages:
peerDependencies:
sharp: '>=0.34.0'
svgo: '>=4'
vite: npm:rolldown-vite@7.3.1
vite: '>=5'
peerDependenciesMeta:
sharp:
optional: true
@@ -10291,7 +10345,7 @@ packages:
vite-tsconfig-paths@6.1.1:
resolution: {integrity: sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==}
peerDependencies:
vite: npm:rolldown-vite@7.3.1
vite: '*'
void-elements@3.1.0:
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
@@ -13901,7 +13955,7 @@ snapshots:
- react-dom
- tailwindcss
'@signozhq/ui@0.0.18(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.27.0(react@18.2.0))(react@18.2.0)':
'@signozhq/ui@0.0.19(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.27.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@chenglou/pretext': 0.0.5
'@radix-ui/react-checkbox': 1.3.3(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)

View File

@@ -15,6 +15,7 @@ import type {
InframonitoringtypesPostableClustersDTO,
InframonitoringtypesPostableDeploymentsDTO,
InframonitoringtypesPostableHostsDTO,
InframonitoringtypesPostableJobsDTO,
InframonitoringtypesPostableNamespacesDTO,
InframonitoringtypesPostableNodesDTO,
InframonitoringtypesPostablePodsDTO,
@@ -23,6 +24,7 @@ import type {
ListClusters200,
ListDeployments200,
ListHosts200,
ListJobs200,
ListNamespaces200,
ListNodes200,
ListPods200,
@@ -286,6 +288,90 @@ export const useListHosts = <
return useMutation(mutationOptions);
};
/**
* Returns a paginated list of Kubernetes Jobs with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the job, plus average CPU/memory request and limit utilization (jobCPURequest, jobCPULimit, jobMemoryRequest, jobMemoryLimit). Each row also reports the latest known job-level counters from kube-state-metrics: desiredSuccessfulPods (k8s.job.desired_successful_pods, the target completion count), activePods (k8s.job.active_pods), failedPods (k8s.job.failed_pods, cumulative across the lifetime of the job), and successfulPods (k8s.job.successful_pods, cumulative). It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value); note podCountsByPhase.failed (current pod-phase) is distinct from failedPods (cumulative job kube-state-metric). Each job includes metadata attributes (k8s.job.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.job.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by jobs in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_successful_pods / active_pods / failed_pods / successful_pods, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (jobCPU, jobCPURequest, jobCPULimit, jobMemory, jobMemoryRequest, jobMemoryLimit, desiredSuccessfulPods, activePods, failedPods, successfulPods) return -1 as a sentinel when no data is available for that field.
* @summary List Jobs for Infra Monitoring
*/
export const listJobs = (
inframonitoringtypesPostableJobsDTO: BodyType<InframonitoringtypesPostableJobsDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ListJobs200>({
url: `/api/v2/infra_monitoring/jobs`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: inframonitoringtypesPostableJobsDTO,
signal,
});
};
export const getListJobsMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof listJobs>>,
TError,
{ data: BodyType<InframonitoringtypesPostableJobsDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof listJobs>>,
TError,
{ data: BodyType<InframonitoringtypesPostableJobsDTO> },
TContext
> => {
const mutationKey = ['listJobs'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof listJobs>>,
{ data: BodyType<InframonitoringtypesPostableJobsDTO> }
> = (props) => {
const { data } = props ?? {};
return listJobs(data);
};
return { mutationFn, ...mutationOptions };
};
export type ListJobsMutationResult = NonNullable<
Awaited<ReturnType<typeof listJobs>>
>;
export type ListJobsMutationBody =
BodyType<InframonitoringtypesPostableJobsDTO>;
export type ListJobsMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List Jobs for Infra Monitoring
*/
export const useListJobs = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof listJobs>>,
TError,
{ data: BodyType<InframonitoringtypesPostableJobsDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof listJobs>>,
TError,
{ data: BodyType<InframonitoringtypesPostableJobsDTO> },
TContext
> => {
const mutationOptions = getListJobsMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* Returns a paginated list of Kubernetes namespaces with key aggregated pod metrics: CPU usage and memory working set (summed across pods in the group), plus per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value in the window). Each namespace includes metadata attributes (k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.namespace.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / memory, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (namespaceCPU, namespaceMemory) return -1 as a sentinel when no data is available for that field.
* @summary List Namespaces for Infra Monitoring

View File

@@ -1840,6 +1840,7 @@ export enum AuthtypesRelationDTO {
list = 'list',
assignee = 'assignee',
attach = 'attach',
detach = 'detach',
}
export interface AuthtypesRoleDTO {
/**
@@ -4161,7 +4162,7 @@ export enum CoretypesTypeDTO {
role = 'role',
organization = 'organization',
metaresource = 'metaresource',
metaresources = 'metaresources',
telemetryresource = 'telemetryresource',
}
export interface DashboardtypesDashboardDTO {
/**
@@ -4790,6 +4791,91 @@ export interface InframonitoringtypesHostsDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
/**
* @nullable
*/
export type InframonitoringtypesJobRecordDTOMeta = {
[key: string]: string;
} | null;
export interface InframonitoringtypesJobRecordDTO {
/**
* @type integer
*/
activePods: number;
/**
* @type integer
*/
desiredSuccessfulPods: number;
/**
* @type integer
*/
failedPods: number;
/**
* @type number
* @format double
*/
jobCPU: number;
/**
* @type number
* @format double
*/
jobCPULimit: number;
/**
* @type number
* @format double
*/
jobCPURequest: number;
/**
* @type number
* @format double
*/
jobMemory: number;
/**
* @type number
* @format double
*/
jobMemoryLimit: number;
/**
* @type number
* @format double
*/
jobMemoryRequest: number;
/**
* @type string
*/
jobName: string;
/**
* @type object
* @nullable true
*/
meta: InframonitoringtypesJobRecordDTOMeta;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
/**
* @type integer
*/
successfulPods: number;
}
export interface InframonitoringtypesJobsDTO {
/**
* @type boolean
*/
endTimeBeforeRetention: boolean;
/**
* @type array
* @nullable true
*/
records: InframonitoringtypesJobRecordDTO[] | null;
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
total: number;
type: InframonitoringtypesResponseTypeDTO;
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
/**
* @nullable
*/
@@ -5106,6 +5192,34 @@ export interface InframonitoringtypesPostableHostsDTO {
start: number;
}
export interface InframonitoringtypesPostableJobsDTO {
/**
* @type integer
* @format int64
*/
end: number;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array
* @nullable true
*/
groupBy?: Querybuildertypesv5GroupByKeyDTO[] | null;
/**
* @type integer
*/
limit: number;
/**
* @type integer
*/
offset?: number;
orderBy?: Querybuildertypesv5OrderByDTO;
/**
* @type integer
* @format int64
*/
start: number;
}
export interface InframonitoringtypesPostableNamespacesDTO {
/**
* @type integer
@@ -9735,6 +9849,14 @@ export type ListHosts200 = {
status: string;
};
export type ListJobs200 = {
data: InframonitoringtypesJobsDTO;
/**
* @type string
*/
status: string;
};
export type ListNamespaces200 = {
data: InframonitoringtypesNamespacesDTO;
/**

View File

@@ -2,7 +2,7 @@ import { useCallback, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { Dot, Sparkles } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Tooltip } from '@signozhq/ui/tooltip';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Popover } from 'antd';
import logEvent from 'api/common/logEvent';
import {
@@ -97,7 +97,7 @@ function HeaderRightSection({
</span>
) : null}
<Tooltip title="AI Assistant">
<TooltipSimple title="AI Assistant">
<Button
variant="solid"
color="secondary"
@@ -113,7 +113,7 @@ function HeaderRightSection({
>
AI Assistant
</Button>
</Tooltip>
</TooltipSimple>
</div>
)}

View File

@@ -360,8 +360,7 @@ describe('createGuardedRoute', () => {
const obj = payload[0]?.object;
const kind = obj?.resource?.kind;
const selector = obj?.selector ?? '*';
const objectStr =
obj?.resource?.type === 'metaresources' ? kind : `${kind}:${selector}`;
const objectStr = `${kind}:${selector}`;
requestedObjects.push(objectStr ?? '');
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));

View File

@@ -1,7 +1,7 @@
import { useCallback } from 'react';
import { useHistory } from 'react-router-dom';
import { Button } from '@signozhq/ui/button';
import { Tooltip } from '@signozhq/ui/tooltip';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Drawer } from 'antd';
import ROUTES from 'constants/routes';
import { Maximize2, MessageSquare, Plus, X } from '@signozhq/icons';
@@ -52,7 +52,7 @@ export default function AIAssistantDrawer(): JSX.Element {
</div>
<div>
<Tooltip title="New conversation">
<TooltipSimple title="New conversation">
<Button
variant="ghost"
size="icon"
@@ -62,9 +62,9 @@ export default function AIAssistantDrawer(): JSX.Element {
>
<Plus size={16} />
</Button>
</Tooltip>
</TooltipSimple>
<Tooltip title="Open full screen">
<TooltipSimple title="Open full screen">
<Button
variant="ghost"
size="icon"
@@ -75,9 +75,9 @@ export default function AIAssistantDrawer(): JSX.Element {
>
<Maximize2 size={16} />
</Button>
</Tooltip>
</TooltipSimple>
<Tooltip title="Close">
<TooltipSimple title="Close">
<Button
variant="ghost"
size="icon"
@@ -87,7 +87,7 @@ export default function AIAssistantDrawer(): JSX.Element {
>
<X size={16} />
</Button>
</Tooltip>
</TooltipSimple>
</div>
</div>
}

View File

@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { useHistory } from 'react-router-dom';
import { Button } from '@signozhq/ui/button';
import { Tooltip } from '@signozhq/ui/tooltip';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import ROUTES from 'constants/routes';
import { History, Maximize2, Minus, Plus, Sparkles, X } from '@signozhq/icons';
@@ -132,7 +132,7 @@ export default function AIAssistantModal(): JSX.Element | null {
</div>
<div className={styles.actions}>
<Tooltip title={showHistory ? 'Back to chat' : 'Conversations'}>
<TooltipSimple title={showHistory ? 'Back to chat' : 'Conversations'}>
<Button
variant="ghost"
size="icon"
@@ -142,9 +142,9 @@ export default function AIAssistantModal(): JSX.Element | null {
>
<History size={14} />
</Button>
</Tooltip>
</TooltipSimple>
<Tooltip title="New conversation">
<TooltipSimple title="New conversation">
<Button
variant="ghost"
size="icon"
@@ -153,9 +153,9 @@ export default function AIAssistantModal(): JSX.Element | null {
>
<Plus size={14} />
</Button>
</Tooltip>
</TooltipSimple>
<Tooltip title="Open full screen">
<TooltipSimple title="Open full screen">
<Button
variant="ghost"
size="icon"
@@ -165,9 +165,9 @@ export default function AIAssistantModal(): JSX.Element | null {
>
<Maximize2 size={14} />
</Button>
</Tooltip>
</TooltipSimple>
<Tooltip title="Minimize to side panel">
<TooltipSimple title="Minimize to side panel">
<Button
variant="ghost"
size="icon"
@@ -176,9 +176,9 @@ export default function AIAssistantModal(): JSX.Element | null {
>
<Minus size={14} />
</Button>
</Tooltip>
</TooltipSimple>
<Tooltip title="Close">
<TooltipSimple title="Close">
<Button
variant="ghost"
size="icon"
@@ -187,7 +187,7 @@ export default function AIAssistantModal(): JSX.Element | null {
>
<X size={14} />
</Button>
</Tooltip>
</TooltipSimple>
</div>
</div>

View File

@@ -1,7 +1,7 @@
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
import { matchPath, useHistory, useLocation } from 'react-router-dom';
import { Button } from '@signozhq/ui/button';
import { Tooltip } from '@signozhq/ui/tooltip';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import ROUTES from 'constants/routes';
import { History, Maximize2, Plus, Sparkles, X } from '@signozhq/icons';
@@ -125,7 +125,7 @@ export default function AIAssistantPanel(): JSX.Element | null {
</div>
<div className={styles.actions}>
<Tooltip title={showHistory ? 'Back to chat' : 'Conversations'}>
<TooltipSimple title={showHistory ? 'Back to chat' : 'Conversations'}>
<Button
variant="ghost"
size="icon"
@@ -135,9 +135,9 @@ export default function AIAssistantPanel(): JSX.Element | null {
>
<History size={14} />
</Button>
</Tooltip>
</TooltipSimple>
<Tooltip title="New conversation">
<TooltipSimple title="New conversation">
<Button
variant="ghost"
size="icon"
@@ -147,9 +147,9 @@ export default function AIAssistantPanel(): JSX.Element | null {
>
<Plus size={14} />
</Button>
</Tooltip>
</TooltipSimple>
<Tooltip title="Open full screen">
<TooltipSimple title="Open full screen">
<Button
variant="ghost"
size="icon"
@@ -160,9 +160,9 @@ export default function AIAssistantPanel(): JSX.Element | null {
>
<Maximize2 size={14} />
</Button>
</Tooltip>
</TooltipSimple>
<Tooltip title="Close">
<TooltipSimple title="Close">
<Button
variant="ghost"
size="icon"
@@ -172,7 +172,7 @@ export default function AIAssistantPanel(): JSX.Element | null {
>
<X size={14} />
</Button>
</Tooltip>
</TooltipSimple>
</div>
</div>

View File

@@ -1,6 +1,6 @@
import { matchPath, useLocation } from 'react-router-dom';
import { Button } from '@signozhq/ui/button';
import { Tooltip } from '@signozhq/ui/tooltip';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import ROUTES from 'constants/routes';
import { Bot } from '@signozhq/icons';
@@ -30,7 +30,7 @@ export default function AIAssistantTrigger(): JSX.Element | null {
}
return (
<Tooltip title="AI Assistant">
<TooltipSimple title="AI Assistant">
<Button
variant="solid"
color="primary"
@@ -40,6 +40,6 @@ export default function AIAssistantTrigger(): JSX.Element | null {
>
<Bot size={20} />
</Button>
</Tooltip>
</TooltipSimple>
);
}

View File

@@ -12,7 +12,7 @@ import {
import cx from 'classnames';
import { v4 as uuidv4 } from 'uuid';
import { Button } from '@signozhq/ui/button';
import { Tooltip } from '@signozhq/ui/tooltip';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import type { MessageActionDTO } from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
import {
ApplyFilterSignalDTO,
@@ -524,9 +524,9 @@ export default function ActionsSection({
);
return tooltip ? (
<Tooltip key={key} title={tooltip}>
<TooltipSimple key={key} title={tooltip}>
{chip}
</Tooltip>
</TooltipSimple>
) : (
<span key={key}>{chip}</span>
);

View File

@@ -5,7 +5,7 @@ import { Badge } from '@signozhq/ui/badge';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { Popover, PopoverContent, PopoverTrigger } from '@signozhq/ui/popover';
import { Tooltip } from '@signozhq/ui/tooltip';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import type { UploadFile } from 'antd';
import {
getListRulesQueryKey,
@@ -899,7 +899,7 @@ export default function ChatInput({
</div>
</div>
) : (
<Tooltip title="Voice input">
<TooltipSimple title="Voice input">
<Button
variant="ghost"
size="icon"
@@ -910,11 +910,11 @@ export default function ChatInput({
>
<Mic size={14} />
</Button>
</Tooltip>
</TooltipSimple>
))}
{isStreaming && onCancel ? (
<Tooltip title="Stop generating">
<TooltipSimple title="Stop generating">
<Button
variant="solid"
size="icon"
@@ -924,7 +924,7 @@ export default function ChatInput({
>
<Square size={10} fill="currentColor" strokeWidth={0} />
</Button>
</Tooltip>
</TooltipSimple>
) : (
<Button
variant="solid"

View File

@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
import cx from 'classnames';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { Tooltip } from '@signozhq/ui/tooltip';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Plus, Search } from '@signozhq/icons';
import { useAIAssistantStore } from '../../store/useAIAssistantStore';
@@ -157,7 +157,7 @@ export default function ConversationsList({
{isLoadingThreads && <HeaderLoadingDots />}
{!isLoadingThreads && showAddNewConversation && (
<Tooltip title="New conversation">
<TooltipSimple title="New conversation">
<Button
variant="solid"
size="sm"
@@ -167,7 +167,7 @@ export default function ConversationsList({
>
<Plus size={12} />
</Button>
</Tooltip>
</TooltipSimple>
)}
</div>

View File

@@ -3,7 +3,7 @@ import cx from 'classnames';
import { useCopyToClipboard } from 'react-use';
import { Button } from '@signozhq/ui/button';
import { DialogWrapper } from '@signozhq/ui/dialog';
import { Tooltip } from '@signozhq/ui/tooltip';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { Check, Copy, RefreshCw, ThumbsDown, ThumbsUp } from '@signozhq/icons';
import { useTimezone } from 'providers/Timezone';
@@ -126,7 +126,7 @@ export default function MessageFeedback({
<>
<div className={cx(styles.feedback, { [styles.visible]: isLastAssistant })}>
<div className={styles.actions}>
<Tooltip title={copied ? 'Copied!' : 'Copy'}>
<TooltipSimple title={copied ? 'Copied!' : 'Copy'}>
<Button
className={styles.btn}
size="icon"
@@ -136,9 +136,9 @@ export default function MessageFeedback({
>
{copied ? <Check size={12} /> : <Copy size={12} />}
</Button>
</Tooltip>
</TooltipSimple>
<Tooltip title="Good response">
<TooltipSimple title="Good response">
<Button
className={cx(styles.btn, { [styles.votedUp]: vote === 'positive' })}
size="icon"
@@ -148,9 +148,9 @@ export default function MessageFeedback({
>
<ThumbsUp size={12} />
</Button>
</Tooltip>
</TooltipSimple>
<Tooltip title="Bad response">
<TooltipSimple title="Bad response">
<Button
className={cx(styles.btn, {
[styles.votedDown]: vote === 'negative',
@@ -162,10 +162,10 @@ export default function MessageFeedback({
>
<ThumbsDown size={12} />
</Button>
</Tooltip>
</TooltipSimple>
{onRegenerate && (
<Tooltip title="Regenerate">
<TooltipSimple title="Regenerate">
<Button
className={styles.btn}
size="icon"
@@ -175,7 +175,7 @@ export default function MessageFeedback({
>
<RefreshCw size={12} />
</Button>
</Tooltip>
</TooltipSimple>
)}
</div>

View File

@@ -1,7 +1,7 @@
import { useCallback, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { Button } from '@signozhq/ui/button';
import { Tooltip } from '@signozhq/ui/tooltip';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Check, Copy } from '@signozhq/icons';
import { Message } from '../../types';
@@ -32,7 +32,7 @@ export default function UserMessageActions({
return (
<div className={styles.actions}>
<Tooltip title={copied ? 'Copied!' : 'Copy'}>
<TooltipSimple title={copied ? 'Copied!' : 'Copy'}>
<Button
className={styles.btn}
size="icon"
@@ -42,7 +42,7 @@ export default function UserMessageActions({
>
{copied ? <Check size={12} /> : <Copy size={12} />}
</Button>
</Tooltip>
</TooltipSimple>
</div>
);
}

View File

@@ -1,6 +1,6 @@
import { GetHosts200 } from 'api/generated/services/sigNoz.schemas';
import { rest, server } from 'mocks-server/server';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
import CustomDomainSettings from '../CustomDomainSettings';
@@ -44,18 +44,20 @@ const mockHostsResponse: GetHosts200 = {
};
describe('CustomDomainSettings', () => {
beforeEach(() => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
);
});
afterEach(() => {
server.resetHandlers();
mockToastCustom.mockClear();
});
it('renders active host URL in the trigger button', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
);
render(<CustomDomainSettings />);
// The active host is the non-default one (custom-host)
@@ -63,20 +65,11 @@ describe('CustomDomainSettings', () => {
});
it('opens edit modal when clicking the edit button', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/custom-host\.test\.cloud/i);
await user.click(
screen.getByRole('button', { name: /edit workspace link/i }),
);
fireEvent.click(screen.getByRole('button', { name: /edit workspace link/i }));
expect(
screen.getByRole('dialog', { name: /edit workspace link/i }),
@@ -89,28 +82,20 @@ describe('CustomDomainSettings', () => {
let capturedBody: Record<string, unknown> = {};
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
rest.put(ZEUS_HOSTS_ENDPOINT, async (req, res, ctx) => {
capturedBody = await req.json<Record<string, unknown>>();
return res(ctx.status(200), ctx.json({}));
}),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/custom-host\.test\.cloud/i);
await user.click(
screen.getByRole('button', { name: /edit workspace link/i }),
);
fireEvent.click(screen.getByRole('button', { name: /edit workspace link/i }));
// The input is inside the modal — find it by its role
const input = screen.getByRole('textbox');
await user.clear(input);
await user.type(input, 'myteam');
await user.click(screen.getByRole('button', { name: /apply changes/i }));
fireEvent.change(input, { target: { value: 'myteam' } });
fireEvent.click(screen.getByRole('button', { name: /apply changes/i }));
await waitFor(() => {
expect(capturedBody).toStrictEqual({ name: 'myteam' });
@@ -119,9 +104,6 @@ describe('CustomDomainSettings', () => {
it('shows contact support option when domain update returns 409', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
rest.put(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(
ctx.status(409),
@@ -130,18 +112,14 @@ describe('CustomDomainSettings', () => {
),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/custom-host\.test\.cloud/i);
await user.click(
screen.getByRole('button', { name: /edit workspace link/i }),
);
fireEvent.click(screen.getByRole('button', { name: /edit workspace link/i }));
const input = screen.getByRole('textbox');
await user.clear(input);
await user.type(input, 'myteam');
await user.click(screen.getByRole('button', { name: /apply changes/i }));
fireEvent.change(input, { target: { value: 'myteam' } });
fireEvent.click(screen.getByRole('button', { name: /apply changes/i }));
await expect(
screen.findByRole('button', { name: /contact support/i }),
@@ -149,24 +127,14 @@ describe('CustomDomainSettings', () => {
});
it('shows validation error when subdomain is less than 3 characters', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/custom-host\.test\.cloud/i);
await user.click(
screen.getByRole('button', { name: /edit workspace link/i }),
);
fireEvent.click(screen.getByRole('button', { name: /edit workspace link/i }));
const input = screen.getByRole('textbox');
await user.clear(input);
await user.type(input, 'ab');
await user.click(screen.getByRole('button', { name: /apply changes/i }));
fireEvent.change(input, { target: { value: 'ab' } });
fireEvent.click(screen.getByRole('button', { name: /apply changes/i }));
expect(
screen.getByText(/minimum 3 characters required/i),
@@ -174,19 +142,12 @@ describe('CustomDomainSettings', () => {
});
it('shows all workspace URLs as links in the dropdown', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/custom-host\.test\.cloud/i);
// Open the URL dropdown
await user.click(
fireEvent.click(
screen.getByRole('button', { name: /custom-host\.test\.cloud/i }),
);
@@ -207,26 +168,19 @@ describe('CustomDomainSettings', () => {
it('calls toast.custom with new URL after successful domain update', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
rest.put(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({})),
),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/custom-host\.test\.cloud/i);
await user.click(
screen.getByRole('button', { name: /edit workspace link/i }),
);
fireEvent.click(screen.getByRole('button', { name: /edit workspace link/i }));
const input = screen.getByRole('textbox');
await user.clear(input);
await user.type(input, 'myteam');
await user.click(screen.getByRole('button', { name: /apply changes/i }));
fireEvent.change(input, { target: { value: 'myteam' } });
fireEvent.click(screen.getByRole('button', { name: /apply changes/i }));
// Verify toast.custom was called
await waitFor(() => {
@@ -243,12 +197,6 @@ describe('CustomDomainSettings', () => {
describe('Workspace Name rendering', () => {
it('renders org displayName when available from appContext', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
);
render(<CustomDomainSettings />, undefined, {
appContextOverrides: {
org: [{ id: 'xyz', displayName: 'My Org Name', createdAt: 0 }],
@@ -259,12 +207,6 @@ describe('CustomDomainSettings', () => {
});
it('falls back to customDomainSubdomain when org displayName is missing', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
);
render(<CustomDomainSettings />, undefined, {
appContextOverrides: { org: [] },
});

View File

@@ -1,12 +1,6 @@
import setRetentionApiV2 from 'api/settings/setRetentionV2';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import {
fireEvent,
render,
screen,
userEvent,
waitFor,
} from 'tests/test-utils';
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
import { IDiskType } from 'types/api/disks/getDisks';
import {
PayloadPropsLogs,
@@ -115,8 +109,6 @@ describe('GeneralSettings - S3 Logs Retention', () => {
describe('Test 1: S3 Enabled - Only Days in Dropdown', () => {
it('should show only Days option for S3 retention and send correct API payload', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<GeneralSettings
metricsTtlValuesPayload={mockMetricsRetention}
@@ -159,8 +151,7 @@ describe('GeneralSettings - S3 Logs Retention', () => {
fireEvent.click(document.body);
// Change S3 retention value to 5 days
await user.clear(s3Input);
await user.type(s3Input, '5');
fireEvent.change(s3Input, { target: { value: '5' } });
// Find the save button in the Logs row
const saveButton = logsRow.querySelector(
@@ -217,8 +208,6 @@ describe('GeneralSettings - S3 Logs Retention', () => {
describe('Test 2: S3 Disabled - Field Hidden', () => {
it('should hide S3 retention field and send empty S3 values to API', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<GeneralSettings
metricsTtlValuesPayload={mockMetricsRetention}
@@ -245,7 +234,7 @@ describe('GeneralSettings - S3 Logs Retention', () => {
const totalDropdown = logsRow.querySelector(
'.ant-select-selector',
) as HTMLElement;
await user.click(totalDropdown);
fireEvent.mouseDown(totalDropdown);
// Wait for dropdown options to appear
await waitFor(() => {
@@ -259,11 +248,10 @@ describe('GeneralSettings - S3 Logs Retention', () => {
opt.textContent?.includes('Days'),
);
expect(daysOption).toBeInTheDocument();
await user.click(daysOption as HTMLElement);
fireEvent.click(daysOption as HTMLElement);
// Now change the value
await user.clear(totalInput);
await user.type(totalInput, '60');
fireEvent.change(totalInput, { target: { value: '60' } });
// Find the save button
const saveButton = logsRow.querySelector(
@@ -277,14 +265,14 @@ describe('GeneralSettings - S3 Logs Retention', () => {
});
// Click save button
await user.click(saveButton);
fireEvent.click(saveButton);
// Wait for modal to appear
const okButton = await screen.findByRole('button', { name: /ok/i });
expect(okButton).toBeInTheDocument();
// Click OK button
await user.click(okButton);
fireEvent.click(okButton);
// Verify API was called with empty S3 values (60 days)
await waitFor(() => {
@@ -333,8 +321,6 @@ describe('GeneralSettings - S3 Logs Retention', () => {
describe('Test 4: Save Button State with S3 Disabled', () => {
it('should disable save button when cold_storage_ttl_days is -1 and no changes made', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<GeneralSettings
metricsTtlValuesPayload={mockMetricsRetention}
@@ -365,8 +351,7 @@ describe('GeneralSettings - S3 Logs Retention', () => {
const totalInput = inputs[0] as HTMLInputElement;
// Change total retention value to trigger button enable
await user.clear(totalInput);
await user.type(totalInput, '60');
fireEvent.change(totalInput, { target: { value: '60' } });
// Button should now be enabled after change
await waitFor(() => {
@@ -374,8 +359,7 @@ describe('GeneralSettings - S3 Logs Retention', () => {
});
// Revert to original value (30 days displays as 1 Month)
await user.clear(totalInput);
await user.type(totalInput, '1');
fireEvent.change(totalInput, { target: { value: '1' } });
// Button should be disabled again (back to original state)
await waitFor(() => {

View File

@@ -1,5 +1,5 @@
import { Button } from '@signozhq/ui/button';
import { Tooltip, TooltipProvider } from '@signozhq/ui/tooltip';
import { TooltipSimple, TooltipProvider } from '@signozhq/ui/tooltip';
import { Copy } from '@signozhq/icons';
import './CopyIconButton.styles.scss';
@@ -20,7 +20,7 @@ function CopyIconButton({
return (
<TooltipProvider>
<Tooltip title={tooltipTitle}>
<TooltipSimple title={tooltipTitle}>
<span>
<Button
color="secondary"
@@ -33,7 +33,7 @@ function CopyIconButton({
onClick={onCopy}
/>
</span>
</Tooltip>
</TooltipSimple>
</TooltipProvider>
);
}

View File

@@ -1,6 +1,6 @@
import type { TypesUserDTO } from 'api/generated/services/sigNoz.schemas';
import { rest, server } from 'mocks-server/server';
import { render, screen, userEvent } from 'tests/test-utils';
import { fireEvent, render, screen } from 'tests/test-utils';
import MembersSettings from '../MembersSettings';
@@ -76,32 +76,27 @@ describe('MembersSettings (integration)', () => {
});
it('filters to pending invites via the filter dropdown', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<MembersSettings />);
await screen.findByText('Alice Smith');
await user.click(screen.getByRole('button', { name: /all members/i }));
fireEvent.click(screen.getByRole('button', { name: /all members/i }));
const pendingOption = await screen.findByText(/pending invites/i);
await user.click(pendingOption);
fireEvent.click(pendingOption);
await screen.findByText('charlie@signoz.io');
expect(screen.queryByText('Alice Smith')).not.toBeInTheDocument();
});
it('filters members by name using the search input', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<MembersSettings />);
await screen.findByText('Alice Smith');
await user.type(
screen.getByPlaceholderText(/Search by name or email/i),
'bob',
);
fireEvent.change(screen.getByPlaceholderText(/Search by name or email/i), {
target: { value: 'bob' },
});
await screen.findByText('Bob Jones');
expect(screen.queryByText('Alice Smith')).not.toBeInTheDocument();
@@ -109,31 +104,25 @@ describe('MembersSettings (integration)', () => {
});
it('opens EditMemberDrawer when an active member row is clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<MembersSettings />);
await user.click(await screen.findByText('Alice Smith'));
fireEvent.click(await screen.findByText('Alice Smith'));
await screen.findByText('Member Details');
});
it('opens EditMemberDrawer when a deleted member row is clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<MembersSettings />);
await user.click(await screen.findByText('Dave Deleted'));
fireEvent.click(await screen.findByText('Dave Deleted'));
expect(screen.queryByText('Member Details')).toBeInTheDocument();
});
it('opens InviteMembersModal when "Invite member" button is clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<MembersSettings />);
await user.click(screen.getByRole('button', { name: /invite member/i }));
fireEvent.click(screen.getByRole('button', { name: /invite member/i }));
await expect(
screen.findAllByPlaceholderText('john@signoz.io'),

View File

@@ -117,8 +117,7 @@ describe('CreateEdit Modal', () => {
});
});
// Todo: to fixed properly - failing with - due to timeout > 5000ms
describe.skip('Form Validation', () => {
describe('Form Validation', () => {
it('shows validation error when submitting without required fields', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
@@ -127,7 +126,7 @@ describe('CreateEdit Modal', () => {
const configureButtons = await screen.findAllByRole('button', {
name: /configure/i,
});
await user.click(configureButtons[0]);
fireEvent.click(configureButtons[0]);
const saveButton = await screen.findByRole('button', {
name: /save changes/i,
@@ -338,11 +337,8 @@ describe('CreateEdit Modal', () => {
});
});
// Todo: to fixed properly - failing with - due to timeout > 5000ms
describe.skip('Modal Actions', () => {
it('calls onClose when cancel button is clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
describe('Modal Actions', () => {
it('calls onClose when cancel button is clicked', () => {
render(
<CreateEdit
isCreate={false}
@@ -352,7 +348,7 @@ describe('CreateEdit Modal', () => {
);
const cancelButton = screen.getByRole('button', { name: /cancel/i });
await user.click(cancelButton);
fireEvent.click(cancelButton);
expect(mockOnClose).toHaveBeenCalled();
});

View File

@@ -1,13 +1,20 @@
// Ungate feature flag for all tests in this file
jest.mock('../../config', () => ({ IS_ROLE_DETAILS_AND_CRUD_ENABLED: true }));
import * as roleApi from 'api/generated/services/role';
import {
customRoleResponse,
managedRoleResponse,
} from 'mocks-server/__mockdata__/roles';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { render, screen, userEvent, waitFor, within } from 'tests/test-utils';
import {
fireEvent,
render,
screen,
userEvent,
waitFor,
within,
} from 'tests/test-utils';
import RoleDetailsPage from '../RoleDetailsPage';
@@ -22,7 +29,7 @@ const allScopeObjectsResponse = {
status: 'success',
data: [
{
resource: { name: 'dashboard', type: 'dashboard' },
resource: { kind: 'role', type: 'metaresources' },
selectors: ['*'],
},
],
@@ -44,8 +51,7 @@ afterEach(() => {
server.resetHandlers();
});
// Todo: to fixed properly - failing with - due to timeout > 5000ms
describe.skip('RoleDetailsPage', () => {
describe('RoleDetailsPage', () => {
it('renders custom role header, tabs, description, permissions, and action buttons', async () => {
setupDefaultHandlers();
@@ -57,20 +63,16 @@ describe.skip('RoleDetailsPage', () => {
screen.findByText('Role — billing-manager'),
).resolves.toBeInTheDocument();
// Tab navigation
expect(screen.getByText('Overview')).toBeInTheDocument();
expect(screen.getByText('Members')).toBeInTheDocument();
// Role description (OverviewTab)
expect(
screen.getByText('Custom role for managing billing and invoices.'),
).toBeInTheDocument();
// Permission items derived from mocked authz relations
expect(screen.getByText('Create')).toBeInTheDocument();
expect(screen.getByText('Read')).toBeInTheDocument();
// Action buttons present for custom role
expect(
screen.getByRole('button', { name: /edit role details/i }),
).toBeInTheDocument();
@@ -96,14 +98,13 @@ describe.skip('RoleDetailsPage', () => {
),
).toBeInTheDocument();
// Action buttons absent for managed role
expect(screen.queryByText('Edit Role Details')).not.toBeInTheDocument();
expect(
screen.queryByRole('button', { name: /delete role/i }),
).not.toBeInTheDocument();
});
it('edit flow: modal opens pre-filled and calls PATCH on save and verify', async () => {
it('edit flow: modal opens pre-filled and calls PATCH on save', async () => {
const patchSpy = jest.fn();
let description = customRoleResponse.data.description;
server.use(
@@ -138,21 +139,16 @@ describe.skip('RoleDetailsPage', () => {
await screen.findByText('Role — billing-manager');
// Open the edit modal
await user.click(screen.getByRole('button', { name: /edit role details/i }));
await expect(
screen.findByText('Edit Role Details', {
selector: '.ant-modal-title',
}),
screen.findByText('Edit Role Details', { selector: '.ant-modal-title' }),
).resolves.toBeInTheDocument();
// Name field is disabled in edit mode (role rename is not allowed)
const nameInput = screen.getByPlaceholderText(
'Enter role name e.g. : Service Owner',
);
expect(nameInput).toBeDisabled();
// Update description and save
const descField = screen.getByPlaceholderText(
'A helpful description of the role',
);
@@ -168,9 +164,7 @@ describe.skip('RoleDetailsPage', () => {
await waitFor(() =>
expect(
screen.queryByText('Edit Role Details', {
selector: '.ant-modal-title',
}),
screen.queryByText('Edit Role Details', { selector: '.ant-modal-title' }),
).not.toBeInTheDocument(),
);
@@ -219,58 +213,61 @@ describe.skip('RoleDetailsPage', () => {
});
describe('permission side panel', () => {
async function openCreatePanel(
user: ReturnType<typeof userEvent.setup>,
): Promise<void> {
beforeEach(() => {
// Both hooks mocked so data renders synchronously — no React Query scheduler or MSW round-trip.
jest.spyOn(roleApi, 'useGetRole').mockReturnValue({
data: customRoleResponse,
isLoading: false,
isFetching: false,
isError: false,
error: null,
} as any);
jest
.spyOn(roleApi, 'useGetObjects')
.mockReturnValue({ data: emptyObjectsResponse, isLoading: false } as any);
});
afterEach(() => {
jest.restoreAllMocks();
});
async function openCreatePanel(): Promise<HTMLElement> {
await screen.findByText('Role — billing-manager');
await user.click(screen.getByText('Create'));
fireEvent.click(screen.getByText('Create'));
await screen.findByText('Edit Create Permissions');
await screen.findByRole('button', { name: /dashboard/i });
const panel = document.querySelector(
'.permission-side-panel',
) as HTMLElement;
await within(panel).findByRole('button', { name: 'Role' });
return panel;
}
it('Save Changes is disabled until a resource scope is changed', async () => {
setupDefaultHandlers();
server.use(
rest.get(
`${rolesApiBase}/:id/relation/:relation/objects`,
(_req, res, ctx) => res(ctx.status(200), ctx.json(emptyObjectsResponse)),
),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<RoleDetailsPage />, undefined, {
initialRoute: `/settings/roles/${CUSTOM_ROLE_ID}`,
});
await openCreatePanel(user);
// No change yet — config matches initial, unsavedCount = 0
expect(screen.getByRole('button', { name: /save changes/i })).toBeDisabled();
// Expand Dashboard and flip to All — now Save is enabled
await user.click(screen.getByRole('button', { name: /dashboard/i }));
await user.click(screen.getByText('All'));
const panel = await openCreatePanel();
expect(
screen.getByRole('button', { name: /save changes/i }),
).not.toBeDisabled();
within(panel).getByRole('button', { name: /save changes/i }),
).toBeDisabled();
// check for what shown now - unsavedCount = 1
fireEvent.click(within(panel).getByRole('button', { name: 'Role' }));
fireEvent.click(screen.getByText('All'));
expect(
within(panel).getByRole('button', { name: /save changes/i }),
).not.toBeDisabled();
expect(screen.getByText('1 unsaved change')).toBeInTheDocument();
});
it('set scope to All → patchObjects additions: ["*"], deletions: null', async () => {
const patchSpy = jest.fn();
setupDefaultHandlers();
server.use(
rest.get(
`${rolesApiBase}/:id/relation/:relation/objects`,
(_req, res, ctx) => res(ctx.status(200), ctx.json(emptyObjectsResponse)),
),
rest.patch(
`${rolesApiBase}/:id/relation/:relation/objects`,
`${rolesApiBase}/:id/relations/:relation/objects`,
async (req, res, ctx) => {
patchSpy(await req.json());
return res(ctx.status(200), ctx.json({ status: 'success', data: null }));
@@ -278,23 +275,23 @@ describe.skip('RoleDetailsPage', () => {
),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<RoleDetailsPage />, undefined, {
initialRoute: `/settings/roles/${CUSTOM_ROLE_ID}`,
});
await openCreatePanel(user);
const panel = await openCreatePanel();
await user.click(screen.getByRole('button', { name: /dashboard/i }));
await user.click(screen.getByText('All'));
await user.click(screen.getByRole('button', { name: /save changes/i }));
fireEvent.click(within(panel).getByRole('button', { name: 'Role' }));
fireEvent.click(screen.getByText('All'));
fireEvent.click(
within(panel).getByRole('button', { name: /save changes/i }),
);
await waitFor(() =>
expect(patchSpy).toHaveBeenCalledWith({
additions: [
{
resource: { name: 'dashboard', type: 'dashboard' },
resource: { kind: 'role', type: 'role' },
selectors: ['*'],
},
],
@@ -306,14 +303,9 @@ describe.skip('RoleDetailsPage', () => {
it('set scope to Only selected with IDs → patchObjects additions contain those IDs', async () => {
const patchSpy = jest.fn();
setupDefaultHandlers();
server.use(
rest.get(
`${rolesApiBase}/:id/relation/:relation/objects`,
(_req, res, ctx) => res(ctx.status(200), ctx.json(emptyObjectsResponse)),
),
rest.patch(
`${rolesApiBase}/:id/relation/:relation/objects`,
`${rolesApiBase}/:id/relations/:relation/objects`,
async (req, res, ctx) => {
patchSpy(await req.json());
return res(ctx.status(200), ctx.json({ status: 'success', data: null }));
@@ -321,29 +313,28 @@ describe.skip('RoleDetailsPage', () => {
),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<RoleDetailsPage />, undefined, {
initialRoute: `/settings/roles/${CUSTOM_ROLE_ID}`,
});
await openCreatePanel(user);
const panel = await openCreatePanel();
await user.click(screen.getByRole('button', { name: /dashboard/i }));
fireEvent.click(within(panel).getByRole('button', { name: 'Role' }));
const combobox = screen.getByRole('combobox');
await user.click(combobox);
await user.type(combobox, 'dash-1');
await user.keyboard('{Enter}');
const combobox = within(panel).getByRole('combobox');
fireEvent.change(combobox, { target: { value: 'role-001' } });
fireEvent.keyDown(combobox, { key: 'Enter', keyCode: 13 });
await user.click(screen.getByRole('button', { name: /save changes/i }));
fireEvent.click(
within(panel).getByRole('button', { name: /save changes/i }),
);
await waitFor(() =>
expect(patchSpy).toHaveBeenCalledWith({
additions: [
{
resource: { name: 'dashboard', type: 'dashboard' },
selectors: ['dash-1'],
resource: { kind: 'role', type: 'role' },
selectors: ['role-001'],
},
],
deletions: null,
@@ -354,15 +345,13 @@ describe.skip('RoleDetailsPage', () => {
it('existing All scope changed to Only selected (empty) → patchObjects deletions: ["*"], additions: null', async () => {
const patchSpy = jest.fn();
setupDefaultHandlers();
jest.spyOn(roleApi, 'useGetObjects').mockReturnValue({
data: allScopeObjectsResponse,
isLoading: false,
} as any);
server.use(
rest.get(
`${rolesApiBase}/:id/relation/:relation/objects`,
(_req, res, ctx) =>
res(ctx.status(200), ctx.json(allScopeObjectsResponse)),
),
rest.patch(
`${rolesApiBase}/:id/relation/:relation/objects`,
`${rolesApiBase}/:id/relations/:relation/objects`,
async (req, res, ctx) => {
patchSpy(await req.json());
return res(ctx.status(200), ctx.json({ status: 'success', data: null }));
@@ -370,26 +359,24 @@ describe.skip('RoleDetailsPage', () => {
),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<RoleDetailsPage />, undefined, {
initialRoute: `/settings/roles/${CUSTOM_ROLE_ID}`,
});
await openCreatePanel(user);
const panel = await openCreatePanel();
await user.click(screen.getByRole('button', { name: /dashboard/i }));
fireEvent.click(within(panel).getByRole('button', { name: 'Role' }));
fireEvent.click(screen.getByText('Only selected'));
fireEvent.click(
within(panel).getByRole('button', { name: /save changes/i }),
);
await user.click(screen.getByText('Only selected'));
await user.click(screen.getByRole('button', { name: /save changes/i }));
// Should delete the '*' selector and add nothing
await waitFor(() =>
expect(patchSpy).toHaveBeenCalledWith({
additions: null,
deletions: [
{
resource: { name: 'dashboard', type: 'dashboard' },
resource: { kind: 'role', type: 'role' },
selectors: ['*'],
},
],
@@ -398,36 +385,25 @@ describe.skip('RoleDetailsPage', () => {
});
it('unsaved changes counter shown on scope change, Discard resets it', async () => {
setupDefaultHandlers();
server.use(
rest.get(
`${rolesApiBase}/:id/relation/:relation/objects`,
(_req, res, ctx) => res(ctx.status(200), ctx.json(emptyObjectsResponse)),
),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<RoleDetailsPage />, undefined, {
initialRoute: `/settings/roles/${CUSTOM_ROLE_ID}`,
});
await openCreatePanel(user);
const panel = await openCreatePanel();
// No unsaved changes indicator yet
expect(screen.queryByText(/unsaved change/)).not.toBeInTheDocument();
// Change dashboard scope to "All"
await user.click(screen.getByRole('button', { name: /dashboard/i }));
await user.click(screen.getByText('All'));
fireEvent.click(within(panel).getByRole('button', { name: 'Role' }));
fireEvent.click(screen.getByText('All'));
expect(screen.getByText('1 unsaved change')).toBeInTheDocument();
// Discard reverts to initial config — counter disappears, Save re-disabled
await user.click(screen.getByRole('button', { name: /discard/i }));
fireEvent.click(within(panel).getByRole('button', { name: /discard/i }));
expect(screen.queryByText(/unsaved change/)).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: /save changes/i })).toBeDisabled();
expect(
within(panel).getByRole('button', { name: /save changes/i }),
).toBeDisabled();
});
});
});

View File

@@ -4,24 +4,27 @@ import {
} from 'mocks-server/__mockdata__/roles';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { render, screen, userEvent } from 'tests/test-utils';
import { fireEvent, render, screen } from 'tests/test-utils';
import RolesSettings from '../RolesSettings';
const rolesApiURL = 'http://localhost/api/v1/roles';
describe('RolesSettings', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('renders the header and search input', () => {
beforeEach(() => {
server.use(
rest.get(rolesApiURL, (_req, res, ctx) =>
res(ctx.status(200), ctx.json(listRolesSuccessResponse)),
),
);
});
afterEach(() => {
jest.clearAllMocks();
server.resetHandlers();
});
it('renders the header and search input', () => {
render(<RolesSettings />);
expect(screen.getByText('Roles')).toBeInTheDocument();
@@ -34,12 +37,6 @@ describe('RolesSettings', () => {
});
it('displays roles grouped by managed and custom sections', async () => {
server.use(
rest.get(rolesApiURL, (_req, res, ctx) =>
res(ctx.status(200), ctx.json(listRolesSuccessResponse)),
),
);
render(<RolesSettings />);
await expect(screen.findByText('signoz-admin')).resolves.toBeInTheDocument();
@@ -68,20 +65,13 @@ describe('RolesSettings', () => {
});
it('filters roles by search query on name', async () => {
server.use(
rest.get(rolesApiURL, (_req, res, ctx) =>
res(ctx.status(200), ctx.json(listRolesSuccessResponse)),
),
);
render(<RolesSettings />);
await expect(screen.findByText('signoz-admin')).resolves.toBeInTheDocument();
const user = userEvent.setup({ pointerEventsCheck: 0 });
const searchInput = screen.getByPlaceholderText('Search for roles...');
await user.type(searchInput, 'billing');
fireEvent.change(screen.getByPlaceholderText('Search for roles...'), {
target: { value: 'billing' },
});
await expect(
screen.findByText('billing-manager'),
@@ -92,20 +82,13 @@ describe('RolesSettings', () => {
});
it('filters roles by search query on description', async () => {
server.use(
rest.get(rolesApiURL, (_req, res, ctx) =>
res(ctx.status(200), ctx.json(listRolesSuccessResponse)),
),
);
render(<RolesSettings />);
await expect(screen.findByText('signoz-admin')).resolves.toBeInTheDocument();
const user = userEvent.setup({ pointerEventsCheck: 0 });
const searchInput = screen.getByPlaceholderText('Search for roles...');
await user.type(searchInput, 'read-only');
fireEvent.change(screen.getByPlaceholderText('Search for roles...'), {
target: { value: 'read-only' },
});
await expect(screen.findByText('signoz-viewer')).resolves.toBeInTheDocument();
expect(screen.queryByText('signoz-admin')).not.toBeInTheDocument();
@@ -113,20 +96,13 @@ describe('RolesSettings', () => {
});
it('shows empty state when search matches nothing', async () => {
server.use(
rest.get(rolesApiURL, (_req, res, ctx) =>
res(ctx.status(200), ctx.json(listRolesSuccessResponse)),
),
);
render(<RolesSettings />);
await expect(screen.findByText('signoz-admin')).resolves.toBeInTheDocument();
const user = userEvent.setup({ pointerEventsCheck: 0 });
const searchInput = screen.getByPlaceholderText('Search for roles...');
await user.type(searchInput, 'nonexistentrole');
fireEvent.change(screen.getByPlaceholderText('Search for roles...'), {
target: { value: 'nonexistentrole' },
});
await expect(
screen.findByText('No roles match your search.'),
@@ -183,12 +159,6 @@ describe('RolesSettings', () => {
});
it('renders descriptions for all roles', async () => {
server.use(
rest.get(rolesApiURL, (_req, res, ctx) =>
res(ctx.status(200), ctx.json(listRolesSuccessResponse)),
),
);
render(<RolesSettings />);
await expect(screen.findByText('signoz-admin')).resolves.toBeInTheDocument();

View File

@@ -2,7 +2,7 @@ import type { ReactNode } from 'react';
import { listRolesSuccessResponse } from 'mocks-server/__mockdata__/roles';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
import ServiceAccountsSettings from '../ServiceAccountsSettings';
@@ -123,8 +123,6 @@ describe('ServiceAccountsSettings (integration)', () => {
});
it('filter dropdown to "Active" hides DISABLED accounts', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<NuqsTestingAdapter>
<ServiceAccountsSettings />
@@ -133,18 +131,16 @@ describe('ServiceAccountsSettings (integration)', () => {
await screen.findByText('CI Bot');
await user.click(screen.getByRole('button', { name: /All accounts/i }));
fireEvent.click(screen.getByRole('button', { name: /All accounts/i }));
const activeOption = await screen.findByText(/Active ⎯/i);
await user.click(activeOption);
fireEvent.click(activeOption);
await screen.findByText('CI Bot');
expect(screen.queryByText('Legacy Bot')).not.toBeInTheDocument();
});
it('search by name filters accounts in real-time', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<NuqsTestingAdapter>
<ServiceAccountsSettings />
@@ -153,10 +149,9 @@ describe('ServiceAccountsSettings (integration)', () => {
await screen.findByText('CI Bot');
await user.type(
screen.getByPlaceholderText(/Search by name or email/i),
'legacy',
);
fireEvent.change(screen.getByPlaceholderText(/Search by name or email/i), {
target: { value: 'legacy' },
});
await screen.findByText('Legacy Bot');
expect(screen.queryByText('CI Bot')).not.toBeInTheDocument();
@@ -164,15 +159,13 @@ describe('ServiceAccountsSettings (integration)', () => {
});
it('clicking a row opens the drawer with account details visible', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<NuqsTestingAdapter hasMemory>
<ServiceAccountsSettings />
</NuqsTestingAdapter>,
);
await user.click(
fireEvent.click(
await screen.findByRole('button', {
name: /View service account CI Bot/i,
}),
@@ -184,7 +177,6 @@ describe('ServiceAccountsSettings (integration)', () => {
});
it('saving changes in the drawer refetches the list', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const listRefetchSpy = jest.fn();
server.use(
@@ -206,15 +198,14 @@ describe('ServiceAccountsSettings (integration)', () => {
await screen.findByText('CI Bot');
listRefetchSpy.mockClear();
await user.click(
fireEvent.click(
await screen.findByRole('button', { name: /View service account CI Bot/i }),
);
const nameInput = await screen.findByDisplayValue('CI Bot');
await user.clear(nameInput);
await user.type(nameInput, 'CI Bot Updated');
fireEvent.change(nameInput, { target: { value: 'CI Bot Updated' } });
await user.click(screen.getByRole('button', { name: /Save Changes/i }));
fireEvent.click(screen.getByRole('button', { name: /Save Changes/i }));
await screen.findByDisplayValue('CI Bot Updated');
await waitFor(() => {
@@ -223,8 +214,6 @@ describe('ServiceAccountsSettings (integration)', () => {
});
it('"New Service Account" button opens the Create Service Account modal', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<NuqsTestingAdapter hasMemory>
<ServiceAccountsSettings />
@@ -233,9 +222,7 @@ describe('ServiceAccountsSettings (integration)', () => {
await screen.findByText('CI Bot');
await user.click(
screen.getByRole('button', { name: /New Service Account/i }),
);
fireEvent.click(screen.getByRole('button', { name: /New Service Account/i }));
await screen.findByRole('dialog', { name: /New Service Account/i });
expect(screen.getByPlaceholderText('Enter a name')).toBeInTheDocument();

View File

@@ -4,12 +4,8 @@ export default {
data: {
resources: [
{
kind: 'role',
type: 'metaresources',
},
{
kind: 'serviceaccount',
type: 'metaresources',
kind: 'factor-api-key',
type: 'metaresource',
},
{
kind: 'role',
@@ -22,12 +18,13 @@ export default {
],
relations: {
assignee: ['role'],
attach: ['role', 'serviceaccount'],
create: ['metaresources'],
delete: ['role', 'serviceaccount'],
list: ['metaresources'],
read: ['role', 'serviceaccount'],
update: ['role', 'serviceaccount'],
attach: ['metaresource', 'role', 'serviceaccount'],
create: ['metaresource', 'role', 'serviceaccount'],
delete: ['metaresource', 'role', 'serviceaccount'],
detach: ['metaresource', 'role', 'serviceaccount'],
list: ['metaresource', 'role', 'serviceaccount'],
read: ['metaresource', 'role', 'serviceaccount'],
update: ['metaresource', 'role', 'serviceaccount'],
},
},
} as const;

View File

@@ -80,19 +80,6 @@ export function permissionToTransactionDto(
permission: BrandedPermission,
): AuthtypesTransactionDTO {
const { relation, object: objectStr } = parsePermission(permission);
const directType = resolveType(relation, objectStr);
if (directType === 'metaresources') {
return {
relation: relation as AuthtypesRelationDTO,
object: {
resource: {
kind: objectStr as ResourceName,
type: directType as CoretypesTypeDTO,
},
selector: '*',
},
};
}
const { resourceName, selector } = splitObjectString(objectStr);
const type = resolveType(relation, resourceName) ?? 'metaresource';
@@ -117,9 +104,6 @@ export function gettableTransactionToPermission(
} = item;
const resourceName = String(resource.kind);
const selectorStr = typeof selector === 'string' ? selector : '*';
const objectStr =
resource.type === 'metaresources'
? resourceName
: `${resourceName}${ObjectSeparator}${selectorStr}`;
const objectStr = `${resourceName}${ObjectSeparator}${selectorStr}`;
return `${relation}${PermissionSeparator}${objectStr}` as BrandedPermission;
}

View File

@@ -7,7 +7,7 @@ import {
TabsTrigger,
} from '@signozhq/ui/tabs';
import {
Tooltip,
TooltipRoot,
TooltipContent,
TooltipProvider,
TooltipTrigger,
@@ -497,7 +497,7 @@ function SpanDetailsPanel({
key: 'dock-toggle',
component: (
<TooltipProvider>
<Tooltip>
<TooltipRoot>
<TooltipTrigger asChild>
<Button
variant="ghost"
@@ -515,7 +515,7 @@ function SpanDetailsPanel({
<TooltipContent className="dock-toggle-tooltip">
{isDocked ? 'Open as floating panel' : 'Dock at the bottom'}
</TooltipContent>
</Tooltip>
</TooltipRoot>
</TooltipProvider>
),
});

View File

@@ -1,5 +1,5 @@
import {
Tooltip,
TooltipRoot,
TooltipContent,
TooltipProvider,
TooltipTrigger,
@@ -149,7 +149,7 @@ export function SpanHoverCard({
return (
<TooltipProvider>
<Tooltip open={hoverCardData !== null} onOpenChange={onOpenChange}>
<TooltipRoot open={hoverCardData !== null} onOpenChange={onOpenChange}>
<TooltipTrigger asChild>
<div
className="span-hover-card-anchor"
@@ -168,7 +168,7 @@ export function SpanHoverCard({
>
{hoverCardData && <SpanTooltipContent {...hoverCardData.tooltip} />}
</TooltipContent>
</Tooltip>
</TooltipRoot>
</TooltipProvider>
);
}

View File

@@ -2,7 +2,7 @@ import { useCallback, useState } from 'react';
import { useParams } from 'react-router-dom';
import { Button } from '@signozhq/ui/button';
import {
Tooltip,
TooltipRoot,
TooltipContent,
TooltipProvider,
TooltipTrigger,
@@ -145,7 +145,7 @@ function TraceDetailsHeader({
{!isFilterExpanded && (
<>
<TooltipProvider>
<Tooltip>
<TooltipRoot>
<TooltipTrigger asChild>
<Button
variant="ghost"
@@ -158,7 +158,7 @@ function TraceDetailsHeader({
</Button>
</TooltipTrigger>
<TooltipContent>Analytics</TooltipContent>
</Tooltip>
</TooltipRoot>
</TooltipProvider>
<TraceOptionsMenu
showTraceDetails={showTraceDetails}

View File

@@ -1,6 +1,6 @@
import { Button } from '@signozhq/ui/button';
import {
Tooltip,
TooltipRoot,
TooltipContent,
TooltipProvider,
TooltipTrigger,
@@ -22,7 +22,7 @@ export default function SpanLineActionButtons({
return (
<div className="span-line-action-buttons">
<TooltipProvider>
<Tooltip>
<TooltipRoot>
<TooltipTrigger asChild>
<Button
variant="ghost"
@@ -37,7 +37,7 @@ export default function SpanLineActionButtons({
<TooltipContent className="span-line-action-tooltip">
Copy Span Link
</TooltipContent>
</Tooltip>
</TooltipRoot>
</TooltipProvider>
</div>
);

View File

@@ -15,7 +15,7 @@ import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
import { toast } from '@signozhq/ui/sonner';
import { Button } from '@signozhq/ui/button';
import {
Tooltip,
TooltipRoot,
TooltipContent,
TooltipProvider,
TooltipTrigger,
@@ -269,7 +269,7 @@ function Filters({
<>
{isFetching && <Loader className="animate-spin" />}
{error && (
<Tooltip>
<TooltipRoot>
<TooltipTrigger asChild>
<span className="filter-status filter-status--error">
<Info />
@@ -279,7 +279,7 @@ function Filters({
<TooltipContent>
{(error as AxiosError)?.message || 'Something went wrong'}
</TooltipContent>
</Tooltip>
</TooltipRoot>
)}
{!error && noData && (
<Typography.Text className="filter-status">
@@ -304,7 +304,7 @@ function Filters({
<TooltipProvider>
<div className="trace-v3-filter-row collapsed">
{expression ? (
<Tooltip>
<TooltipRoot>
<TooltipTrigger asChild>{pill}</TooltipTrigger>
<TooltipContent side="bottom" align="start">
<div className="filter-pill-popover">
@@ -328,7 +328,7 @@ function Filters({
<div className="filter-pill-popover__expression">{expression}</div>
</div>
</TooltipContent>
</Tooltip>
</TooltipRoot>
) : (
pill
)}

View File

@@ -12,7 +12,7 @@ import {
import { Badge } from '@signozhq/ui/badge';
import { Button } from '@signozhq/ui/button';
import {
Tooltip,
TooltipRoot,
TooltipContent,
TooltipProvider,
TooltipTrigger,
@@ -112,9 +112,9 @@ const LazyEventDotPopover = memo(function LazyEventDotPopover({
return (
<TooltipProvider>
<Tooltip
<TooltipRoot
open
onOpenChange={(open): void => {
onOpenChange={(open: boolean): void => {
if (!open) {
setShowPopover(false);
}
@@ -129,7 +129,7 @@ const LazyEventDotPopover = memo(function LazyEventDotPopover({
attributeMap={event.attributeMap || {}}
/>
</TooltipContent>
</Tooltip>
</TooltipRoot>
</TooltipProvider>
);
});
@@ -329,7 +329,7 @@ const SpanOverview = memo(function SpanOverview({
{/* Action buttons — shown on hover via CSS, right-aligned */}
<span className="span-row-actions">
<TooltipProvider delayDuration={200}>
<Tooltip>
<TooltipRoot>
<TooltipTrigger asChild>
<Button
variant="ghost"
@@ -344,8 +344,8 @@ const SpanOverview = memo(function SpanOverview({
<TooltipContent className="span-action-tooltip">
Copy Span Link
</TooltipContent>
</Tooltip>
<Tooltip>
</TooltipRoot>
<TooltipRoot>
<TooltipTrigger asChild>
<Button
variant="ghost"
@@ -360,7 +360,7 @@ const SpanOverview = memo(function SpanOverview({
<TooltipContent className="span-action-tooltip">
Add to Trace Funnel
</TooltipContent>
</Tooltip>
</TooltipRoot>
</TooltipProvider>
</span>
</div>

View File

@@ -162,5 +162,24 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/infra_monitoring/jobs", handler.New(
provider.authzMiddleware.ViewAccess(provider.infraMonitoringHandler.ListJobs),
handler.OpenAPIDef{
ID: "ListJobs",
Tags: []string{"inframonitoring"},
Summary: "List Jobs for Infra Monitoring",
Description: "Returns a paginated list of Kubernetes Jobs with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the job, plus average CPU/memory request and limit utilization (jobCPURequest, jobCPULimit, jobMemoryRequest, jobMemoryLimit). Each row also reports the latest known job-level counters from kube-state-metrics: desiredSuccessfulPods (k8s.job.desired_successful_pods, the target completion count), activePods (k8s.job.active_pods), failedPods (k8s.job.failed_pods, cumulative across the lifetime of the job), and successfulPods (k8s.job.successful_pods, cumulative). It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value); note podCountsByPhase.failed (current pod-phase) is distinct from failedPods (cumulative job kube-state-metric). Each job includes metadata attributes (k8s.job.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.job.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by jobs in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_successful_pods / active_pods / failed_pods / successful_pods, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (jobCPU, jobCPURequest, jobCPULimit, jobMemory, jobMemoryRequest, jobMemoryLimit, desiredSuccessfulPods, activePods, failedPods, successfulPods) return -1 as a sentinel when no data is available for that field.",
Request: new(inframonitoringtypes.PostableJobs),
RequestContentType: "application/json",
Response: new(inframonitoringtypes.Jobs),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -7,11 +7,14 @@ import (
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
func (provider *provider) addRoleRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/roles", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.Create), handler.OpenAPIDef{
if err := router.Handle("/api/v1/roles", handler.New(provider.authzMiddleware.Check(provider.authzHandler.Create, authtypes.Relation{Verb: coretypes.VerbCreate}, coretypes.ResourceRole, roleCollectionSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "CreateRole",
Tags: []string{"role"},
Summary: "Create role",
@@ -23,12 +26,14 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbCreate)}),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/roles", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.List), handler.OpenAPIDef{
if err := router.Handle("/api/v1/roles", handler.New(provider.authzMiddleware.Check(provider.authzHandler.List, authtypes.Relation{Verb: coretypes.VerbList}, coretypes.ResourceRole, roleCollectionSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "ListRoles",
Tags: []string{"role"},
Summary: "List roles",
@@ -40,12 +45,14 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbList)}),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.Get), handler.OpenAPIDef{
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authzMiddleware.Check(provider.authzHandler.Get, authtypes.Relation{Verb: coretypes.VerbRead}, coretypes.ResourceRole, provider.roleInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "GetRole",
Tags: []string{"role"},
Summary: "Get role",
@@ -57,12 +64,14 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbRead)}),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/roles/{id}/relations/{relation}/objects", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.GetObjects), handler.OpenAPIDef{
if err := router.Handle("/api/v1/roles/{id}/relations/{relation}/objects", handler.New(provider.authzMiddleware.Check(provider.authzHandler.GetObjects, authtypes.Relation{Verb: coretypes.VerbRead}, coretypes.ResourceRole, provider.roleInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "GetObjects",
Tags: []string{"role"},
Summary: "Get objects for a role by relation",
@@ -74,12 +83,14 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbRead)}),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.Patch), handler.OpenAPIDef{
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authzMiddleware.Check(provider.authzHandler.Patch, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceRole, provider.roleInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "PatchRole",
Tags: []string{"role"},
Summary: "Patch role",
@@ -91,12 +102,14 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbUpdate)}),
})).Methods(http.MethodPatch).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/roles/{id}/relations/{relation}/objects", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.PatchObjects), handler.OpenAPIDef{
if err := router.Handle("/api/v1/roles/{id}/relations/{relation}/objects", handler.New(provider.authzMiddleware.Check(provider.authzHandler.PatchObjects, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceRole, provider.roleInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "PatchObjects",
Tags: []string{"role"},
Summary: "Patch objects for a role by relation",
@@ -108,12 +121,14 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusBadRequest, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbUpdate)}),
})).Methods(http.MethodPatch).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.Delete), handler.OpenAPIDef{
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authzMiddleware.Check(provider.authzHandler.Delete, authtypes.Relation{Verb: coretypes.VerbDelete}, coretypes.ResourceRole, provider.roleInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "DeleteRole",
Tags: []string{"role"},
Summary: "Delete role",
@@ -125,10 +140,33 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbDelete)}),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
return nil
}
func roleCollectionSelectorCallback(_ *http.Request, _ authtypes.Claims) ([]coretypes.Selector, error) {
return []coretypes.Selector{
coretypes.TypeRole.MustSelector(coretypes.WildCardSelectorString),
}, nil
}
func (provider *provider) roleInstanceSelectorCallback(req *http.Request, claims authtypes.Claims) ([]coretypes.Selector, error) {
roleID, err := valuer.NewUUID(mux.Vars(req)["id"])
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
}

View File

@@ -17,7 +17,7 @@ import (
)
func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
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{
if err := router.Handle("/api/v1/service_accounts", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.Create, authtypes.Relation{Verb: coretypes.VerbCreate}, coretypes.ResourceServiceAccount, serviceAccountCollectionSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "CreateServiceAccount",
@@ -31,12 +31,12 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourcesServiceAccount.Scope(coretypes.VerbCreate)}),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbCreate)}),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
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{
if err := router.Handle("/api/v1/service_accounts", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.List, authtypes.Relation{Verb: coretypes.VerbList}, coretypes.ResourceServiceAccount, serviceAccountCollectionSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "ListServiceAccounts",
@@ -50,7 +50,7 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourcesServiceAccount.Scope(coretypes.VerbList)}),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbList)}),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -135,10 +135,10 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
}
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{
{{Relation: authtypes.Relation{Verb: coretypes.VerbDetach}, Resource: coretypes.ResourceServiceAccount, SelectorCallback: serviceAccountInstanceSelectorCallback, Roles: []string{
authtypes.SigNozAdminRoleName,
}}},
{{Relation: authtypes.Relation{Verb: coretypes.VerbAttach}, Resource: coretypes.ResourceRole, SelectorCallback: provider.roleAttachSelectorFromPath, Roles: []string{
{{Relation: authtypes.Relation{Verb: coretypes.VerbDetach}, Resource: coretypes.ResourceRole, SelectorCallback: provider.roleDetachSelectorFromPath, Roles: []string{
authtypes.SigNozAdminRoleName,
}}},
}), handler.OpenAPIDef{
@@ -153,7 +153,7 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbAttach), coretypes.ResourceRole.Scope(coretypes.VerbAttach)}),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbDetach), coretypes.ResourceRole.Scope(coretypes.VerbDetach)}),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
@@ -213,8 +213,13 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
return err
}
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,
if err := router.Handle("/api/v1/service_accounts/{id}/keys", handler.New(provider.authzMiddleware.CheckAll(provider.serviceAccountHandler.CreateFactorAPIKey, []middleware.AuthZCheckGroup{
{{Relation: authtypes.Relation{Verb: coretypes.VerbCreate}, Resource: coretypes.ResourceMetaResourceFactorAPIKey, SelectorCallback: factorAPIKeyCollectionSelectorCallback, Roles: []string{
authtypes.SigNozAdminRoleName,
}}},
{{Relation: authtypes.Relation{Verb: coretypes.VerbAttach}, Resource: coretypes.ResourceServiceAccount, SelectorCallback: serviceAccountInstanceSelectorCallback, Roles: []string{
authtypes.SigNozAdminRoleName,
}}},
}), handler.OpenAPIDef{
ID: "CreateServiceAccountKey",
Tags: []string{"serviceaccount"},
@@ -227,12 +232,12 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbUpdate)}),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceFactorAPIKey.Scope(coretypes.VerbCreate), coretypes.ResourceServiceAccount.Scope(coretypes.VerbAttach)}),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
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{
if err := router.Handle("/api/v1/service_accounts/{id}/keys", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.ListFactorAPIKey, authtypes.Relation{Verb: coretypes.VerbList}, coretypes.ResourceMetaResourceFactorAPIKey, factorAPIKeyCollectionSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "ListServiceAccountKeys",
@@ -246,12 +251,12 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbRead)}),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceFactorAPIKey.Scope(coretypes.VerbList)}),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
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{
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.ResourceMetaResourceFactorAPIKey, factorAPIKeyInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "UpdateServiceAccountKey",
@@ -265,13 +270,18 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbUpdate)}),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceFactorAPIKey.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.authzMiddleware.Check(provider.serviceAccountHandler.RevokeFactorAPIKey, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
if err := router.Handle("/api/v1/service_accounts/{id}/keys/{fid}", handler.New(provider.authzMiddleware.CheckAll(provider.serviceAccountHandler.RevokeFactorAPIKey, []middleware.AuthZCheckGroup{
{{Relation: authtypes.Relation{Verb: coretypes.VerbDelete}, Resource: coretypes.ResourceMetaResourceFactorAPIKey, SelectorCallback: factorAPIKeyInstanceSelectorCallback, Roles: []string{
authtypes.SigNozAdminRoleName,
}}},
{{Relation: authtypes.Relation{Verb: coretypes.VerbDetach}, Resource: coretypes.ResourceServiceAccount, SelectorCallback: serviceAccountInstanceSelectorCallback, Roles: []string{
authtypes.SigNozAdminRoleName,
}}},
}), handler.OpenAPIDef{
ID: "RevokeServiceAccountKey",
Tags: []string{"serviceaccount"},
@@ -284,7 +294,7 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbUpdate)}),
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceFactorAPIKey.Scope(coretypes.VerbDelete), coretypes.ResourceServiceAccount.Scope(coretypes.VerbDetach)}),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
@@ -292,7 +302,7 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
return nil
}
func (provider *provider) roleAttachSelectorFromPath(req *http.Request, claims authtypes.Claims) ([]coretypes.Selector, error) {
func (provider *provider) roleDetachSelectorFromPath(req *http.Request, claims authtypes.Claims) ([]coretypes.Selector, error) {
roleID, err := valuer.NewUUID(mux.Vars(req)["rid"])
if err != nil {
return nil, err
@@ -333,9 +343,28 @@ func (provider *provider) roleAttachSelectorFromBody(req *http.Request, claims a
}, nil
}
func factorAPIKeyCollectionSelectorCallback(_ *http.Request, _ authtypes.Claims) ([]coretypes.Selector, error) {
return []coretypes.Selector{
coretypes.TypeMetaResource.MustSelector(coretypes.WildCardSelectorString),
}, nil
}
func factorAPIKeyInstanceSelectorCallback(req *http.Request, _ authtypes.Claims) ([]coretypes.Selector, error) {
fid := mux.Vars(req)["fid"]
fidSelector, err := coretypes.TypeMetaResource.Selector(fid)
if err != nil {
return nil, err
}
return []coretypes.Selector{
fidSelector,
coretypes.TypeMetaResource.MustSelector(coretypes.WildCardSelectorString),
}, nil
}
func serviceAccountCollectionSelectorCallback(_ *http.Request, _ authtypes.Claims) ([]coretypes.Selector, error) {
return []coretypes.Selector{
coretypes.TypeMetaResources.MustSelector(coretypes.WildCardSelectorString),
coretypes.TypeServiceAccount.MustSelector(coretypes.WildCardSelectorString),
}, nil
}

View File

@@ -169,7 +169,7 @@ func (handler *handler) Patch(rw http.ResponseWriter, r *http.Request) {
return
}
render.Success(rw, http.StatusAccepted, nil)
render.Success(rw, http.StatusNoContent, nil)
}
func (handler *handler) PatchObjects(rw http.ResponseWriter, r *http.Request) {

View File

@@ -213,3 +213,27 @@ func (h *handler) ListStatefulSets(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusOK, result)
}
func (h *handler) ListJobs(rw http.ResponseWriter, req *http.Request) {
claims, err := authtypes.ClaimsFromContext(req.Context())
if err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
var parsedReq inframonitoringtypes.PostableJobs
if err := binding.JSON.BindBody(req.Body, &parsedReq); err != nil {
render.Error(rw, err)
return
}
result, err := h.module.ListJobs(req.Context(), orgID, &parsedReq)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, result)
}

View File

@@ -0,0 +1,156 @@
package implinframonitoring
import (
"context"
"slices"
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/valuer"
)
// buildJobRecords assembles the page records. Pod phase counts come from
// phaseCounts in both modes; every row is a group of pods (one job in
// list mode, an arbitrary roll-up in grouped_list mode), so there's no
// per-row "current phase" concept.
func buildJobRecords(
resp *qbtypes.QueryRangeResponse,
pageGroups []map[string]string,
groupBy []qbtypes.GroupByKey,
metadataMap map[string]map[string]string,
phaseCounts map[string]podPhaseCounts,
) []inframonitoringtypes.JobRecord {
metricsMap := parseFullQueryResponse(resp, groupBy)
records := make([]inframonitoringtypes.JobRecord, 0, len(pageGroups))
for _, labels := range pageGroups {
compositeKey := compositeKeyFromLabels(labels, groupBy)
jobName := labels[jobNameAttrKey]
record := inframonitoringtypes.JobRecord{ // initialize with default values
JobName: jobName,
JobCPU: -1,
JobCPURequest: -1,
JobCPULimit: -1,
JobMemory: -1,
JobMemoryRequest: -1,
JobMemoryLimit: -1,
DesiredSuccessfulPods: -1,
ActivePods: -1,
FailedPods: -1,
SuccessfulPods: -1,
Meta: map[string]string{},
}
if metrics, ok := metricsMap[compositeKey]; ok {
if v, exists := metrics["A"]; exists {
record.JobCPU = v
}
if v, exists := metrics["B"]; exists {
record.JobCPURequest = v
}
if v, exists := metrics["C"]; exists {
record.JobCPULimit = v
}
if v, exists := metrics["D"]; exists {
record.JobMemory = v
}
if v, exists := metrics["E"]; exists {
record.JobMemoryRequest = v
}
if v, exists := metrics["F"]; exists {
record.JobMemoryLimit = v
}
if v, exists := metrics["H"]; exists {
record.DesiredSuccessfulPods = int(v)
}
if v, exists := metrics["I"]; exists {
record.ActivePods = int(v)
}
if v, exists := metrics["J"]; exists {
record.FailedPods = int(v)
}
if v, exists := metrics["K"]; exists {
record.SuccessfulPods = int(v)
}
}
if phaseCountsForGroup, ok := phaseCounts[compositeKey]; ok {
record.PodCountsByPhase = inframonitoringtypes.PodCountsByPhase{
Pending: phaseCountsForGroup.Pending,
Running: phaseCountsForGroup.Running,
Succeeded: phaseCountsForGroup.Succeeded,
Failed: phaseCountsForGroup.Failed,
Unknown: phaseCountsForGroup.Unknown,
}
}
if attrs, ok := metadataMap[compositeKey]; ok {
for k, v := range attrs {
record.Meta[k] = v
}
}
records = append(records, record)
}
return records
}
func (m *module) getTopJobGroups(
ctx context.Context,
orgID valuer.UUID,
req *inframonitoringtypes.PostableJobs,
metadataMap map[string]map[string]string,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
queryNamesForOrderBy := orderByToJobsQueryNames[orderByKey]
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]
topReq := &qbtypes.QueryRangeRequest{
Start: uint64(req.Start),
End: uint64(req.End),
RequestType: qbtypes.RequestTypeScalar,
CompositeQuery: qbtypes.CompositeQuery{
Queries: make([]qbtypes.QueryEnvelope, 0, len(queryNamesForOrderBy)),
},
}
for _, envelope := range m.newJobsTableListQuery().CompositeQuery.Queries {
if !slices.Contains(queryNamesForOrderBy, envelope.GetQueryName()) {
continue
}
copied := envelope
if copied.Type == qbtypes.QueryTypeBuilder {
existingExpr := ""
if f := copied.GetFilter(); f != nil {
existingExpr = f.Expression
}
reqFilterExpr := ""
if req.Filter != nil {
reqFilterExpr = req.Filter.Expression
}
merged := mergeFilterExpressions(existingExpr, reqFilterExpr)
copied.SetFilter(&qbtypes.Filter{Expression: merged})
copied.SetGroupBy(req.GroupBy)
}
topReq.CompositeQuery.Queries = append(topReq.CompositeQuery.Queries, copied)
}
resp, err := m.querier.QueryRange(ctx, orgID, topReq)
if err != nil {
return nil, err
}
allMetricGroups := parseAndSortGroups(resp, rankingQueryName, req.GroupBy, req.OrderBy.Direction)
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), nil
}
func (m *module) getJobsTableMetadata(ctx context.Context, req *inframonitoringtypes.PostableJobs) (map[string]map[string]string, error) {
var nonGroupByAttrs []string
for _, key := range jobAttrKeysForMetadata {
if !isKeyInGroupByAttrs(req.GroupBy, key) {
nonGroupByAttrs = append(nonGroupByAttrs, key)
}
}
return m.getMetadata(ctx, jobsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
}

View File

@@ -0,0 +1,278 @@
package implinframonitoring
import (
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
"github.com/SigNoz/signoz/pkg/types/metrictypes"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
const (
jobNameAttrKey = "k8s.job.name"
jobsBaseFilterExpr = "k8s.job.name != ''"
)
var jobNameGroupByKey = qbtypes.GroupByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: jobNameAttrKey,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
}
// jobsTableMetricNamesList drives the existence/retention check.
// Includes k8s.pod.phase even though phase isn't part of the QB composite query —
// it is queried separately via getPerGroupPodPhaseCounts, and we want the
// response to short-circuit cleanly when the phase metric is absent.
var jobsTableMetricNamesList = []string{
"k8s.pod.phase",
"k8s.pod.cpu.usage",
"k8s.pod.cpu_request_utilization",
"k8s.pod.cpu_limit_utilization",
"k8s.pod.memory.working_set",
"k8s.pod.memory_request_utilization",
"k8s.pod.memory_limit_utilization",
"k8s.job.active_pods",
"k8s.job.failed_pods",
"k8s.job.successful_pods",
"k8s.job.desired_successful_pods",
}
// Carried forward from v1 jobAttrsToEnrich
// (pkg/query-service/app/inframetrics/jobs.go:31-35).
var jobAttrKeysForMetadata = []string{
"k8s.job.name",
"k8s.namespace.name",
"k8s.cluster.name",
}
// orderByToJobsQueryNames maps the orderBy column to the query name
// used for ranking job groups. v2 B/C/E/F are direct metrics, no
// formula deps — so unlike v1 we don't carry A/D.
var orderByToJobsQueryNames = map[string][]string{
inframonitoringtypes.JobsOrderByCPU: {"A"},
inframonitoringtypes.JobsOrderByCPURequest: {"B"},
inframonitoringtypes.JobsOrderByCPULimit: {"C"},
inframonitoringtypes.JobsOrderByMemory: {"D"},
inframonitoringtypes.JobsOrderByMemoryRequest: {"E"},
inframonitoringtypes.JobsOrderByMemoryLimit: {"F"},
inframonitoringtypes.JobsOrderByDesiredSuccessfulPods: {"H"},
inframonitoringtypes.JobsOrderByActivePods: {"I"},
inframonitoringtypes.JobsOrderByFailedPods: {"J"},
inframonitoringtypes.JobsOrderBySuccessfulPods: {"K"},
}
// newJobsTableListQuery builds the composite QB v5 request for the jobs list.
// Ten builder queries: A..F roll up pod-level metrics by job, H/I/J/K take the
// latest job-level desired/active/failed/successful counts. Restarts (v1 query G)
// is intentionally omitted to match the v2 pods/deployments pattern.
//
// Every builder query carries the base filter `jobsBaseFilterExpr`. Reason:
// pod-level metrics (A..F) are emitted for every pod regardless of whether the
// pod belongs to a Job; only Job-owned pods carry the `k8s.job.name` resource
// attribute. Without this filter, standalone pods and pods owned by other
// workloads (Deployment/StatefulSet/DaemonSet/...) collapse into a single
// empty-string group under the default groupBy.
func (m *module) newJobsTableListQuery() *qbtypes.QueryRangeRequest {
queries := []qbtypes.QueryEnvelope{
// Query A: k8s.pod.cpu.usage — sum of pod CPU within the group.
{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: "A",
Signal: telemetrytypes.SignalMetrics,
Aggregations: []qbtypes.MetricAggregation{
{
MetricName: "k8s.pod.cpu.usage",
TimeAggregation: metrictypes.TimeAggregationAvg,
SpaceAggregation: metrictypes.SpaceAggregationSum,
ReduceTo: qbtypes.ReduceToAvg,
},
},
Filter: &qbtypes.Filter{Expression: jobsBaseFilterExpr},
GroupBy: []qbtypes.GroupByKey{jobNameGroupByKey},
Disabled: false,
},
},
// Query B: k8s.pod.cpu_request_utilization — avg across pods in the group.
{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: "B",
Signal: telemetrytypes.SignalMetrics,
Aggregations: []qbtypes.MetricAggregation{
{
MetricName: "k8s.pod.cpu_request_utilization",
TimeAggregation: metrictypes.TimeAggregationAvg,
SpaceAggregation: metrictypes.SpaceAggregationAvg,
ReduceTo: qbtypes.ReduceToAvg,
},
},
Filter: &qbtypes.Filter{Expression: jobsBaseFilterExpr},
GroupBy: []qbtypes.GroupByKey{jobNameGroupByKey},
Disabled: false,
},
},
// Query C: k8s.pod.cpu_limit_utilization — avg across pods in the group.
{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: "C",
Signal: telemetrytypes.SignalMetrics,
Aggregations: []qbtypes.MetricAggregation{
{
MetricName: "k8s.pod.cpu_limit_utilization",
TimeAggregation: metrictypes.TimeAggregationAvg,
SpaceAggregation: metrictypes.SpaceAggregationAvg,
ReduceTo: qbtypes.ReduceToAvg,
},
},
Filter: &qbtypes.Filter{Expression: jobsBaseFilterExpr},
GroupBy: []qbtypes.GroupByKey{jobNameGroupByKey},
Disabled: false,
},
},
// Query D: k8s.pod.memory.working_set — sum of pod memory within the group.
{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: "D",
Signal: telemetrytypes.SignalMetrics,
Aggregations: []qbtypes.MetricAggregation{
{
MetricName: "k8s.pod.memory.working_set",
TimeAggregation: metrictypes.TimeAggregationAvg,
SpaceAggregation: metrictypes.SpaceAggregationSum,
ReduceTo: qbtypes.ReduceToAvg,
},
},
Filter: &qbtypes.Filter{Expression: jobsBaseFilterExpr},
GroupBy: []qbtypes.GroupByKey{jobNameGroupByKey},
Disabled: false,
},
},
// Query E: k8s.pod.memory_request_utilization — avg across pods in the group.
{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: "E",
Signal: telemetrytypes.SignalMetrics,
Aggregations: []qbtypes.MetricAggregation{
{
MetricName: "k8s.pod.memory_request_utilization",
TimeAggregation: metrictypes.TimeAggregationAvg,
SpaceAggregation: metrictypes.SpaceAggregationAvg,
ReduceTo: qbtypes.ReduceToAvg,
},
},
Filter: &qbtypes.Filter{Expression: jobsBaseFilterExpr},
GroupBy: []qbtypes.GroupByKey{jobNameGroupByKey},
Disabled: false,
},
},
// Query F: k8s.pod.memory_limit_utilization — avg across pods in the group.
{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: "F",
Signal: telemetrytypes.SignalMetrics,
Aggregations: []qbtypes.MetricAggregation{
{
MetricName: "k8s.pod.memory_limit_utilization",
TimeAggregation: metrictypes.TimeAggregationAvg,
SpaceAggregation: metrictypes.SpaceAggregationAvg,
ReduceTo: qbtypes.ReduceToAvg,
},
},
Filter: &qbtypes.Filter{Expression: jobsBaseFilterExpr},
GroupBy: []qbtypes.GroupByKey{jobNameGroupByKey},
Disabled: false,
},
},
// Query H: k8s.job.desired_successful_pods — latest known desired completion count per group.
// v1 used TimeAggregationAnyLast (v3) → mapped to TimeAggregationLatest in v5;
// SpaceAggregationSum + ReduceToLast preserve v1's "latest, summed across the group".
{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: "H",
Signal: telemetrytypes.SignalMetrics,
Aggregations: []qbtypes.MetricAggregation{
{
MetricName: "k8s.job.desired_successful_pods",
TimeAggregation: metrictypes.TimeAggregationLatest,
SpaceAggregation: metrictypes.SpaceAggregationSum,
ReduceTo: qbtypes.ReduceToLast,
},
},
Filter: &qbtypes.Filter{Expression: jobsBaseFilterExpr},
GroupBy: []qbtypes.GroupByKey{jobNameGroupByKey},
Disabled: false,
},
},
// Query I: k8s.job.active_pods — latest known active pod count per group.
{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: "I",
Signal: telemetrytypes.SignalMetrics,
Aggregations: []qbtypes.MetricAggregation{
{
MetricName: "k8s.job.active_pods",
TimeAggregation: metrictypes.TimeAggregationLatest,
SpaceAggregation: metrictypes.SpaceAggregationSum,
ReduceTo: qbtypes.ReduceToLast,
},
},
Filter: &qbtypes.Filter{Expression: jobsBaseFilterExpr},
GroupBy: []qbtypes.GroupByKey{jobNameGroupByKey},
Disabled: false,
},
},
// Query J: k8s.job.failed_pods — cumulative failed pod count per group.
{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: "J",
Signal: telemetrytypes.SignalMetrics,
Aggregations: []qbtypes.MetricAggregation{
{
MetricName: "k8s.job.failed_pods",
TimeAggregation: metrictypes.TimeAggregationLatest,
SpaceAggregation: metrictypes.SpaceAggregationSum,
ReduceTo: qbtypes.ReduceToLast,
},
},
Filter: &qbtypes.Filter{Expression: jobsBaseFilterExpr},
GroupBy: []qbtypes.GroupByKey{jobNameGroupByKey},
Disabled: false,
},
},
// Query K: k8s.job.successful_pods — cumulative successful pod count per group.
{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: "K",
Signal: telemetrytypes.SignalMetrics,
Aggregations: []qbtypes.MetricAggregation{
{
MetricName: "k8s.job.successful_pods",
TimeAggregation: metrictypes.TimeAggregationLatest,
SpaceAggregation: metrictypes.SpaceAggregationSum,
ReduceTo: qbtypes.ReduceToLast,
},
},
Filter: &qbtypes.Filter{Expression: jobsBaseFilterExpr},
GroupBy: []qbtypes.GroupByKey{jobNameGroupByKey},
Disabled: false,
},
},
}
return &qbtypes.QueryRangeRequest{
RequestType: qbtypes.RequestTypeScalar,
CompositeQuery: qbtypes.CompositeQuery{
Queries: queries,
},
}
}

View File

@@ -803,3 +803,100 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
return resp, nil
}
func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableJobs) (*inframonitoringtypes.Jobs, error) {
if err := req.Validate(); err != nil {
return nil, err
}
resp := &inframonitoringtypes.Jobs{}
if req.OrderBy == nil {
req.OrderBy = &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: inframonitoringtypes.JobsOrderByCPU,
},
},
Direction: qbtypes.OrderDirectionDesc,
}
}
if len(req.GroupBy) == 0 {
req.GroupBy = []qbtypes.GroupByKey{jobNameGroupByKey}
resp.Type = inframonitoringtypes.ResponseTypeList
} else {
resp.Type = inframonitoringtypes.ResponseTypeGroupedList
}
// Bake the jobs base filter into req.Filter so all downstream helpers pick it up.
if req.Filter == nil {
req.Filter = &qbtypes.Filter{}
}
req.Filter.Expression = mergeFilterExpressions(jobsBaseFilterExpr, req.Filter.Expression)
missingMetrics, minFirstReportedUnixMilli, err := m.getMetricsExistenceAndEarliestTime(ctx, jobsTableMetricNamesList)
if err != nil {
return nil, err
}
if len(missingMetrics) > 0 {
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: missingMetrics}
resp.Records = []inframonitoringtypes.JobRecord{}
resp.Total = 0
return resp, nil
}
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []inframonitoringtypes.JobRecord{}
resp.Total = 0
return resp, nil
}
resp.RequiredMetricsCheck = inframonitoringtypes.RequiredMetricsCheck{MissingMetrics: []string{}}
metadataMap, err := m.getJobsTableMetadata(ctx, req)
if err != nil {
return nil, err
}
resp.Total = len(metadataMap)
pageGroups, err := m.getTopJobGroups(ctx, orgID, req, metadataMap)
if err != nil {
return nil, err
}
if len(pageGroups) == 0 {
resp.Records = []inframonitoringtypes.JobRecord{}
return resp, nil
}
filterExpr := ""
if req.Filter != nil {
filterExpr = req.Filter.Expression
}
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newJobsTableListQuery())
queryResp, err := m.querier.QueryRange(ctx, orgID, fullQueryReq)
if err != nil {
return nil, err
}
// Reuse the pods phase-counts CTE function via a temp struct — it reads only
// Start/End/Filter/GroupBy from PostablePods. Pods owned by a Job carry
// k8s.job.name as a resource attribute, so default-groupBy gives
// per-job phase counts automatically.
phaseCounts, err := m.getPerGroupPodPhaseCounts(ctx, &inframonitoringtypes.PostablePods{
Start: req.Start,
End: req.End,
Filter: req.Filter,
GroupBy: req.GroupBy,
}, pageGroups)
if err != nil {
return nil, err
}
resp.Records = buildJobRecords(queryResp, pageGroups, req.GroupBy, metadataMap, phaseCounts)
resp.Warning = queryResp.Warning
return resp, nil
}

View File

@@ -17,6 +17,7 @@ type Handler interface {
ListVolumes(http.ResponseWriter, *http.Request)
ListDeployments(http.ResponseWriter, *http.Request)
ListStatefulSets(http.ResponseWriter, *http.Request)
ListJobs(http.ResponseWriter, *http.Request)
}
type Module interface {
@@ -28,4 +29,5 @@ type Module interface {
ListVolumes(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableVolumes) (*inframonitoringtypes.Volumes, error)
ListDeployments(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableDeployments) (*inframonitoringtypes.Deployments, error)
ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableStatefulSets) (*inframonitoringtypes.StatefulSets, error)
ListJobs(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableJobs) (*inframonitoringtypes.Jobs, error)
}

View File

@@ -200,6 +200,7 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewAddServiceAccountManagedRoleTransactionsFactory(sqlstore),
sqlmigration.NewAddSpanMapperFactory(sqlstore, sqlschema),
sqlmigration.NewAddLLMPricingRulesFactory(sqlstore, sqlschema),
sqlmigration.NewMigrateMetaresourcesTuplesFactory(sqlstore),
)
}

View File

@@ -0,0 +1,175 @@
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 migrateMetaresourcesTuples struct {
sqlstore sqlstore.SQLStore
}
func NewMigrateMetaresourcesTuplesFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("migrate_metaresources_tuples"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &migrateMetaresourcesTuples{sqlstore: sqlstore}, nil
})
}
func (migration *migrateMetaresourcesTuples) Register(migrations *migrate.Migrations) error {
return migrations.Register(migration.Up, migration.Down)
}
// migrationTuple describes a single FGA tuple to insert.
type migrationTuple struct {
roleName string // "signoz-admin", "signoz-editor", "signoz-viewer"
objectType string // "serviceaccount", "user", "role", "metaresource"
objectName string // "serviceaccount", "user", "role", etc.
relation string // "create", "list", "detach", etc.
}
func (migration *migrateMetaresourcesTuples) Up(ctx context.Context, db *bun.DB) error {
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
// Step 1: Delete all tuples with the old "metaresources" object_type.
for _, orgID := range orgIDs {
if isPG {
_, err = tx.ExecContext(ctx, `DELETE FROM tuple WHERE store = ? AND object_type = ? AND object_id LIKE ?`,
storeID, "metaresources", "organization/"+orgID+"/%")
} else {
_, err = tx.ExecContext(ctx, `DELETE FROM tuple WHERE store = ? AND object_type = ? AND object_id LIKE ?`,
storeID, "metaresources", "organization/"+orgID+"/%")
}
if err != nil {
return err
}
}
// Step 2: Insert replacement tuples.
// For types with their own FGA type (user, serviceaccount, role), create/list
// go on the type directly. For all other resources, create/list go on "metaresource".
// Also add new detach tuples for role/user/serviceaccount.
tuples := []migrationTuple{
// New detach tuples for admin
{authtypes.SigNozAdminRoleName, "role", "role", "detach"},
{authtypes.SigNozAdminRoleName, "serviceaccount", "serviceaccount", "detach"},
// Replacement create/list for user/serviceaccount/role (moved from metaresources to own types)
{authtypes.SigNozAdminRoleName, "serviceaccount", "serviceaccount", "create"},
{authtypes.SigNozAdminRoleName, "serviceaccount", "serviceaccount", "list"},
{authtypes.SigNozAdminRoleName, "role", "role", "create"},
{authtypes.SigNozAdminRoleName, "role", "role", "list"},
// Replacement create/list for resources that move from "metaresources" to "metaresource"
{authtypes.SigNozAdminRoleName, "metaresource", "factor-api-key", "create"},
{authtypes.SigNozAdminRoleName, "metaresource", "factor-api-key", "list"},
{authtypes.SigNozAdminRoleName, "metaresource", "factor-api-key", "read"},
{authtypes.SigNozAdminRoleName, "metaresource", "factor-api-key", "update"},
{authtypes.SigNozAdminRoleName, "metaresource", "factor-api-key", "delete"},
}
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 *migrateMetaresourcesTuples) Down(context.Context, *bun.DB) error {
return nil
}

View File

@@ -1,8 +1,6 @@
package sqlschema
import (
"fmt"
"hash/fnv"
"slices"
"strings"
@@ -51,23 +49,9 @@ type Index interface {
ToDropSQL(fmter SQLFormatter) []byte
}
// UniqueIndex models a unique index on a table.
//
// In the common case the index keys on plain columns: set only ColumnNames and
// the SQL is emitted with each column identifier-quoted by the formatter
// (`CREATE UNIQUE INDEX uq_t_a_b ON t (a, b)`).
//
// For functional indexes (e.g. case-insensitive uniqueness on `LOWER(col)`),
// set Expressions to the raw SQL parts and use ColumnNames as metadata for
// "which columns does this index touch". When Expressions is non-empty, it
// overrides ColumnNames for SQL emission — each entry is written verbatim, so
// the caller owns well-formedness — and the auto-generated name uses a hash
// suffix instead of a readable column join because expressions aren't valid
// identifier fragments.
type UniqueIndex struct {
TableName TableName
ColumnNames []ColumnName
Expressions []string
name string
}
@@ -87,28 +71,16 @@ func (index *UniqueIndex) Name() string {
}
b.WriteString(string(column))
}
if len(index.Expressions) > 0 {
if len(index.ColumnNames) > 0 {
b.WriteString("_")
}
hasher := fnv.New32a()
_, _ = hasher.Write([]byte(strings.Join(index.Expressions, "\x00")))
fmt.Fprintf(&b, "%08x", hasher.Sum32())
}
return b.String()
}
func (index *UniqueIndex) Named(name string) Index {
copyOfColumnNames := make([]ColumnName, len(index.ColumnNames))
copy(copyOfColumnNames, index.ColumnNames)
copyOfExpressions := make([]string, len(index.Expressions))
copy(copyOfExpressions, index.Expressions)
return &UniqueIndex{
TableName: index.TableName,
ColumnNames: copyOfColumnNames,
Expressions: copyOfExpressions,
name: name,
}
}
@@ -129,18 +101,7 @@ func (index *UniqueIndex) Equals(other Index) bool {
if other.Type() != IndexTypeUnique {
return false
}
otherUnique, ok := other.(*UniqueIndex)
if !ok {
return false
}
// Plain and functional indexes produce different SQL even if their column
// sets overlap; require both shapes to match.
if (len(index.Expressions) == 0) != (len(otherUnique.Expressions) == 0) {
return false
}
if len(index.Expressions) > 0 && !slices.Equal(index.Expressions, otherUnique.Expressions) {
return false
}
return index.Name() == other.Name() && slices.Equal(index.Columns(), other.Columns())
}
@@ -153,20 +114,12 @@ func (index *UniqueIndex) ToCreateSQL(fmter SQLFormatter) []byte {
sql = fmter.AppendIdent(sql, string(index.TableName))
sql = append(sql, " ("...)
if len(index.Expressions) > 0 {
for i, expr := range index.Expressions {
if i > 0 {
sql = append(sql, ", "...)
}
sql = append(sql, expr...)
}
} else {
for i, column := range index.ColumnNames {
if i > 0 {
sql = append(sql, ", "...)
}
sql = fmter.AppendIdent(sql, string(column))
for i, column := range index.ColumnNames {
if i > 0 {
sql = append(sql, ", "...)
}
sql = fmter.AppendIdent(sql, string(column))
}
sql = append(sql, ")"...)

View File

@@ -38,43 +38,6 @@ func TestIndexToCreateSQL(t *testing.T) {
},
sql: `CREATE UNIQUE INDEX IF NOT EXISTS "my_index" ON "users" ("id", "name", "email")`,
},
{
name: "Unique_Functional_SingleExpression",
index: &UniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Expressions: []string{"LOWER(email)"},
},
sql: `CREATE UNIQUE INDEX IF NOT EXISTS "uq_users_email_1e5a87f1" ON "users" (LOWER(email))`,
},
{
name: "Unique_Functional_MixedColumnsAndExpressions",
index: &UniqueIndex{
TableName: "tag",
ColumnNames: []ColumnName{"org_id", "kind", "key", "value"},
Expressions: []string{"org_id", "kind", "LOWER(key)", "LOWER(value)"},
},
sql: `CREATE UNIQUE INDEX IF NOT EXISTS "uq_tag_org_id_kind_key_value_57e8f81f" ON "tag" (org_id, kind, LOWER(key), LOWER(value))`,
},
{
name: "Unique_Functional_ComplexExpression",
index: &UniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"first_name", "last_name"},
Expressions: []string{"LOWER(TRIM(first_name) || ' ' || TRIM(last_name))"},
},
sql: `CREATE UNIQUE INDEX IF NOT EXISTS "uq_users_first_name_last_name_adb1ff53" ON "users" (LOWER(TRIM(first_name) || ' ' || TRIM(last_name)))`,
},
{
name: "Unique_Functional_Named",
index: &UniqueIndex{
TableName: "tag",
ColumnNames: []ColumnName{"org_id", "kind", "key", "value"},
Expressions: []string{"org_id", "kind", "LOWER(key)", "LOWER(value)"},
name: "uq_tag_org_kind_lower_key_lower_value",
},
sql: `CREATE UNIQUE INDEX IF NOT EXISTS "uq_tag_org_kind_lower_key_lower_value" ON "tag" (org_id, kind, LOWER(key), LOWER(value))`,
},
{
name: "PartialUnique_1Column",
index: &PartialUniqueIndex{
@@ -266,47 +229,6 @@ func TestIndexEquals(t *testing.T) {
},
equals: false,
},
{
name: "Unique_Functional_Same",
a: &UniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Expressions: []string{"LOWER(email)"},
},
b: &UniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Expressions: []string{"LOWER(email)"},
},
equals: true,
},
{
name: "Unique_Functional_DifferentExpressions",
a: &UniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Expressions: []string{"LOWER(email)"},
},
b: &UniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Expressions: []string{"UPPER(email)"},
},
equals: false,
},
{
name: "Unique_Functional_NotEqualToPlainSameColumns",
a: &UniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Expressions: []string{"LOWER(email)"},
},
b: &UniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
},
equals: false,
},
}
for _, testCase := range testCases {
@@ -316,75 +238,6 @@ func TestIndexEquals(t *testing.T) {
}
}
func TestUniqueIndexFunctionalName(t *testing.T) {
t.Run("autogen uses uq_<table>_<hash>", func(t *testing.T) {
idx := &UniqueIndex{
TableName: "tag",
ColumnNames: []ColumnName{"org_id", "kind", "key", "value"},
Expressions: []string{"org_id", "kind", "LOWER(key)", "LOWER(value)"},
}
assert.Equal(t, "uq_tag_org_id_kind_key_value_57e8f81f", idx.Name())
})
t.Run("same expressions produce the same name", func(t *testing.T) {
a := &UniqueIndex{
TableName: "users",
Expressions: []string{"LOWER(email)"},
}
b := &UniqueIndex{
TableName: "users",
Expressions: []string{"LOWER(email)"},
}
assert.Equal(t, a.Name(), b.Name())
})
t.Run("different expressions produce different names", func(t *testing.T) {
a := &UniqueIndex{
TableName: "users",
Expressions: []string{"LOWER(email)"},
}
b := &UniqueIndex{
TableName: "users",
Expressions: []string{"UPPER(email)"},
}
assert.NotEqual(t, a.Name(), b.Name())
})
t.Run("expressions in different order produce different names", func(t *testing.T) {
a := &UniqueIndex{
TableName: "tag",
Expressions: []string{"org_id", "LOWER(key)"},
}
b := &UniqueIndex{
TableName: "tag",
Expressions: []string{"LOWER(key)", "org_id"},
}
assert.NotEqual(t, a.Name(), b.Name())
})
t.Run("functional autogen differs from plain autogen for same columns", func(t *testing.T) {
plain := &UniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
}
functional := &UniqueIndex{
TableName: "users",
ColumnNames: []ColumnName{"email"},
Expressions: []string{"LOWER(email)"},
}
assert.Equal(t, "uq_users_email", plain.Name())
assert.NotEqual(t, plain.Name(), functional.Name())
})
t.Run("Named() override wins over hash", func(t *testing.T) {
idx := (&UniqueIndex{
TableName: "tag",
Expressions: []string{"org_id", "LOWER(key)"},
}).Named("my_functional_index")
assert.Equal(t, "my_functional_index", idx.Name())
})
}
func TestPartialUniqueIndexName(t *testing.T) {
a := &PartialUniqueIndex{
TableName: "users",

View File

@@ -47,6 +47,42 @@ func NewTuplesFromTransactions(transactions []*Transaction, subject string, orgI
return tuples, nil
}
// NewTuplesFromTransactionsWithCorrelations converts transactions to tuples for BatchCheck,
// and for each transaction whose selector is not already a wildcard, generates an additional
// tuple with the wildcard selector. This ensures that permissions granted via wildcard
// selectors (e.g., dashboard:*) are checked alongside exact selectors (e.g., dashboard:abc-123).
//
// Returns:
// - tuples: all tuples to check (exact + correlated), keyed by transaction ID or generated correlation ID
// - correlations: maps transaction ID to a slice of correlation IDs for the additional tuples
func NewTuplesFromTransactionsWithCorrelations(transactions []*Transaction, subject string, orgID valuer.UUID) (tuples map[string]*openfgav1.TupleKey, correlations map[string][]string, err error) {
tuples = make(map[string]*openfgav1.TupleKey)
correlations = make(map[string][]string)
for _, txn := range transactions {
resource, err := coretypes.NewResourceFromTypeAndKind(txn.Object.Resource.Type, txn.Object.Resource.Kind)
if err != nil {
return nil, nil, err
}
txnID := txn.ID.StringValue()
txnTuples := NewTuples(resource, subject, txn.Relation, []coretypes.Selector{txn.Object.Selector}, orgID)
tuples[txnID] = txnTuples[0]
if txn.Object.Selector.String() != coretypes.WildCardSelectorString {
wildcardSelector := txn.Object.Resource.Type.MustSelector(coretypes.WildCardSelectorString)
wildcardTuples := NewTuples(resource, subject, txn.Relation, []coretypes.Selector{wildcardSelector}, orgID)
correlationID := valuer.GenerateUUID().StringValue()
tuples[correlationID] = wildcardTuples[0]
correlations[txnID] = append(correlations[txnID], correlationID)
}
}
return tuples, correlations, nil
}
// NewTuplesFromTransactionsWithManagedRoles converts transactions to tuples for BatchCheck.
// Direct role-assignment transactions (TypeRole + VerbAssignee) produce one tuple keyed by txn ID.
// Other transactions are expanded via managedRolesByTransaction into role-assignee checks, keyed by "txnID:roleName".

View File

@@ -18,46 +18,49 @@ const (
var ManagedRoleToTransactions = map[string][]Transaction{
SigNozAdminRoleName: {
// role attach — admin can attach/detach role assignments
// role attach/detach — admin can attach/detach role assignments
{Verb: VerbAttach, Object: *MustNewObject(ResourceRef{Type: TypeRole, Kind: KindRole}, WildCardSelectorString)},
// user attach — admin can attach roles to any user
{Verb: VerbDetach, Object: *MustNewObject(ResourceRef{Type: TypeRole, Kind: KindRole}, WildCardSelectorString)},
// user attach/detach — admin can attach/detach roles to any user
{Verb: VerbAttach, Object: *MustNewObject(ResourceRef{Type: TypeUser, Kind: KindUser}, WildCardSelectorString)},
// serviceaccount attach — admin can attach roles to any SA
{Verb: VerbDetach, Object: *MustNewObject(ResourceRef{Type: TypeUser, Kind: KindUser}, WildCardSelectorString)},
// serviceaccount attach/detach — admin can attach/detach roles to any SA
{Verb: VerbAttach, Object: *MustNewObject(ResourceRef{Type: TypeServiceAccount, Kind: KindServiceAccount}, WildCardSelectorString)},
{Verb: VerbDetach, Object: *MustNewObject(ResourceRef{Type: TypeServiceAccount, Kind: KindServiceAccount}, WildCardSelectorString)},
// auth-domain — admin only
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindAuthDomain}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindAuthDomain}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindAuthDomain}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindAuthDomain}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindAuthDomain}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindAuthDomain}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindAuthDomain}, WildCardSelectorString)},
// cloud-integration — admin only
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindCloudIntegration}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindCloudIntegration}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindCloudIntegration}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindCloudIntegration}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindCloudIntegration}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindCloudIntegration}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindCloudIntegration}, WildCardSelectorString)},
// cloud-integration-service — admin only
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindCloudIntegrationService}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindCloudIntegrationService}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindCloudIntegrationService}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindCloudIntegrationService}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindCloudIntegrationService}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindCloudIntegrationService}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindCloudIntegrationService}, WildCardSelectorString)},
// integration — viewer/editor/admin (install/uninstall via ViewAccess)
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIntegration}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIntegration}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIntegration}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindIntegration}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindIntegration}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIntegration}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIntegration}, WildCardSelectorString)},
// factor-api-key — admin only
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindFactorAPIKey}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindFactorAPIKey}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindFactorAPIKey}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindFactorAPIKey}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindFactorAPIKey}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindFactorAPIKey}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindFactorAPIKey}, WildCardSelectorString)},
// factor-password — admin can issue and inspect reset tokens; users change their own password via OpenAccess
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindFactorPassword}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindFactorPassword}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindFactorPassword}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindFactorPassword}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindFactorPassword}, WildCardSelectorString)},
// license — admin only.
// Uniform LCRUD shape; actual ee routes are POST /api/v3/licenses (create
// = Activate), PUT /api/v3/licenses (update = Refresh), GET
@@ -67,8 +70,8 @@ var ManagedRoleToTransactions = map[string][]Transaction{
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindLicense}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindLicense}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindLicense}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindLicense}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindLicense}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindLicense}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindLicense}, WildCardSelectorString)},
// subscription — admin only.
// Uniform LCRUD shape; actual ee routes are POST /api/v1/checkout
// (create), POST /api/v1/portal (update — opens Stripe portal), GET
@@ -78,123 +81,121 @@ var ManagedRoleToTransactions = map[string][]Transaction{
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindSubscription}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindSubscription}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindSubscription}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindSubscription}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindSubscription}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindSubscription}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindSubscription}, WildCardSelectorString)},
// organization — admin only
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeOrganization, Kind: KindOrganization}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeOrganization, Kind: KindOrganization}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindOrganization}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindOrganization}, WildCardSelectorString)},
// org-preference — admin only
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindOrgPreference}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindOrgPreference}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindOrgPreference}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindOrgPreference}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindOrgPreference}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindOrgPreference}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindOrgPreference}, WildCardSelectorString)},
// public-dashboard — admin manages, anonymous reads
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPublicDashboard}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPublicDashboard}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPublicDashboard}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindPublicDashboard}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindPublicDashboard}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPublicDashboard}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPublicDashboard}, WildCardSelectorString)},
// role — admin only
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeRole, Kind: KindRole}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeRole, Kind: KindRole}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeRole, Kind: KindRole}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindRole}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindRole}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeRole, Kind: KindRole}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeRole, Kind: KindRole}, WildCardSelectorString)},
// serviceaccount — admin only
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeServiceAccount, Kind: KindServiceAccount}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeServiceAccount, Kind: KindServiceAccount}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeServiceAccount, Kind: KindServiceAccount}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindServiceAccount}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindServiceAccount}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeServiceAccount, Kind: KindServiceAccount}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeServiceAccount, Kind: KindServiceAccount}, WildCardSelectorString)},
// session — admin can revoke and list
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindSession}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindSession}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindSession}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindSession}, WildCardSelectorString)},
// user — admin only
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeUser, Kind: KindUser}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeUser, Kind: KindUser}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeUser, Kind: KindUser}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindUser}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindUser}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeUser, Kind: KindUser}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeUser, Kind: KindUser}, WildCardSelectorString)},
// dashboard — full CRUD (also held by editor)
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindDashboard}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindDashboard}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindDashboard}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindDashboard}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindDashboard}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindDashboard}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindDashboard}, WildCardSelectorString)},
// pipeline — full CRUD
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPipeline}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPipeline}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPipeline}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindPipeline}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindPipeline}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPipeline}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPipeline}, WildCardSelectorString)},
// planned-maintenance — full CRUD
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPlannedMaintenance}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPlannedMaintenance}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPlannedMaintenance}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindPlannedMaintenance}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindPlannedMaintenance}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPlannedMaintenance}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPlannedMaintenance}, WildCardSelectorString)},
// rule — full CRUD
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRule}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRule}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRule}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindRule}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindRule}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRule}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRule}, WildCardSelectorString)},
// saved-view — full CRUD
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindSavedView}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindSavedView}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindSavedView}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindSavedView}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindSavedView}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindSavedView}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindSavedView}, WildCardSelectorString)},
// trace-funnel — full CRUD
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTraceFunnel}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTraceFunnel}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTraceFunnel}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindTraceFunnel}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindTraceFunnel}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTraceFunnel}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTraceFunnel}, WildCardSelectorString)},
// ingestion-key — editor+admin only
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIngestionKey}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIngestionKey}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIngestionKey}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindIngestionKey}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindIngestionKey}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIngestionKey}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIngestionKey}, WildCardSelectorString)},
// ingestion-limit — editor+admin only
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIngestionLimit}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIngestionLimit}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIngestionLimit}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindIngestionLimit}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindIngestionLimit}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIngestionLimit}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIngestionLimit}, WildCardSelectorString)},
// notification-channel — admin writes, viewer reads
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindNotificationChannel}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindNotificationChannel}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindNotificationChannel}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindNotificationChannel}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindNotificationChannel}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindNotificationChannel}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindNotificationChannel}, WildCardSelectorString)},
// route-policy — admin writes, viewer reads
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRoutePolicy}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRoutePolicy}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRoutePolicy}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindRoutePolicy}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindRoutePolicy}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRoutePolicy}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRoutePolicy}, WildCardSelectorString)},
// apdex-setting — admin updates, viewer reads
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindApdexSetting}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindApdexSetting}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindApdexSetting}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindApdexSetting}, WildCardSelectorString)},
// quick-filter — admin updates, viewer reads
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindQuickFilter}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindQuickFilter}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindQuickFilter}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindQuickFilter}, WildCardSelectorString)},
// ttl-setting — admin updates, viewer reads
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTTLSetting}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTTLSetting}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindTTLSetting}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTTLSetting}, WildCardSelectorString)},
// user-preference — every authenticated user can read+update their own
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindUserPreference}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindUserPreference}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindUserPreference}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindUserPreference}, WildCardSelectorString)},
// telemetry — read on each signal (logs/traces/metrics); schema permits read only
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeTelemetryResource, Kind: KindLogs}, WildCardSelectorString)},
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeTelemetryResource, Kind: KindTraces}, WildCardSelectorString)},
@@ -207,86 +208,86 @@ var ManagedRoleToTransactions = map[string][]Transaction{
// logs-field — editor+admin update (POST overwrites field config), viewer reads
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindLogsField}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindLogsField}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindLogsField}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindLogsField}, WildCardSelectorString)},
// traces-field — editor+admin update, viewer reads
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTracesField}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTracesField}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindTracesField}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTracesField}, WildCardSelectorString)},
},
SigNozEditorRoleName: {
// dashboard — full CRUD
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindDashboard}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindDashboard}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindDashboard}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindDashboard}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindDashboard}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindDashboard}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindDashboard}, WildCardSelectorString)},
// pipeline — full CRUD
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPipeline}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPipeline}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPipeline}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindPipeline}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindPipeline}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPipeline}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPipeline}, WildCardSelectorString)},
// planned-maintenance — full CRUD
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPlannedMaintenance}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPlannedMaintenance}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPlannedMaintenance}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindPlannedMaintenance}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindPlannedMaintenance}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPlannedMaintenance}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPlannedMaintenance}, WildCardSelectorString)},
// rule — full CRUD
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRule}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRule}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRule}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindRule}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindRule}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRule}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRule}, WildCardSelectorString)},
// saved-view — full CRUD
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindSavedView}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindSavedView}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindSavedView}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindSavedView}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindSavedView}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindSavedView}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindSavedView}, WildCardSelectorString)},
// trace-funnel — full CRUD
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTraceFunnel}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTraceFunnel}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTraceFunnel}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindTraceFunnel}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindTraceFunnel}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTraceFunnel}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTraceFunnel}, WildCardSelectorString)},
// integration — viewer/editor/admin
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIntegration}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIntegration}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIntegration}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindIntegration}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindIntegration}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIntegration}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIntegration}, WildCardSelectorString)},
// ingestion-key — editor+admin only
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIngestionKey}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIngestionKey}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIngestionKey}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindIngestionKey}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindIngestionKey}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIngestionKey}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIngestionKey}, WildCardSelectorString)},
// ingestion-limit — editor+admin only
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIngestionLimit}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIngestionLimit}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIngestionLimit}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindIngestionLimit}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindIngestionLimit}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIngestionLimit}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIngestionLimit}, WildCardSelectorString)},
// notification-channel — read only (admin writes)
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindNotificationChannel}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindNotificationChannel}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindNotificationChannel}, WildCardSelectorString)},
// route-policy — read only (admin writes)
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRoutePolicy}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindRoutePolicy}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRoutePolicy}, WildCardSelectorString)},
// apdex-setting — read only (admin updates)
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindApdexSetting}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindApdexSetting}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindApdexSetting}, WildCardSelectorString)},
// quick-filter — read only (admin updates)
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindQuickFilter}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindQuickFilter}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindQuickFilter}, WildCardSelectorString)},
// ttl-setting — read only (admin updates)
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTTLSetting}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindTTLSetting}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTTLSetting}, WildCardSelectorString)},
// user-preference — every authenticated user can read+update their own
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindUserPreference}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindUserPreference}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindUserPreference}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindUserPreference}, WildCardSelectorString)},
// telemetry — read on each signal
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeTelemetryResource, Kind: KindLogs}, WildCardSelectorString)},
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeTelemetryResource, Kind: KindTraces}, WildCardSelectorString)},
@@ -294,66 +295,66 @@ var ManagedRoleToTransactions = map[string][]Transaction{
// logs-field — editor reads+updates
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindLogsField}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindLogsField}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindLogsField}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindLogsField}, WildCardSelectorString)},
// traces-field — editor reads+updates
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTracesField}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTracesField}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindTracesField}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTracesField}, WildCardSelectorString)},
},
SigNozViewerRoleName: {
// dashboard — read only
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindDashboard}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindDashboard}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindDashboard}, WildCardSelectorString)},
// pipeline — read only
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPipeline}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindPipeline}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPipeline}, WildCardSelectorString)},
// planned-maintenance — read only
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPlannedMaintenance}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindPlannedMaintenance}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPlannedMaintenance}, WildCardSelectorString)},
// rule — read only
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRule}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindRule}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRule}, WildCardSelectorString)},
// saved-view — read only
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindSavedView}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindSavedView}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindSavedView}, WildCardSelectorString)},
// trace-funnel — read only
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTraceFunnel}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindTraceFunnel}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTraceFunnel}, WildCardSelectorString)},
// integration — viewer/editor/admin (install/uninstall via ViewAccess)
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIntegration}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIntegration}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIntegration}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindIntegration}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindIntegration}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIntegration}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindIntegration}, WildCardSelectorString)},
// notification-channel — read only
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindNotificationChannel}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindNotificationChannel}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindNotificationChannel}, WildCardSelectorString)},
// route-policy — read only
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRoutePolicy}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindRoutePolicy}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRoutePolicy}, WildCardSelectorString)},
// apdex-setting — read only
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindApdexSetting}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindApdexSetting}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindApdexSetting}, WildCardSelectorString)},
// quick-filter — read only
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindQuickFilter}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindQuickFilter}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindQuickFilter}, WildCardSelectorString)},
// ttl-setting — read only
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTTLSetting}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindTTLSetting}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTTLSetting}, WildCardSelectorString)},
// user-preference — every authenticated user can read+update their own
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindUserPreference}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindUserPreference}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindUserPreference}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindUserPreference}, WildCardSelectorString)},
// telemetry — read on each signal
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeTelemetryResource, Kind: KindLogs}, WildCardSelectorString)},
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeTelemetryResource, Kind: KindTraces}, WildCardSelectorString)},
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeTelemetryResource, Kind: KindMetrics}, WildCardSelectorString)},
// logs-field — viewer reads
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindLogsField}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindLogsField}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindLogsField}, WildCardSelectorString)},
// traces-field — viewer reads
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTracesField}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResources, Kind: KindTracesField}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTracesField}, WildCardSelectorString)},
},
SigNozAnonymousRoleName: {
// public-dashboard — anonymous read

View File

@@ -6,138 +6,76 @@ var Resources = []Resource{
ResourceRole,
ResourceServiceAccount,
ResourceUser,
ResourceMetaResourcesRole,
ResourceMetaResourcesOrganization,
ResourceMetaResourcesServiceAccount,
ResourceMetaResourcesUser,
ResourceMetaResourceNotificationChannel,
ResourceMetaResourcesNotificationChannel,
ResourceMetaResourceRoutePolicy,
ResourceMetaResourcesRoutePolicy,
ResourceMetaResourceApdexSetting,
ResourceMetaResourcesApdexSetting,
ResourceMetaResourceAuthDomain,
ResourceMetaResourcesAuthDomain,
ResourceMetaResourceSession,
ResourceMetaResourcesSession,
ResourceMetaResourceCloudIntegration,
ResourceMetaResourcesCloudIntegration,
ResourceMetaResourceCloudIntegrationService,
ResourceMetaResourcesCloudIntegrationService,
ResourceMetaResourceIntegration,
ResourceMetaResourcesIntegration,
ResourceMetaResourceDashboard,
ResourceMetaResourcesDashboard,
ResourceMetaResourcePublicDashboard,
ResourceMetaResourcesPublicDashboard,
ResourceMetaResourceIngestionKey,
ResourceMetaResourcesIngestionKey,
ResourceMetaResourceIngestionLimit,
ResourceMetaResourcesIngestionLimit,
ResourceMetaResourcePipeline,
ResourceMetaResourcesPipeline,
ResourceMetaResourceUserPreference,
ResourceMetaResourcesUserPreference,
ResourceMetaResourceOrgPreference,
ResourceMetaResourcesOrgPreference,
ResourceMetaResourceQuickFilter,
ResourceMetaResourcesQuickFilter,
ResourceMetaResourceTTLSetting,
ResourceMetaResourcesTTLSetting,
ResourceMetaResourceRule,
ResourceMetaResourcesRule,
ResourceMetaResourcePlannedMaintenance,
ResourceMetaResourcesPlannedMaintenance,
ResourceMetaResourceSavedView,
ResourceMetaResourcesSavedView,
ResourceMetaResourceTraceFunnel,
ResourceMetaResourcesTraceFunnel,
ResourceMetaResourceFactorPassword,
ResourceMetaResourcesFactorPassword,
ResourceMetaResourceFactorAPIKey,
ResourceMetaResourcesFactorAPIKey,
ResourceMetaResourceLicense,
ResourceMetaResourcesLicense,
ResourceMetaResourceSubscription,
ResourceMetaResourcesSubscription,
ResourceTelemetryResourceLogs,
ResourceTelemetryResourceTraces,
ResourceTelemetryResourceMetrics,
ResourceTelemetryResourceAuditLogs,
ResourceTelemetryResourceMeterMetrics,
ResourceMetaResourceLogsField,
ResourceMetaResourcesLogsField,
ResourceMetaResourceTracesField,
ResourceMetaResourcesTracesField,
}
var (
ResourceAnonymous Resource = NewResourceAnonymous()
ResourceOrganization = NewResourceOrganization()
ResourceRole = NewResourceRole()
ResourceServiceAccount = NewResourceServiceAccount()
ResourceUser = NewResourceUser()
ResourceMetaResourcesRole = NewResourceMetaResources(KindRole)
ResourceMetaResourcesOrganization = NewResourceMetaResources(KindOrganization)
ResourceMetaResourcesServiceAccount = NewResourceMetaResources(KindServiceAccount)
ResourceMetaResourcesUser = NewResourceMetaResources(KindUser)
ResourceMetaResourceNotificationChannel = NewResourceMetaResource(KindNotificationChannel)
ResourceMetaResourcesNotificationChannel = NewResourceMetaResources(KindNotificationChannel)
ResourceMetaResourceRoutePolicy = NewResourceMetaResource(KindRoutePolicy)
ResourceMetaResourcesRoutePolicy = NewResourceMetaResources(KindRoutePolicy)
ResourceMetaResourceApdexSetting = NewResourceMetaResource(KindApdexSetting)
ResourceMetaResourcesApdexSetting = NewResourceMetaResources(KindApdexSetting)
ResourceMetaResourceAuthDomain = NewResourceMetaResource(KindAuthDomain)
ResourceMetaResourcesAuthDomain = NewResourceMetaResources(KindAuthDomain)
ResourceMetaResourceSession = NewResourceMetaResource(KindSession)
ResourceMetaResourcesSession = NewResourceMetaResources(KindSession)
ResourceMetaResourceCloudIntegration = NewResourceMetaResource(KindCloudIntegration)
ResourceMetaResourcesCloudIntegration = NewResourceMetaResources(KindCloudIntegration)
ResourceMetaResourceCloudIntegrationService = NewResourceMetaResource(KindCloudIntegrationService)
ResourceMetaResourcesCloudIntegrationService = NewResourceMetaResources(KindCloudIntegrationService)
ResourceMetaResourceIntegration = NewResourceMetaResource(KindIntegration)
ResourceMetaResourcesIntegration = NewResourceMetaResources(KindIntegration)
ResourceMetaResourceDashboard = NewResourceMetaResource(KindDashboard)
ResourceMetaResourcesDashboard = NewResourceMetaResources(KindDashboard)
ResourceMetaResourcePublicDashboard = NewResourceMetaResource(KindPublicDashboard)
ResourceMetaResourcesPublicDashboard = NewResourceMetaResources(KindPublicDashboard)
ResourceMetaResourceIngestionKey = NewResourceMetaResource(KindIngestionKey)
ResourceMetaResourcesIngestionKey = NewResourceMetaResources(KindIngestionKey)
ResourceMetaResourceIngestionLimit = NewResourceMetaResource(KindIngestionLimit)
ResourceMetaResourcesIngestionLimit = NewResourceMetaResources(KindIngestionLimit)
ResourceMetaResourcePipeline = NewResourceMetaResource(KindPipeline)
ResourceMetaResourcesPipeline = NewResourceMetaResources(KindPipeline)
ResourceMetaResourceUserPreference = NewResourceMetaResource(KindUserPreference)
ResourceMetaResourcesUserPreference = NewResourceMetaResources(KindUserPreference)
ResourceMetaResourceOrgPreference = NewResourceMetaResource(KindOrgPreference)
ResourceMetaResourcesOrgPreference = NewResourceMetaResources(KindOrgPreference)
ResourceMetaResourceQuickFilter = NewResourceMetaResource(KindQuickFilter)
ResourceMetaResourcesQuickFilter = NewResourceMetaResources(KindQuickFilter)
ResourceMetaResourceTTLSetting = NewResourceMetaResource(KindTTLSetting)
ResourceMetaResourcesTTLSetting = NewResourceMetaResources(KindTTLSetting)
ResourceMetaResourceRule = NewResourceMetaResource(KindRule)
ResourceMetaResourcesRule = NewResourceMetaResources(KindRule)
ResourceMetaResourcePlannedMaintenance = NewResourceMetaResource(KindPlannedMaintenance)
ResourceMetaResourcesPlannedMaintenance = NewResourceMetaResources(KindPlannedMaintenance)
ResourceMetaResourceSavedView = NewResourceMetaResource(KindSavedView)
ResourceMetaResourcesSavedView = NewResourceMetaResources(KindSavedView)
ResourceMetaResourceTraceFunnel = NewResourceMetaResource(KindTraceFunnel)
ResourceMetaResourcesTraceFunnel = NewResourceMetaResources(KindTraceFunnel)
ResourceMetaResourceFactorPassword = NewResourceMetaResource(KindFactorPassword)
ResourceMetaResourcesFactorPassword = NewResourceMetaResources(KindFactorPassword)
ResourceMetaResourceFactorAPIKey = NewResourceMetaResource(KindFactorAPIKey)
ResourceMetaResourcesFactorAPIKey = NewResourceMetaResources(KindFactorAPIKey)
ResourceMetaResourceLicense = NewResourceMetaResource(KindLicense)
ResourceMetaResourcesLicense = NewResourceMetaResources(KindLicense)
ResourceMetaResourceSubscription = NewResourceMetaResource(KindSubscription)
ResourceMetaResourcesSubscription = NewResourceMetaResources(KindSubscription)
ResourceTelemetryResourceLogs = NewResourceTelemetryResource(KindLogs)
ResourceTelemetryResourceTraces = NewResourceTelemetryResource(KindTraces)
ResourceTelemetryResourceMetrics = NewResourceTelemetryResource(KindMetrics)
ResourceTelemetryResourceAuditLogs = NewResourceTelemetryResource(KindAuditLogs)
ResourceTelemetryResourceMeterMetrics = NewResourceTelemetryResource(KindMeterMetrics)
ResourceMetaResourceLogsField = NewResourceMetaResource(KindLogsField)
ResourceMetaResourcesLogsField = NewResourceMetaResources(KindLogsField)
ResourceMetaResourceTracesField = NewResourceMetaResource(KindTracesField)
ResourceMetaResourcesTracesField = NewResourceMetaResources(KindTracesField)
ResourceAnonymous Resource = NewResourceAnonymous()
ResourceOrganization = NewResourceOrganization()
ResourceRole = NewResourceRole()
ResourceServiceAccount = NewResourceServiceAccount()
ResourceUser = NewResourceUser()
ResourceMetaResourceNotificationChannel = NewResourceMetaResource(KindNotificationChannel)
ResourceMetaResourceRoutePolicy = NewResourceMetaResource(KindRoutePolicy)
ResourceMetaResourceApdexSetting = NewResourceMetaResource(KindApdexSetting)
ResourceMetaResourceAuthDomain = NewResourceMetaResource(KindAuthDomain)
ResourceMetaResourceSession = NewResourceMetaResource(KindSession)
ResourceMetaResourceCloudIntegration = NewResourceMetaResource(KindCloudIntegration)
ResourceMetaResourceCloudIntegrationService = NewResourceMetaResource(KindCloudIntegrationService)
ResourceMetaResourceIntegration = NewResourceMetaResource(KindIntegration)
ResourceMetaResourceDashboard = NewResourceMetaResource(KindDashboard)
ResourceMetaResourcePublicDashboard = NewResourceMetaResource(KindPublicDashboard)
ResourceMetaResourceIngestionKey = NewResourceMetaResource(KindIngestionKey)
ResourceMetaResourceIngestionLimit = NewResourceMetaResource(KindIngestionLimit)
ResourceMetaResourcePipeline = NewResourceMetaResource(KindPipeline)
ResourceMetaResourceUserPreference = NewResourceMetaResource(KindUserPreference)
ResourceMetaResourceOrgPreference = NewResourceMetaResource(KindOrgPreference)
ResourceMetaResourceQuickFilter = NewResourceMetaResource(KindQuickFilter)
ResourceMetaResourceTTLSetting = NewResourceMetaResource(KindTTLSetting)
ResourceMetaResourceRule = NewResourceMetaResource(KindRule)
ResourceMetaResourcePlannedMaintenance = NewResourceMetaResource(KindPlannedMaintenance)
ResourceMetaResourceSavedView = NewResourceMetaResource(KindSavedView)
ResourceMetaResourceTraceFunnel = NewResourceMetaResource(KindTraceFunnel)
ResourceMetaResourceFactorPassword = NewResourceMetaResource(KindFactorPassword)
ResourceMetaResourceFactorAPIKey = NewResourceMetaResource(KindFactorAPIKey)
ResourceMetaResourceLicense = NewResourceMetaResource(KindLicense)
ResourceMetaResourceSubscription = NewResourceMetaResource(KindSubscription)
ResourceTelemetryResourceLogs = NewResourceTelemetryResource(KindLogs)
ResourceTelemetryResourceTraces = NewResourceTelemetryResource(KindTraces)
ResourceTelemetryResourceMetrics = NewResourceTelemetryResource(KindMetrics)
ResourceTelemetryResourceAuditLogs = NewResourceTelemetryResource(KindAuditLogs)
ResourceTelemetryResourceMeterMetrics = NewResourceTelemetryResource(KindMeterMetrics)
ResourceMetaResourceLogsField = NewResourceMetaResource(KindLogsField)
ResourceMetaResourceTracesField = NewResourceMetaResource(KindTracesField)
)

View File

@@ -13,17 +13,15 @@ var Types = []Type{
TypeRole,
TypeOrganization,
TypeMetaResource,
TypeMetaResources,
TypeTelemetryResource,
}
var (
TypeUser = Type{valuer.NewString("user"), regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`), []Verb{VerbAttach, VerbRead, VerbUpdate, VerbDelete}}
TypeServiceAccount = Type{valuer.NewString("serviceaccount"), regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`), []Verb{VerbAttach, VerbRead, VerbUpdate, VerbDelete}}
TypeUser = Type{valuer.NewString("user"), regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`), []Verb{VerbCreate, VerbList, VerbRead, VerbUpdate, VerbDelete, VerbAttach, VerbDetach}}
TypeServiceAccount = Type{valuer.NewString("serviceaccount"), regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`), []Verb{VerbCreate, VerbList, VerbRead, VerbUpdate, VerbDelete, VerbAttach, VerbDetach}}
TypeAnonymous = Type{valuer.NewString("anonymous"), regexp.MustCompile(`^\*$`), []Verb{}}
TypeRole = Type{valuer.NewString("role"), regexp.MustCompile(`^([a-z-]{1,50}|\*)$`), []Verb{VerbAssignee, VerbAttach, VerbRead, VerbUpdate, VerbDelete}}
TypeRole = Type{valuer.NewString("role"), regexp.MustCompile(`^([a-z-]{1,50}|\*)$`), []Verb{VerbAssignee, VerbCreate, VerbList, VerbRead, VerbUpdate, VerbDelete, VerbAttach, VerbDetach}}
TypeOrganization = Type{valuer.NewString("organization"), regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`), []Verb{VerbRead, VerbUpdate, VerbDelete}}
TypeMetaResource = Type{valuer.NewString("metaresource"), regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`), []Verb{VerbRead, VerbUpdate, VerbDelete}}
TypeMetaResources = Type{valuer.NewString("metaresources"), regexp.MustCompile(`^\*$`), []Verb{VerbCreate, VerbList}}
TypeMetaResource = Type{valuer.NewString("metaresource"), regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`), []Verb{VerbCreate, VerbList, VerbRead, VerbUpdate, VerbDelete, VerbAttach, VerbDetach}}
TypeTelemetryResource = Type{valuer.NewString("telemetryresource"), regexp.MustCompile(`^\*$`), []Verb{VerbRead}}
)

View File

@@ -10,6 +10,7 @@ var Verbs = []Verb{
VerbList,
VerbAssignee,
VerbAttach,
VerbDetach,
}
var (
@@ -20,4 +21,5 @@ var (
VerbList = Verb{valuer.NewString("list"), "listed"}
VerbAssignee = Verb{valuer.NewString("assignee"), "assigned"}
VerbAttach = Verb{valuer.NewString("attach"), "attached"}
VerbDetach = Verb{valuer.NewString("detach"), "detached"}
)

View File

@@ -1,34 +0,0 @@
package coretypes
import (
"github.com/SigNoz/signoz/pkg/valuer"
)
type resourceMetaResources struct {
kind Kind
}
func NewResourceMetaResources(kind Kind) Resource {
return &resourceMetaResources{kind: kind}
}
func (*resourceMetaResources) Type() Type {
return TypeMetaResources
}
func (resourceMetaResources *resourceMetaResources) Kind() Kind {
return resourceMetaResources.kind
}
// example: metaresources:organization/0199c47d-f61b-7833-bc5f-c0730f12f046/dashboards
func (resourceMetaResources *resourceMetaResources) Prefix(orgID valuer.UUID) string {
return resourceMetaResources.Type().StringValue() + ":" + "organization" + "/" + orgID.StringValue() + "/" + resourceMetaResources.Kind().String()
}
func (resourceMetaResources *resourceMetaResources) Object(orgID valuer.UUID, selector string) string {
return resourceMetaResources.Prefix(orgID) + "/" + selector
}
func (resourceMetaResources *resourceMetaResources) Scope(verb Verb) string {
return resourceMetaResources.Kind().String() + ":" + verb.StringValue()
}

View File

@@ -33,8 +33,8 @@ func NewType(input string) (Type, error) {
return TypeOrganization, nil
case "metaresource":
return TypeMetaResource, nil
case "metaresources":
return TypeMetaResources, nil
case "telemetryresource":
return TypeTelemetryResource, nil
default:
return Type{}, errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidType, "invalid type: %s", input)
}
@@ -80,7 +80,7 @@ func (typed Type) Enum() []any {
TypeRole,
TypeOrganization,
TypeMetaResource,
TypeMetaResources,
TypeTelemetryResource,
}
}

View File

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

View File

@@ -0,0 +1,107 @@
package inframonitoringtypes
import (
"encoding/json"
"slices"
"github.com/SigNoz/signoz/pkg/errors"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
)
type Jobs struct {
Type ResponseType `json:"type" required:"true"`
Records []JobRecord `json:"records" required:"true"`
Total int `json:"total" required:"true"`
RequiredMetricsCheck RequiredMetricsCheck `json:"requiredMetricsCheck" required:"true"`
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention" required:"true"`
Warning *qbtypes.QueryWarnData `json:"warning,omitempty"`
}
type JobRecord struct {
JobName string `json:"jobName" required:"true"`
JobCPU float64 `json:"jobCPU" required:"true"`
JobCPURequest float64 `json:"jobCPURequest" required:"true"`
JobCPULimit float64 `json:"jobCPULimit" required:"true"`
JobMemory float64 `json:"jobMemory" required:"true"`
JobMemoryRequest float64 `json:"jobMemoryRequest" required:"true"`
JobMemoryLimit float64 `json:"jobMemoryLimit" required:"true"`
DesiredSuccessfulPods int `json:"desiredSuccessfulPods" required:"true"`
ActivePods int `json:"activePods" required:"true"`
FailedPods int `json:"failedPods" required:"true"`
SuccessfulPods int `json:"successfulPods" required:"true"`
PodCountsByPhase PodCountsByPhase `json:"podCountsByPhase" required:"true"`
Meta map[string]string `json:"meta" required:"true"`
}
// PostableJobs is the request body for the v2 jobs list API.
type PostableJobs struct {
Start int64 `json:"start" required:"true"`
End int64 `json:"end" required:"true"`
Filter *qbtypes.Filter `json:"filter"`
GroupBy []qbtypes.GroupByKey `json:"groupBy"`
OrderBy *qbtypes.OrderBy `json:"orderBy"`
Offset int `json:"offset"`
Limit int `json:"limit" required:"true"`
}
// Validate ensures PostableJobs contains acceptable values.
func (req *PostableJobs) Validate() error {
if req == nil {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "request is nil")
}
if req.Start <= 0 {
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"invalid start time %d: start must be greater than 0",
req.Start,
)
}
if req.End <= 0 {
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"invalid end time %d: end must be greater than 0",
req.End,
)
}
if req.Start >= req.End {
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"invalid time range: start (%d) must be less than end (%d)",
req.Start,
req.End,
)
}
if req.Limit < 1 || req.Limit > 5000 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "limit must be between 1 and 5000")
}
if req.Offset < 0 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "offset cannot be negative")
}
if req.OrderBy != nil {
if !slices.Contains(JobsValidOrderByKeys, req.OrderBy.Key.Name) {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by key: %s", req.OrderBy.Key.Name)
}
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
}
}
return nil
}
// UnmarshalJSON validates input immediately after decoding.
func (req *PostableJobs) UnmarshalJSON(data []byte) error {
type raw PostableJobs
var decoded raw
if err := json.Unmarshal(data, &decoded); err != nil {
return err
}
*req = PostableJobs(decoded)
return req.Validate()
}

View File

@@ -0,0 +1,27 @@
package inframonitoringtypes
const (
JobsOrderByCPU = "cpu"
JobsOrderByCPURequest = "cpu_request"
JobsOrderByCPULimit = "cpu_limit"
JobsOrderByMemory = "memory"
JobsOrderByMemoryRequest = "memory_request"
JobsOrderByMemoryLimit = "memory_limit"
JobsOrderByDesiredSuccessfulPods = "desired_successful_pods"
JobsOrderByActivePods = "active_pods"
JobsOrderByFailedPods = "failed_pods"
JobsOrderBySuccessfulPods = "successful_pods"
)
var JobsValidOrderByKeys = []string{
JobsOrderByCPU,
JobsOrderByCPURequest,
JobsOrderByCPULimit,
JobsOrderByMemory,
JobsOrderByMemoryRequest,
JobsOrderByMemoryLimit,
JobsOrderByDesiredSuccessfulPods,
JobsOrderByActivePods,
JobsOrderByFailedPods,
JobsOrderBySuccessfulPods,
}

View File

@@ -0,0 +1,309 @@
package inframonitoringtypes
import (
"testing"
"github.com/SigNoz/signoz/pkg/errors"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/stretchr/testify/require"
)
func TestPostableJobs_Validate(t *testing.T) {
tests := []struct {
name string
req *PostableJobs
wantErr bool
}{
{
name: "valid request",
req: &PostableJobs{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
},
wantErr: false,
},
{
name: "nil request",
req: nil,
wantErr: true,
},
{
name: "start time zero",
req: &PostableJobs{
Start: 0,
End: 2000,
Limit: 100,
Offset: 0,
},
wantErr: true,
},
{
name: "start time negative",
req: &PostableJobs{
Start: -1000,
End: 2000,
Limit: 100,
Offset: 0,
},
wantErr: true,
},
{
name: "end time zero",
req: &PostableJobs{
Start: 1000,
End: 0,
Limit: 100,
Offset: 0,
},
wantErr: true,
},
{
name: "start time greater than end time",
req: &PostableJobs{
Start: 2000,
End: 1000,
Limit: 100,
Offset: 0,
},
wantErr: true,
},
{
name: "start time equal to end time",
req: &PostableJobs{
Start: 1000,
End: 1000,
Limit: 100,
Offset: 0,
},
wantErr: true,
},
{
name: "limit zero",
req: &PostableJobs{
Start: 1000,
End: 2000,
Limit: 0,
Offset: 0,
},
wantErr: true,
},
{
name: "limit negative",
req: &PostableJobs{
Start: 1000,
End: 2000,
Limit: -10,
Offset: 0,
},
wantErr: true,
},
{
name: "limit exceeds max",
req: &PostableJobs{
Start: 1000,
End: 2000,
Limit: 5001,
Offset: 0,
},
wantErr: true,
},
{
name: "offset negative",
req: &PostableJobs{
Start: 1000,
End: 2000,
Limit: 100,
Offset: -5,
},
wantErr: true,
},
{
name: "orderBy nil is valid",
req: &PostableJobs{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
},
wantErr: false,
},
{
name: "orderBy with valid key cpu and direction asc",
req: &PostableJobs{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: JobsOrderByCPU,
},
},
Direction: qbtypes.OrderDirectionAsc,
},
},
wantErr: false,
},
{
name: "orderBy with valid key memory_limit and direction desc",
req: &PostableJobs{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: JobsOrderByMemoryLimit,
},
},
Direction: qbtypes.OrderDirectionDesc,
},
},
wantErr: false,
},
{
name: "orderBy with valid key desired_successful_pods and direction desc",
req: &PostableJobs{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: JobsOrderByDesiredSuccessfulPods,
},
},
Direction: qbtypes.OrderDirectionDesc,
},
},
wantErr: false,
},
{
name: "orderBy with valid key active_pods and direction asc",
req: &PostableJobs{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: JobsOrderByActivePods,
},
},
Direction: qbtypes.OrderDirectionAsc,
},
},
wantErr: false,
},
{
name: "orderBy with valid key failed_pods",
req: &PostableJobs{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: JobsOrderByFailedPods,
},
},
Direction: qbtypes.OrderDirectionDesc,
},
},
wantErr: false,
},
{
name: "orderBy with valid key successful_pods",
req: &PostableJobs{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: JobsOrderBySuccessfulPods,
},
},
Direction: qbtypes.OrderDirectionDesc,
},
},
wantErr: false,
},
{
name: "orderBy with restarts key is rejected",
req: &PostableJobs{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "restarts",
},
},
Direction: qbtypes.OrderDirectionDesc,
},
},
wantErr: true,
},
{
name: "orderBy with invalid key",
req: &PostableJobs{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "unknown",
},
},
Direction: qbtypes.OrderDirectionDesc,
},
},
wantErr: true,
},
{
name: "orderBy with valid key but invalid direction",
req: &PostableJobs{
Start: 1000,
End: 2000,
Limit: 100,
Offset: 0,
OrderBy: &qbtypes.OrderBy{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: JobsOrderByCPU,
},
},
Direction: qbtypes.OrderDirection{String: valuer.NewString("invalid")},
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.req.Validate()
if tt.wantErr {
require.Error(t, err)
require.True(t, errors.Ast(err, errors.TypeInvalidInput), "expected error to be of type InvalidInput")
} else {
require.NoError(t, err)
}
})
}
}

1
tests/e2e/.npmrc Normal file
View File

@@ -0,0 +1 @@
engine-strict=true

View File

@@ -4,6 +4,7 @@
"description": "E2E tests for SigNoz frontend with Playwright",
"main": "index.js",
"scripts": {
"preinstall": "npx only-allow pnpm",
"test": "playwright test",
"test:staging": "SIGNOZ_E2E_BASE_URL=https://app.us.staging.signoz.cloud playwright test",
"test:ui": "playwright test --ui",
@@ -41,6 +42,7 @@
"typescript": "^5.0.0"
},
"engines": {
"node": ">=18.0.0"
"node": ">=18.0.0",
"pnpm": ">=10.0.0 <11.0.0"
}
}

View File

@@ -0,0 +1,326 @@
"""Tests for resource-level FGA on role endpoints.
Validates that a custom role with specific role permissions gets exactly
the access it was granted — read/list allowed, create/update/delete forbidden
until explicitly granted, and revocation removes access.
"""
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 (
ROLES_BASE,
create_custom_role,
delete_custom_role,
find_role_by_name,
object_group,
patch_role_objects,
)
ROLE_FGA_CUSTOM_ROLE_NAME = "role-fga-readonly"
ROLE_FGA_CUSTOM_USER_EMAIL = "customrole+rolefga@integration.test"
ROLE_FGA_CUSTOM_USER_PASSWORD = "password123Z$"
# ---------------------------------------------------------------------------
# 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 with read/list on roles
# ---------------------------------------------------------------------------
def test_create_custom_role_for_role_fga(
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, ROLE_FGA_CUSTOM_ROLE_NAME)
# Grant read on role instances.
patch_role_objects(
signoz,
admin_token,
role_id,
"read",
additions=[
object_group("role", "role", ["*"]),
],
)
# Grant list on role collection.
patch_role_objects(
signoz,
admin_token,
role_id,
"list",
additions=[
object_group("role", "role", ["*"]),
],
)
# Create the custom-role user: invite as VIEWER, activate, change role.
user_id = create_active_user(
signoz,
admin_token,
email=ROLE_FGA_CUSTOM_USER_EMAIL,
role="VIEWER",
password=ROLE_FGA_CUSTOM_USER_PASSWORD,
name="role-fga-test-user",
)
change_user_role(signoz, admin_token, user_id, "signoz-viewer", ROLE_FGA_CUSTOM_ROLE_NAME)
# ---------------------------------------------------------------------------
# 3. Read-only access: allowed operations
# ---------------------------------------------------------------------------
def test_role_readonly_allowed_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(ROLE_FGA_CUSTOM_USER_EMAIL, ROLE_FGA_CUSTOM_USER_PASSWORD)
target_role_id = find_role_by_name(signoz, admin_token, "signoz-viewer")
# List roles.
resp = requests.get(
signoz.self.host_configs["8080"].get(ROLES_BASE),
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.OK, f"list roles: {resp.text}"
# Get role.
resp = requests.get(
signoz.self.host_configs["8080"].get(f"{ROLES_BASE}/{target_role_id}"),
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.OK, f"get role: {resp.text}"
# Get objects for role.
resp = requests.get(
signoz.self.host_configs["8080"].get(f"{ROLES_BASE}/{target_role_id}/relations/read/objects"),
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.OK, f"get role objects: {resp.text}"
# ---------------------------------------------------------------------------
# 4. Read-only access: forbidden operations
# ---------------------------------------------------------------------------
def test_role_readonly_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(ROLE_FGA_CUSTOM_USER_EMAIL, ROLE_FGA_CUSTOM_USER_PASSWORD)
target_role_id = find_role_by_name(signoz, admin_token, "signoz-viewer")
# Create role — forbidden.
resp = requests.post(
signoz.self.host_configs["8080"].get(ROLES_BASE),
json={"name": "role-fga-should-fail"},
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.FORBIDDEN, f"create role: expected 403, got {resp.status_code}: {resp.text}"
# Patch role — forbidden.
resp = requests.patch(
signoz.self.host_configs["8080"].get(f"{ROLES_BASE}/{target_role_id}"),
json={"description": "role-fga-renamed"},
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.FORBIDDEN, f"patch role: expected 403, got {resp.status_code}: {resp.text}"
# Patch objects — forbidden.
resp = requests.patch(
signoz.self.host_configs["8080"].get(f"{ROLES_BASE}/{target_role_id}/relations/read/objects"),
json={"additions": [object_group("metaresource", "dashboard", ["*"])]},
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.FORBIDDEN, f"patch objects: expected 403, got {resp.status_code}: {resp.text}"
# Delete role — forbidden (cannot delete managed role, but auth check comes first).
# Use the custom role itself as target (non-managed, but user lacks delete permission).
custom_role_id = find_role_by_name(signoz, admin_token, ROLE_FGA_CUSTOM_ROLE_NAME)
resp = requests.delete(
signoz.self.host_configs["8080"].get(f"{ROLES_BASE}/{custom_role_id}"),
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.FORBIDDEN, f"delete role: expected 403, got {resp.status_code}: {resp.text}"
# ---------------------------------------------------------------------------
# 5. Grant write permissions, verify access opens up
# ---------------------------------------------------------------------------
def test_role_grant_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, ROLE_FGA_CUSTOM_ROLE_NAME)
# Grant create, update, delete on roles.
for verb in ("create", "update", "delete"):
patch_role_objects(
signoz,
admin_token,
role_id,
verb,
additions=[object_group("role", "role", ["*"])],
)
custom_token = get_token(ROLE_FGA_CUSTOM_USER_EMAIL, ROLE_FGA_CUSTOM_USER_PASSWORD)
# Create role — now allowed.
resp = requests.post(
signoz.self.host_configs["8080"].get(ROLES_BASE),
json={"name": "role-fga-write-test"},
headers={"Authorization": f"Bearer {custom_token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.CREATED, f"create role: {resp.text}"
new_role_id = resp.json()["data"]["id"]
# Patch role — now allowed.
resp = requests.patch(
signoz.self.host_configs["8080"].get(f"{ROLES_BASE}/{new_role_id}"),
json={"description": "role-fga-write-renamed"},
headers={"Authorization": f"Bearer {custom_token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.NO_CONTENT, f"patch role: {resp.text}"
# Delete role — now allowed.
resp = requests.delete(
signoz.self.host_configs["8080"].get(f"{ROLES_BASE}/{new_role_id}"),
headers={"Authorization": f"Bearer {custom_token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.NO_CONTENT, f"delete role: {resp.text}"
# ---------------------------------------------------------------------------
# 6. Revoke read/list → verify access lost
# ---------------------------------------------------------------------------
def test_role_revoke_read_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, ROLE_FGA_CUSTOM_ROLE_NAME)
target_role_id = find_role_by_name(signoz, admin_token, "signoz-viewer")
# Revoke read.
patch_role_objects(
signoz,
admin_token,
role_id,
"read",
deletions=[object_group("role", "role", ["*"])],
)
# Revoke list.
patch_role_objects(
signoz,
admin_token,
role_id,
"list",
deletions=[object_group("role", "role", ["*"])],
)
custom_token = get_token(ROLE_FGA_CUSTOM_USER_EMAIL, ROLE_FGA_CUSTOM_USER_PASSWORD)
# List roles — forbidden.
resp = requests.get(
signoz.self.host_configs["8080"].get(ROLES_BASE),
headers={"Authorization": f"Bearer {custom_token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.FORBIDDEN, f"list roles after revoke: expected 403, got {resp.status_code}: {resp.text}"
# Get role — forbidden.
resp = requests.get(
signoz.self.host_configs["8080"].get(f"{ROLES_BASE}/{target_role_id}"),
headers={"Authorization": f"Bearer {custom_token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.FORBIDDEN, f"get role after revoke: expected 403, got {resp.status_code}: {resp.text}"
# ---------------------------------------------------------------------------
# 7. Clean up: delete custom role
# ---------------------------------------------------------------------------
def test_role_fga_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, ROLE_FGA_CUSTOM_ROLE_NAME)
user = find_user_by_email(signoz, admin_token, ROLE_FGA_CUSTOM_USER_EMAIL)
# Remove the custom role from the user first.
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"] == ROLE_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)

View File

@@ -1,8 +1,11 @@
"""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.
the access it was granted, and that:
- SA role assignment requires BOTH serviceaccount:attach AND role:attach.
- SA role removal requires BOTH serviceaccount:detach AND role:detach.
- Factor API key creation requires factor-api-key:create AND serviceaccount:attach.
- Factor API key revocation requires factor-api-key:delete AND serviceaccount:detach.
"""
from collections.abc import Callable
@@ -80,14 +83,36 @@ def test_create_custom_role_readonly_sa(
],
)
# Grant list on serviceaccount collection.
# Grant list on serviceaccount (now on the serviceaccount type directly).
patch_role_objects(
signoz,
admin_token,
role_id,
"list",
additions=[
object_group("metaresources", "serviceaccount", ["*"]),
object_group("serviceaccount", "serviceaccount", ["*"]),
],
)
# Grant read on factor-api-key (needed for listing keys).
patch_role_objects(
signoz,
admin_token,
role_id,
"read",
additions=[
object_group("metaresource", "factor-api-key", ["*"]),
],
)
# Grant list on factor-api-key.
patch_role_objects(
signoz,
admin_token,
role_id,
"list",
additions=[
object_group("metaresource", "factor-api-key", ["*"]),
],
)
@@ -203,7 +228,7 @@ def test_readonly_role_forbidden_operations(
)
assert resp.status_code == HTTPStatus.FORBIDDEN, f"delete SA: expected 403, got {resp.status_code}: {resp.text}"
# Assign role to SA — forbidden.
# Assign role to SA — forbidden (needs attach on both SA and role).
resp = requests.post(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{sa_id}/roles"),
json={"id": viewer_role_id},
@@ -212,7 +237,7 @@ def test_readonly_role_forbidden_operations(
)
assert resp.status_code == HTTPStatus.FORBIDDEN, f"assign SA role: expected 403, got {resp.status_code}: {resp.text}"
# Remove role from SA — forbidden.
# Remove role from SA — forbidden (needs detach on both SA and role).
resp = requests.delete(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{sa_id}/roles/{viewer_role_id}"),
headers={"Authorization": f"Bearer {token}"},
@@ -220,7 +245,7 @@ def test_readonly_role_forbidden_operations(
)
assert resp.status_code == HTTPStatus.FORBIDDEN, f"remove SA role: expected 403, got {resp.status_code}: {resp.text}"
# Create key — forbidden (needs update).
# Create key — forbidden (needs factor-api-key:create + serviceaccount:attach).
resp = requests.post(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{sa_id}/keys"),
json={"name": "fga-key-fail", "expiresAt": 0},
@@ -229,7 +254,7 @@ def test_readonly_role_forbidden_operations(
)
assert resp.status_code == HTTPStatus.FORBIDDEN, f"create key: expected 403, got {resp.status_code}: {resp.text}"
# Revoke key — forbidden (needs update).
# Revoke key — forbidden (needs factor-api-key:delete + serviceaccount:detach).
resp = requests.delete(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{sa_id}/keys/{key_id}"),
headers={"Authorization": f"Bearer {token}"},
@@ -253,14 +278,14 @@ def test_patch_role_add_write_permissions(
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.
# Grant create on serviceaccount (now on serviceaccount type directly).
patch_role_objects(
signoz,
admin_token,
role_id,
"create",
additions=[
object_group("metaresources", "serviceaccount", ["*"]),
object_group("serviceaccount", "serviceaccount", ["*"]),
],
)
@@ -286,6 +311,44 @@ def test_patch_role_add_write_permissions(
],
)
# Grant factor-api-key create/delete + serviceaccount attach/detach for key operations.
patch_role_objects(
signoz,
admin_token,
role_id,
"create",
additions=[
object_group("metaresource", "factor-api-key", ["*"]),
],
)
patch_role_objects(
signoz,
admin_token,
role_id,
"delete",
additions=[
object_group("metaresource", "factor-api-key", ["*"]),
],
)
patch_role_objects(
signoz,
admin_token,
role_id,
"attach",
additions=[
object_group("serviceaccount", "serviceaccount", ["*"]),
],
)
patch_role_objects(
signoz,
admin_token,
role_id,
"detach",
additions=[
object_group("serviceaccount", "serviceaccount", ["*"]),
],
)
custom_token = get_token(SA_FGA_CUSTOM_USER_EMAIL, SA_FGA_CUSTOM_USER_PASSWORD)
# Create SA — now allowed.
@@ -307,7 +370,7 @@ def test_patch_role_add_write_permissions(
)
assert resp.status_code == HTTPStatus.NO_CONTENT, f"update SA: {resp.text}"
# Create key — now allowed (update permission covers key create).
# Create key — now allowed (factor-api-key:create + serviceaccount:attach).
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},
@@ -317,7 +380,7 @@ def test_patch_role_add_write_permissions(
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).
# Revoke key — now allowed (factor-api-key:delete + serviceaccount:detach).
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}"},
@@ -333,7 +396,7 @@ def test_patch_role_add_write_permissions(
)
assert resp.status_code == HTTPStatus.NO_CONTENT, f"delete SA: {resp.text}"
# Role assignment still forbidden (no attach).
# Role assignment still forbidden (has attach on SA but not on role).
resp = requests.post(
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{sa_id}/roles"),
json={"id": viewer_role_id},
@@ -342,6 +405,7 @@ def test_patch_role_add_write_permissions(
)
assert resp.status_code == HTTPStatus.FORBIDDEN, f"assign SA role: expected 403, got {resp.status_code}: {resp.text}"
# Role removal still forbidden (has detach on SA but not on role).
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}"},
@@ -351,7 +415,7 @@ def test_patch_role_add_write_permissions(
# ---------------------------------------------------------------------------
# 6. Dual-attach: SA attach only (no role attach) → forbidden
# 6. Dual-attach: SA attach only (no role attach) → assign forbidden
# ---------------------------------------------------------------------------
@@ -361,21 +425,10 @@ def test_attach_with_only_sa_attach_forbidden(
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", ["*"]),
],
)
# SA attach already granted from previous test; role attach not yet granted.
custom_token = get_token(SA_FGA_CUSTOM_USER_EMAIL, SA_FGA_CUSTOM_USER_PASSWORD)
# Assign role — forbidden (has SA attach, missing role attach).
@@ -387,17 +440,36 @@ def test_attach_with_only_sa_attach_forbidden(
)
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).
# ---------------------------------------------------------------------------
# 7. Dual-detach: SA detach only (no role detach) → remove forbidden
# ---------------------------------------------------------------------------
def test_detach_with_only_sa_detach_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)
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")
# SA detach already granted from test_patch_role_add_write_permissions;
# role detach not yet granted.
custom_token = get_token(SA_FGA_CUSTOM_USER_EMAIL, SA_FGA_CUSTOM_USER_PASSWORD)
# Remove role — forbidden (has SA detach, missing role detach).
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}"
assert resp.status_code == HTTPStatus.FORBIDDEN, f"remove with only SA detach: expected 403, got {resp.status_code}: {resp.text}"
# ---------------------------------------------------------------------------
# 7. Dual-attach: role attach only (no SA attach) → forbidden
# 8. Dual-attach: role attach only (no SA attach) → assign forbidden
# ---------------------------------------------------------------------------
@@ -432,21 +504,49 @@ def test_attach_with_only_role_attach_forbidden(
)
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).
# ---------------------------------------------------------------------------
# 9. Dual-detach: role detach only (no SA detach) → remove forbidden
# ---------------------------------------------------------------------------
def test_detach_with_only_role_detach_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 detach, grant role detach.
patch_role_objects(
signoz,
admin_token,
role_id,
"detach",
additions=[object_group("role", "role", ["*"])],
deletions=[object_group("serviceaccount", "serviceaccount", ["*"])],
)
custom_token = get_token(SA_FGA_CUSTOM_USER_EMAIL, SA_FGA_CUSTOM_USER_PASSWORD)
# Remove role — forbidden (SA detach check 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}"
assert resp.status_code == HTTPStatus.FORBIDDEN, f"remove with only role detach: expected 403, got {resp.status_code}: {resp.text}"
# ---------------------------------------------------------------------------
# 8. Dual-attach: both SA + role attach → succeeds
# 10. Both attach + detach → assign and remove succeed
# ---------------------------------------------------------------------------
def test_attach_with_both_permissions_succeeds(
def test_attach_detach_with_both_permissions_succeeds(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
@@ -455,7 +555,7 @@ def test_attach_with_both_permissions_succeeds(
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).
# Add back SA attach and SA detach (role attach/detach already present from previous tests).
patch_role_objects(
signoz,
admin_token,
@@ -465,6 +565,15 @@ def test_attach_with_both_permissions_succeeds(
object_group("serviceaccount", "serviceaccount", ["*"]),
],
)
patch_role_objects(
signoz,
admin_token,
role_id,
"detach",
additions=[
object_group("serviceaccount", "serviceaccount", ["*"]),
],
)
custom_token = get_token(SA_FGA_CUSTOM_USER_EMAIL, SA_FGA_CUSTOM_USER_PASSWORD)
@@ -480,17 +589,17 @@ def test_attach_with_both_permissions_succeeds(
)
assert resp.status_code == HTTPStatus.NO_CONTENT, f"assign with both attach: {resp.text}"
# Remove the editor role — should succeed (CheckAll: both groups pass).
# Remove the editor role — should succeed (both SA detach + role detach).
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}"
assert resp.status_code == HTTPStatus.NO_CONTENT, f"remove with both detach: {resp.text}"
# ---------------------------------------------------------------------------
# 9. Revoke read/list → verify access lost
# 11. Revoke read/list → verify access lost
# ---------------------------------------------------------------------------
@@ -514,14 +623,14 @@ def test_remove_read_permissions_revokes_access(
],
)
# Revoke list.
# Revoke list (now on serviceaccount type directly).
patch_role_objects(
signoz,
admin_token,
role_id,
"list",
deletions=[
object_group("metaresources", "serviceaccount", ["*"]),
object_group("serviceaccount", "serviceaccount", ["*"]),
],
)
@@ -545,7 +654,7 @@ def test_remove_read_permissions_revokes_access(
# ---------------------------------------------------------------------------
# 10. Clean up: delete custom role
# 12. Clean up: delete custom role
# ---------------------------------------------------------------------------