From 3e2daaea0e39a9f0df4b1572f50f7b36c5a1e78a Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Thu, 27 Aug 2026 12:34:32 +0000 Subject: [PATCH 1/3] test(service-account): fix flaky ServiceAccountDrawer authz test (#12708) #### Description - The `shows PermissionDeniedCallout in Keys tab when list-keys permission is denied` test intermittently failed in CI: the `fireEvent.click` on the Keys tab races with the nuqs testing adapter, which can abort the queued `tab=keys` URL update mid-flight, leaving the drawer stuck on the Overview tab. - Since the tab is URL state, the test now lands directly on the Keys tab via initial search params (`{ account: 'sa-1', tab: 'keys' }`), avoiding the userEvent/click interaction altogether. The click-to-Keys flow remains covered in `ServiceAccountDrawer.test.tsx`. #### Issues closed by this PR closes SigNoz/platform-pod#3053 --- .../__tests__/ServiceAccountDrawer.authz.test.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/ServiceAccountDrawer/__tests__/ServiceAccountDrawer.authz.test.tsx b/frontend/src/components/ServiceAccountDrawer/__tests__/ServiceAccountDrawer.authz.test.tsx index 8ace6d1c10..f8fef2dc1a 100644 --- a/frontend/src/components/ServiceAccountDrawer/__tests__/ServiceAccountDrawer.authz.test.tsx +++ b/frontend/src/components/ServiceAccountDrawer/__tests__/ServiceAccountDrawer.authz.test.tsx @@ -4,7 +4,7 @@ import { } from 'mocks-server/__mockdata__/roles'; import { rest, server } from 'mocks-server/server'; import { NuqsTestingAdapter } from 'nuqs/adapters/testing'; -import { fireEvent, render, screen, waitFor } from 'tests/test-utils'; +import { render, screen, waitFor } from 'tests/test-utils'; import { setupAuthzAdmin, setupAuthzDeny, @@ -110,10 +110,7 @@ describe('ServiceAccountDrawer — permissions', () => { it('shows PermissionDeniedCallout in Keys tab when list-keys permission is denied', async () => { server.use(setupAuthzDeny(APIKeyListPermission)); - renderDrawer(); - await screen.findByDisplayValue('CI Bot'); - - fireEvent.click(screen.getByRole('radio', { name: /keys/i })); + renderDrawer({ account: 'sa-1', tab: 'keys' }); await waitFor(() => { expect(screen.getByText(/list:factor-api-key/)).toBeInTheDocument(); From 1086914a3b6d4ad81780f8073f71bf7a3190d0e9 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Thu, 27 Aug 2026 12:43:34 +0000 Subject: [PATCH 2/3] fix(query-builder): prevent qb crash on partial queries (#12705) #### Description - traces explorer was crashing on some shared/legacy links...the query builder reads `builder.queryFormulas` and `queryTraceOperator` directly and these can be missing from a partial compositeQuery (old saved view, shared link, older release) - fixed it where the query enters state from the url param, defaulting the arrays to `[]`...this way every consumer gets `[]` and not undefined, not just the component that crashed - also default them in `prepareQueryBuilderData` so any query loaded into the provider is normalized - optional chained the reads in `QueryBuilderV2` as a render level safety net - added a test that a partial query (missing these arrays) renders without crashing #### Issues closed by this PR Closes https://github.com/SigNoz/engineering-pod/issues/5949 Closes https://github.com/SigNoz/engineering-pod/issues/4320 Closes https://github.com/SigNoz/engineering-pod/issues/3335 Closes https://github.com/SigNoz/engineering-pod/issues/4314 #### Screenshots / Screen Recordings [Test URL Link](https://app.us.staging.signoz.cloud/traces-explorer?relativeTime=1h&compositeQuery=%257B%2522queryType%2522%253A%2522builder%2522%252C%2522builder%2522%253A%257B%2522queryData%2522%253A%255B%257B%2522dataSource%2522%253A%2522logs%2522%252C%2522queryName%2522%253A%2522A%2522%252C%2522aggregateOperator%2522%253A%2522noop%2522%252C%2522aggregateAttribute%2522%253A%257B%2522id%2522%253A%2522----%2522%252C%2522dataType%2522%253A%2522%2522%252C%2522key%2522%253A%2522%2522%252C%2522type%2522%253A%2522%2522%257D%252C%2522timeAggregation%2522%253A%2522rate%2522%252C%2522spaceAggregation%2522%253A%2522sum%2522%252C%2522filter%2522%253A%257B%2522expression%2522%253A%2522service.name%2520%253D%2520%27midtier-api-production%27%2520AND%2520severity_text%2520%253D%2520%27ERROR%27%2522%257D%252C%2522aggregations%2522%253A%255B%257B%2522expression%2522%253A%2522count%28%29%2522%257D%255D%252C%2522functions%2522%253A%255B%255D%252C%2522filters%2522%253A%257B%2522items%2522%253A%255B%255D%252C%2522op%2522%253A%2522AND%2522%257D%252C%2522expression%2522%253A%2522A%2522%252C%2522disabled%2522%253Afalse%252C%2522stepInterval%2522%253Anull%252C%2522having%2522%253A%257B%2522expression%2522%253A%2522%2522%257D%252C%2522limit%2522%253Anull%252C%2522orderBy%2522%253A%255B%255D%252C%2522groupBy%2522%253A%255B%255D%252C%2522legend%2522%253A%2522%2522%252C%2522reduceTo%2522%253A%2522avg%2522%252C%2522source%2522%253A%2522%2522%257D%255D%257D%252C%2522promql%2522%253A%255B%257B%2522name%2522%253A%2522A%2522%252C%2522query%2522%253A%2522%2522%252C%2522legend%2522%253A%2522%2522%252C%2522disabled%2522%253Afalse%257D%255D%252C%2522clickhouse_sql%2522%253A%255B%257B%2522name%2522%253A%2522A%2522%252C%2522legend%2522%253A%2522%2522%252C%2522disabled%2522%253Afalse%252C%2522query%2522%253A%2522%2522%257D%255D%252C%2522id%2522%253A%25225d398425-eb7e-41e4-990a-de9375baf74a%2522%252C%2522unit%2522%253A%2522%2522%257D&options=%7B%22selectColumns%22%3A%5B%7B%22name%22%3A%22timestamp%22%2C%22signal%22%3A%22logs%22%2C%22fieldContext%22%3A%22log%22%2C%22fieldDataType%22%3A%22%22%2C%22isIndexed%22%3Afalse%7D%2C%7B%22name%22%3A%22body%22%2C%22signal%22%3A%22logs%22%2C%22fieldContext%22%3A%22log%22%2C%22fieldDataType%22%3A%22%22%2C%22isIndexed%22%3Afalse%7D%5D%2C%22maxLines%22%3A1%2C%22format%22%3A%22raw%22%2C%22fontSize%22%3A%22small%22%7D) Before: image After: image #### Additional Information Pager: https://signoz-1.pagerduty.com/incidents/Q3YCF0KG3OYSYX Sentry:https://signoz-io.sentry.io/issues/7498496470/?referrer=pagerduty_integration¬ification_uuid=5cd4dc50-634b-416f-ab84-9540706c43aa --- .../QueryBuilderV2/QueryBuilderV2.tsx | 47 +++++++++--------- .../QueryV2/__tests__/QueryV2.test.tsx | 26 ++++++++++ .../src/container/FormAlertRules/utils.ts | 25 ++++++---- .../queryBuilder/useGetCompositeQueryParam.ts | 8 +++ frontend/src/providers/QueryBuilder.tsx | 49 ++++++++++--------- 5 files changed, 101 insertions(+), 54 deletions(-) diff --git a/frontend/src/components/QueryBuilderV2/QueryBuilderV2.tsx b/frontend/src/components/QueryBuilderV2/QueryBuilderV2.tsx index 78a2071e38..2734a85856 100644 --- a/frontend/src/components/QueryBuilderV2/QueryBuilderV2.tsx +++ b/frontend/src/components/QueryBuilderV2/QueryBuilderV2.tsx @@ -241,28 +241,29 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({ )) )} - {!showOnlyWhereClause && currentQuery.builder.queryFormulas.length > 0 && ( -
- {currentQuery.builder.queryFormulas.map((formula, index) => { - const query = - currentQuery.builder.queryData[index] || - currentQuery.builder.queryData[0]; - - return ( -
- -
- ); - })} -
- )} + {!showOnlyWhereClause && + currentQuery.builder.queryFormulas?.length > 0 && ( +
+ {currentQuery.builder.queryFormulas.map((formula, index) => { + const query = + currentQuery.builder.queryData[index] || + currentQuery.builder.queryData[0]; + + return ( +
+ +
+ ); + })} +
+ )} {shouldShowFooter && ( ))} - {currentQuery.builder.queryFormulas.map((formula) => ( + {currentQuery.builder.queryFormulas?.map((formula) => (
{formula.queryName}
diff --git a/frontend/src/components/QueryBuilderV2/QueryV2/__tests__/QueryV2.test.tsx b/frontend/src/components/QueryBuilderV2/QueryV2/__tests__/QueryV2.test.tsx index 09f95b27cc..d5c39e5041 100644 --- a/frontend/src/components/QueryBuilderV2/QueryV2/__tests__/QueryV2.test.tsx +++ b/frontend/src/components/QueryBuilderV2/QueryV2/__tests__/QueryV2.test.tsx @@ -212,6 +212,32 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => { expect(handleRunQueryMock).toHaveBeenCalled(); }); + it('does not crash when builder.queryFormulas/queryTraceOperator are missing (partial/legacy query)', () => { + const currentQueryBase = baseQBContext.currentQuery as Query; + + mockedUseQueryBuilder.mockReturnValue({ + ...baseQBContext, + currentQuery: { + ...currentQueryBase, + builder: { + queryData: currentQueryBase.builder.queryData, + queryFormulas: undefined as unknown as [], + queryTraceOperator: undefined as unknown as [], + }, + }, + }); + + expect(() => + render(), + ).not.toThrow(); + + // query list still renders from queryData, formulas block is skipped + expect(document.querySelector('.query-names-section')).toBeInTheDocument(); + expect( + document.querySelector('.qb-formulas-container'), + ).not.toBeInTheDocument(); + }); + it('fx button is disabled when functions already exist', () => { const currentQueryBase = baseQBContext.currentQuery as Query; const supersetQueryBase = baseQBContext.supersetQuery as Query; diff --git a/frontend/src/container/FormAlertRules/utils.ts b/frontend/src/container/FormAlertRules/utils.ts index 967b00a921..6deb1ad0f1 100644 --- a/frontend/src/container/FormAlertRules/utils.ts +++ b/frontend/src/container/FormAlertRules/utils.ts @@ -53,17 +53,24 @@ export const getUpdatedStepInterval = (evalWindow?: string): number => { }; export const getSelectedQueryOptions = ( - queries: Array< - | IBuilderQuery - | IBuilderTraceOperator - | IBuilderFormula - | IClickHouseQuery - | IPromQLQuery - >, -): SelectProps['options'] => - queries + queries: + | Array< + | IBuilderQuery + | IBuilderTraceOperator + | IBuilderFormula + | IClickHouseQuery + | IPromQLQuery + > + | undefined + | null, +): SelectProps['options'] => { + if (!queries) { + return []; + } + return queries .filter((query) => !query.disabled) .map((query) => ({ label: 'queryName' in query ? query.queryName : query.name, value: 'queryName' in query ? query.queryName : query.name, })); +}; diff --git a/frontend/src/hooks/queryBuilder/useGetCompositeQueryParam.ts b/frontend/src/hooks/queryBuilder/useGetCompositeQueryParam.ts index 0989c2e423..6d27779828 100644 --- a/frontend/src/hooks/queryBuilder/useGetCompositeQueryParam.ts +++ b/frontend/src/hooks/queryBuilder/useGetCompositeQueryParam.ts @@ -27,6 +27,14 @@ export const useGetCompositeQueryParam = (): Query | null => { decodeURIComponent(compositeQuery.replace(/\+/g, ' ')), ); + // Add default values for optional fields if empty + if (parsedCompositeQuery?.builder) { + parsedCompositeQuery.builder.queryFormulas = + parsedCompositeQuery.builder.queryFormulas ?? []; + parsedCompositeQuery.builder.queryTraceOperator = + parsedCompositeQuery.builder.queryTraceOperator ?? []; + } + // Convert old format to new format for each query in builder.queryData if (parsedCompositeQuery?.builder?.queryData) { parsedCompositeQuery.builder.queryData = diff --git a/frontend/src/providers/QueryBuilder.tsx b/frontend/src/providers/QueryBuilder.tsx index 4ca154173c..fbadfdaa2c 100644 --- a/frontend/src/providers/QueryBuilder.tsx +++ b/frontend/src/providers/QueryBuilder.tsx @@ -163,20 +163,23 @@ export function QueryBuilderProvider({ const prepareQueryBuilderData = useCallback( (query: Query): Query => { const builder: QueryBuilderData = { - queryData: query.builder.queryData?.map((item) => ({ - ...initialQueryBuilderFormValuesMap[ - initialDataSource || DataSource.METRICS - ], - ...item, - })), - queryFormulas: query.builder.queryFormulas?.map((item) => ({ - ...initialFormulaBuilderFormValues, - ...item, - })), - queryTraceOperator: query.builder.queryTraceOperator?.map((item) => ({ - ...initialQueryBuilderFormTraceOperatorValues, - ...item, - })), + queryData: + query.builder.queryData?.map((item) => ({ + ...initialQueryBuilderFormValuesMap[ + initialDataSource || DataSource.METRICS + ], + ...item, + })) ?? [], + queryFormulas: + query.builder.queryFormulas?.map((item) => ({ + ...initialFormulaBuilderFormValues, + ...item, + })) ?? [], + queryTraceOperator: + query.builder.queryTraceOperator?.map((item) => ({ + ...initialQueryBuilderFormTraceOperatorValues, + ...item, + })) ?? [], }; const setupedQueryData = builder.queryData.map((item) => { @@ -209,15 +212,17 @@ export function QueryBuilderProvider({ return currentElement; }); - const promql: IPromQLQuery[] = query.promql.map((item) => ({ - ...initialQueryPromQLData, - ...item, - })); + const promql: IPromQLQuery[] = + query.promql?.map((item) => ({ + ...initialQueryPromQLData, + ...item, + })) ?? []; - const clickHouse: IClickHouseQuery[] = query.clickhouse_sql.map((item) => ({ - ...initialClickHouseData, - ...item, - })); + const clickHouse: IClickHouseQuery[] = + query.clickhouse_sql?.map((item) => ({ + ...initialClickHouseData, + ...item, + })) ?? []; const newQueryState: QueryState = { clickhouse_sql: clickHouse, From 5069bf80b08f1f00d7e014eccc09902f9871004f Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Thu, 27 Aug 2026 13:22:27 +0000 Subject: [PATCH 3/3] feat(authz): enable FGA for deployment hosts (#12699) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### Description - Deployment host routes (`GET`/`PUT /api/v2/zeus/hosts`) now use `CheckResources` + `ResourceDef` instead of the coarse `ViewAccess`/`AdminAccess` gates — per-resource FGA checks on enterprise, role gate on community. - New `deployment-host` metaresource kind with `list`/`update` verbs — the GET returns the deployment's host collection and the PUT upserts the single editable host. Admins get `list`+`update`, editors and viewers get `list`, preserving current behavior. - Migration `118_add_deployment_host_tuples` backfills the tuples for existing organizations and re-syncs the stored managed-role transaction groups; new organizations get both from the registry at bootstrap. - Regenerated OpenAPI spec and transaction-groups schema: the operations advertise `deployment-host:list`/`deployment-host:update` scopes instead of `VIEWER`/`ADMIN`. - Added `deploymenthost/01_authz.py` covering managed-role gating, custom-role `list`/`update` grants, and rejection of verbs the resource does not support. #### Issues closed by this PR Closes SigNoz/platform-pod#2652 --- docs/api/openapi.yml | 9 +- .../api/generated/services/sigNoz.schemas.ts | 1 + .../generated/transactionGroups.schema.json | 1 + pkg/apiserver/signozapiserver/zeus.go | 25 ++- pkg/signoz/provider.go | 1 + .../118_add_deployment_host_tuples.go | 161 ++++++++++++++++++ pkg/types/coretypes/kind.go | 1 + pkg/types/coretypes/registry_kind.go | 2 + pkg/types/coretypes/registry_managed_role.go | 7 + pkg/types/coretypes/registry_resource.go | 2 + .../role/managed_role_transactions.json | 22 +++ 11 files changed, 222 insertions(+), 10 deletions(-) create mode 100644 pkg/sqlmigration/118_add_deployment_host_tuples.go diff --git a/docs/api/openapi.yml b/docs/api/openapi.yml index b4615b1326..79dd3f74a3 100644 --- a/docs/api/openapi.yml +++ b/docs/api/openapi.yml @@ -2559,6 +2559,7 @@ components: - factor-api-key - license - subscription + - deployment-host - logs - traces - metrics @@ -23942,9 +23943,9 @@ paths: description: Internal Server Error security: - api_key: - - VIEWER + - deployment-host:list - tokenizer: - - VIEWER + - deployment-host:list summary: Get host info from Zeus. tags: - zeus @@ -23998,9 +23999,9 @@ paths: description: Internal Server Error security: - api_key: - - ADMIN + - deployment-host:update - tokenizer: - - ADMIN + - deployment-host:update summary: Put host in Zeus for a deployment. tags: - zeus diff --git a/frontend/src/api/generated/services/sigNoz.schemas.ts b/frontend/src/api/generated/services/sigNoz.schemas.ts index 2a80cf0f3f..19404b3b03 100644 --- a/frontend/src/api/generated/services/sigNoz.schemas.ts +++ b/frontend/src/api/generated/services/sigNoz.schemas.ts @@ -2175,6 +2175,7 @@ export enum CoretypesKindDTO { 'factor-api-key' = 'factor-api-key', license = 'license', subscription = 'subscription', + 'deployment-host' = 'deployment-host', logs = 'logs', traces = 'traces', metrics = 'metrics', diff --git a/frontend/src/schemas/generated/transactionGroups.schema.json b/frontend/src/schemas/generated/transactionGroups.schema.json index bb7bd0fca6..7b2333aa5c 100644 --- a/frontend/src/schemas/generated/transactionGroups.schema.json +++ b/frontend/src/schemas/generated/transactionGroups.schema.json @@ -66,6 +66,7 @@ "factor-api-key", "license", "subscription", + "deployment-host", "logs", "traces", "metrics", diff --git a/pkg/apiserver/signozapiserver/zeus.go b/pkg/apiserver/signozapiserver/zeus.go index 93299825f3..3a5e475693 100644 --- a/pkg/apiserver/signozapiserver/zeus.go +++ b/pkg/apiserver/signozapiserver/zeus.go @@ -5,6 +5,8 @@ import ( "github.com/SigNoz/signoz/pkg/http/handler" "github.com/SigNoz/signoz/pkg/types" + "github.com/SigNoz/signoz/pkg/types/authtypes" + "github.com/SigNoz/signoz/pkg/types/coretypes" "github.com/SigNoz/signoz/pkg/types/zeustypes" "github.com/gorilla/mux" ) @@ -27,7 +29,7 @@ func (provider *provider) addZeusRoutes(router *mux.Router) error { return err } - if err := router.Handle("/api/v2/zeus/hosts", handler.New(provider.authzMiddleware.ViewAccess(provider.zeusHandler.GetHosts), handler.OpenAPIDef{ + if err := router.Handle("/api/v2/zeus/hosts", handler.New(provider.authzMiddleware.CheckResources(provider.zeusHandler.GetHosts, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName), handler.OpenAPIDef{ ID: "GetHosts", Tags: []string{"zeus"}, Summary: "Get host info from Zeus.", @@ -39,12 +41,17 @@ func (provider *provider) addZeusRoutes(router *mux.Router) error { SuccessStatusCode: http.StatusOK, ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound}, Deprecated: false, - SecuritySchemes: newSecuritySchemes(types.RoleViewer), - })).Methods(http.MethodGet).GetError(); err != nil { + SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceDeploymentHost.Scope(coretypes.VerbList)}), + }, handler.WithResourceDefs(handler.BasicResourceDef{ + Resource: coretypes.ResourceMetaResourceDeploymentHost, + Verb: coretypes.VerbList, + Category: coretypes.ActionCategoryDataAccess, + Selector: coretypes.WildcardSelector, + }))).Methods(http.MethodGet).GetError(); err != nil { return err } - if err := router.Handle("/api/v2/zeus/hosts", handler.New(provider.authzMiddleware.AdminAccess(provider.zeusHandler.PutHost), handler.OpenAPIDef{ + if err := router.Handle("/api/v2/zeus/hosts", handler.New(provider.authzMiddleware.CheckResources(provider.zeusHandler.PutHost, authtypes.SigNozAdminRoleName), handler.OpenAPIDef{ ID: "PutHost", Tags: []string{"zeus"}, Summary: "Put host in Zeus for a deployment.", @@ -56,8 +63,14 @@ func (provider *provider) addZeusRoutes(router *mux.Router) error { SuccessStatusCode: http.StatusNoContent, ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound, http.StatusConflict}, Deprecated: false, - SecuritySchemes: newSecuritySchemes(types.RoleAdmin), - })).Methods(http.MethodPut).GetError(); err != nil { + SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceDeploymentHost.Scope(coretypes.VerbUpdate)}), + }, handler.WithResourceDefs(handler.BasicResourceDef{ + Resource: coretypes.ResourceMetaResourceDeploymentHost, + Verb: coretypes.VerbUpdate, + Category: coretypes.ActionCategoryConfigurationChange, + ID: coretypes.BodyJSONPath("name"), + Selector: coretypes.WildcardSelector, + }))).Methods(http.MethodPut).GetError(); err != nil { return err } diff --git a/pkg/signoz/provider.go b/pkg/signoz/provider.go index 52d80b2e19..68cc7ab4a2 100644 --- a/pkg/signoz/provider.go +++ b/pkg/signoz/provider.go @@ -244,6 +244,7 @@ func NewSQLMigrationProviderFactories( sqlmigration.NewDeleteOrphanUserRolesFactory(), sqlmigration.NewMigrateLambdaDashboardsFactory(), sqlmigration.NewAddAuthDomainTuplesFactory(sqlstore), + sqlmigration.NewAddDeploymentHostTuplesFactory(sqlstore), ) } diff --git a/pkg/sqlmigration/118_add_deployment_host_tuples.go b/pkg/sqlmigration/118_add_deployment_host_tuples.go new file mode 100644 index 0000000000..026fff6485 --- /dev/null +++ b/pkg/sqlmigration/118_add_deployment_host_tuples.go @@ -0,0 +1,161 @@ +package sqlmigration + +import ( + "context" + "database/sql" + "encoding/json" + "time" + + "github.com/SigNoz/signoz/pkg/factory" + "github.com/SigNoz/signoz/pkg/sqlstore" + "github.com/SigNoz/signoz/pkg/types/authtypes" + "github.com/SigNoz/signoz/pkg/types/coretypes" + "github.com/oklog/ulid/v2" + "github.com/uptrace/bun" + "github.com/uptrace/bun/dialect" + "github.com/uptrace/bun/migrate" +) + +type addDeploymentHostTuples struct { + sqlstore sqlstore.SQLStore +} + +func NewAddDeploymentHostTuplesFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] { + return factory.NewProviderFactory(factory.MustNewName("add_deployment_host_tuples"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) { + return &addDeploymentHostTuples{sqlstore: sqlstore}, nil + }) +} + +func (migration *addDeploymentHostTuples) Register(migrations *migrate.Migrations) error { + return migrations.Register(migration.Up, migration.Down) +} + +func (migration *addDeploymentHostTuples) 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 + } + + var orgIDs []string + err = tx.NewSelect(). + Table("organizations"). + Column("id"). + Scan(ctx, &orgIDs) + if err != nil && err != sql.ErrNoRows { + return err + } + + isPG := migration.sqlstore.BunDB().Dialect().Name() == dialect.PG + + // zeus hosts moved from the legacy ViewAccess/AdminAccess role gates to + // CheckResources, which on enterprise requires real tuples -- existing orgs + // never had these written, only new orgs get them from the registry at bootstrap. + tuples := []migrationTuple{ + {authtypes.SigNozAdminRoleName, "metaresource", "deployment-host", "list"}, + {authtypes.SigNozAdminRoleName, "metaresource", "deployment-host", "update"}, + {authtypes.SigNozEditorRoleName, "metaresource", "deployment-host", "list"}, + {authtypes.SigNozViewerRoleName, "metaresource", "deployment-host", "list"}, + } + + 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, 0, 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 + } + } + } + } + + managedRoleGroups := make(map[string]string, len(coretypes.ManagedRoleToTransactions)) + for roleName, transactions := range coretypes.ManagedRoleToTransactions { + data, err := json.Marshal(authtypes.NewTransactionGroupsFromTransactions(transactions)) + if err != nil { + return err + } + managedRoleGroups[roleName] = string(data) + } + + for _, orgID := range orgIDs { + for roleName, data := range managedRoleGroups { + if _, err := tx.NewUpdate(). + Model(new(roles)). + Set("transaction_groups = ?", data). + Where("org_id = ?", orgID). + Where("type = ?", authtypes.RoleTypeManaged.StringValue()). + Where("name = ?", roleName). + Exec(ctx); err != nil { + return err + } + } + } + + return tx.Commit() +} + +func (migration *addDeploymentHostTuples) Down(context.Context, *bun.DB) error { + return nil +} diff --git a/pkg/types/coretypes/kind.go b/pkg/types/coretypes/kind.go index 41cfb6f7b6..7cca0a874a 100644 --- a/pkg/types/coretypes/kind.go +++ b/pkg/types/coretypes/kind.go @@ -71,6 +71,7 @@ func (name Kind) Enum() []any { KindFactorAPIKey, KindLicense, KindSubscription, + KindDeploymentHost, KindLogs, KindTraces, KindMetrics, diff --git a/pkg/types/coretypes/registry_kind.go b/pkg/types/coretypes/registry_kind.go index d4ab9e176b..e9de2c1e81 100644 --- a/pkg/types/coretypes/registry_kind.go +++ b/pkg/types/coretypes/registry_kind.go @@ -31,6 +31,7 @@ var Kinds = []Kind{ KindFactorAPIKey, KindLicense, KindSubscription, + KindDeploymentHost, KindLogs, KindTraces, KindMetrics, @@ -71,6 +72,7 @@ var ( KindFactorAPIKey = MustNewKind("factor-api-key") KindLicense = MustNewKind("license") KindSubscription = MustNewKind("subscription") + KindDeploymentHost = MustNewKind("deployment-host") KindLogs = MustNewKind("logs") KindTraces = MustNewKind("traces") KindMetrics = MustNewKind("metrics") diff --git a/pkg/types/coretypes/registry_managed_role.go b/pkg/types/coretypes/registry_managed_role.go index 3cb0e6d823..9e23defb6f 100644 --- a/pkg/types/coretypes/registry_managed_role.go +++ b/pkg/types/coretypes/registry_managed_role.go @@ -191,6 +191,9 @@ var ManagedRoleToTransactions = map[string][]Transaction{ {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: TypeMetaResource, Kind: KindTTLSetting}, WildCardSelectorString)}, + // deployment-host — admin updates, viewer lists + {Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindDeploymentHost}, WildCardSelectorString)}, + {Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindDeploymentHost}, 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)}, @@ -283,6 +286,8 @@ var ManagedRoleToTransactions = map[string][]Transaction{ // ttl-setting — read only (admin updates) {Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTTLSetting}, WildCardSelectorString)}, {Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTTLSetting}, WildCardSelectorString)}, + // deployment-host — list only (admin updates) + {Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindDeploymentHost}, 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)}, @@ -341,6 +346,8 @@ var ManagedRoleToTransactions = map[string][]Transaction{ // ttl-setting — read only {Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTTLSetting}, WildCardSelectorString)}, {Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindTTLSetting}, WildCardSelectorString)}, + // deployment-host — list only + {Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindDeploymentHost}, 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)}, diff --git a/pkg/types/coretypes/registry_resource.go b/pkg/types/coretypes/registry_resource.go index 030494cd89..fc09f125ee 100644 --- a/pkg/types/coretypes/registry_resource.go +++ b/pkg/types/coretypes/registry_resource.go @@ -31,6 +31,7 @@ var Resources = []Resource{ ResourceMetaResourceFactorAPIKey, ResourceMetaResourceLicense, ResourceMetaResourceSubscription, + ResourceMetaResourceDeploymentHost, ResourceTelemetryResourceLogs, ResourceTelemetryResourceTraces, ResourceTelemetryResourceMetrics, @@ -71,6 +72,7 @@ var ( ResourceMetaResourceFactorAPIKey = NewResourceMetaResource(KindFactorAPIKey, VerbCreate, VerbList, VerbRead, VerbUpdate, VerbDelete) ResourceMetaResourceLicense = NewResourceMetaResource(KindLicense) ResourceMetaResourceSubscription = NewResourceMetaResource(KindSubscription) + ResourceMetaResourceDeploymentHost = NewResourceMetaResource(KindDeploymentHost, VerbList, VerbUpdate) ResourceTelemetryResourceLogs = NewResourceTelemetryResource(KindLogs) ResourceTelemetryResourceTraces = NewResourceTelemetryResource(KindTraces) ResourceTelemetryResourceMetrics = NewResourceTelemetryResource(KindMetrics) diff --git a/tests/integration/testdata/role/managed_role_transactions.json b/tests/integration/testdata/role/managed_role_transactions.json index ce19659827..f94b56f281 100644 --- a/tests/integration/testdata/role/managed_role_transactions.json +++ b/tests/integration/testdata/role/managed_role_transactions.json @@ -295,6 +295,14 @@ "list" ] }, + { + "type": "metaresource", + "kind": "deployment-host", + "verbs": [ + "list", + "update" + ] + }, { "type": "metaresource", "kind": "user-preference", @@ -498,6 +506,13 @@ "list" ] }, + { + "type": "metaresource", + "kind": "deployment-host", + "verbs": [ + "list" + ] + }, { "type": "metaresource", "kind": "user-preference", @@ -654,6 +669,13 @@ "list" ] }, + { + "type": "metaresource", + "kind": "deployment-host", + "verbs": [ + "list" + ] + }, { "type": "metaresource", "kind": "user-preference",