mirror of
https://github.com/SigNoz/signoz.git
synced 2026-11-15 04:40:43 +00:00
Compare commits
8 Commits
main
...
feat/prefe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2736c82220 | ||
|
|
d5dc1cbfb7 | ||
|
|
433e777258 | ||
|
|
ca97c23788 | ||
|
|
ea990196dc | ||
|
|
24aab06a86 | ||
|
|
c25fb32935 | ||
|
|
7e87e1ca04 |
@@ -16319,9 +16319,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- org-preference:list
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
- org-preference:list
|
||||
summary: List org preferences
|
||||
tags:
|
||||
- preferences
|
||||
@@ -16384,9 +16384,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- org-preference:read
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
- org-preference:read
|
||||
summary: Get org preference
|
||||
tags:
|
||||
- preferences
|
||||
@@ -16441,9 +16441,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- org-preference:update
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
- org-preference:update
|
||||
summary: Update org preference
|
||||
tags:
|
||||
- preferences
|
||||
@@ -24702,9 +24702,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- organization:read
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
- organization:read
|
||||
summary: Get my organization
|
||||
tags:
|
||||
- orgs
|
||||
@@ -24753,9 +24753,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- organization:update
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
- organization:update
|
||||
summary: Update my organization
|
||||
tags:
|
||||
- orgs
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import {
|
||||
autocompletion,
|
||||
closeCompletion,
|
||||
@@ -23,16 +24,14 @@ import CodeMirror, {
|
||||
ViewUpdate,
|
||||
} from '@uiw/react-codemirror';
|
||||
import { Button, Popover, Tooltip } from 'antd';
|
||||
import { TelemetrytypesFieldDataTypeDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
|
||||
import { QueryBuilderKeys } from 'constants/queryBuilder';
|
||||
import { tracesAggregateOperatorOptions } from 'constants/queryBuilderOperators';
|
||||
import { useFieldKeysSuggestion } from 'hooks/querySuggestions/useFieldKeysSuggestion';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { Info, TriangleAlert } from '@signozhq/icons';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
DATA_SOURCE_TO_SIGNAL,
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { FieldDataType } from 'types/api/v5/queryRange';
|
||||
import { TracesAggregatorOperator } from 'types/common/queryBuilder';
|
||||
|
||||
import { useQueryBuilderV2Context } from '../../QueryBuilderV2Context';
|
||||
|
||||
@@ -42,12 +41,6 @@ const chipDecoration = Decoration.mark({
|
||||
class: 'chip-decorator',
|
||||
});
|
||||
|
||||
const OPERATORS_WITHOUT_DATA_TYPE: string[] = [
|
||||
TracesAggregatorOperator.COUNT,
|
||||
TracesAggregatorOperator.COUNT_DISTINCT,
|
||||
TracesAggregatorOperator.RATE,
|
||||
];
|
||||
|
||||
const operatorArgMeta: Record<
|
||||
string,
|
||||
{ acceptsArgs: boolean; multiple: boolean }
|
||||
@@ -317,19 +310,31 @@ function QueryAggregationSelect({
|
||||
// Find function context for fetching suggestions
|
||||
const functionContextForFetch = getFunctionContextAtCursor(input, cursorPos);
|
||||
|
||||
const numericKeysOnly = !(
|
||||
functionContextForFetch &&
|
||||
OPERATORS_WITHOUT_DATA_TYPE.includes(functionContextForFetch)
|
||||
);
|
||||
const { data: fieldKeys, isLoading: isLoadingFields } = useFieldKeysSuggestion(
|
||||
{
|
||||
signal: DATA_SOURCE_TO_SIGNAL[queryData.dataSource],
|
||||
searchText: '',
|
||||
fieldDataType: numericKeysOnly
|
||||
? TelemetrytypesFieldDataTypeDTO.number
|
||||
: undefined,
|
||||
const { data: aggregateAttributeData, isLoading: isLoadingFields } = useQuery(
|
||||
[
|
||||
QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE,
|
||||
functionContextForFetch,
|
||||
queryData.dataSource,
|
||||
],
|
||||
() => {
|
||||
const operatorsWithoutDataType: (string | undefined)[] = [
|
||||
TracesAggregatorOperator.COUNT,
|
||||
TracesAggregatorOperator.COUNT_DISTINCT,
|
||||
TracesAggregatorOperator.RATE,
|
||||
];
|
||||
|
||||
const fieldDataType: FieldDataType | undefined =
|
||||
functionContextForFetch &&
|
||||
operatorsWithoutDataType.includes(functionContextForFetch)
|
||||
? undefined
|
||||
: 'number';
|
||||
|
||||
return getKeySuggestions({
|
||||
signal: queryData.dataSource,
|
||||
searchText: '',
|
||||
fieldDataType,
|
||||
});
|
||||
},
|
||||
queryData.builderQueryType,
|
||||
{
|
||||
enabled:
|
||||
!!functionContextForFetch &&
|
||||
@@ -422,42 +427,48 @@ function QueryAggregationSelect({
|
||||
}),
|
||||
);
|
||||
|
||||
// Memoize field suggestions from API (no search filtering here)
|
||||
// Memoize field suggestions from API (no filtering here)
|
||||
const fieldSuggestions = useMemo(
|
||||
() =>
|
||||
(fieldKeys ?? []).map((key) => ({
|
||||
// The backend reads trace.-prefixed columns as per-trace aggregates.
|
||||
label: key.fieldContext === 'trace' ? `trace.${key.name}` : key.name,
|
||||
type: 'variable',
|
||||
info: key.fieldDataType,
|
||||
apply: (
|
||||
view: EditorView,
|
||||
completion: Completion,
|
||||
from: number,
|
||||
to: number,
|
||||
): void => {
|
||||
const text = view.state.sliceDoc(0, from);
|
||||
const funcName = getFunctionContextAtCursor(text, from);
|
||||
const multiple = funcName ? operatorArgMeta[funcName]?.multiple : false;
|
||||
Object.keys(aggregateAttributeData?.data.data.keys || {}).flatMap((key) => {
|
||||
const attributeKeys = aggregateAttributeData?.data.data.keys[key];
|
||||
if (!attributeKeys) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Insert the selected key followed by either a comma or closing parenthesis
|
||||
const insertText = multiple
|
||||
? `${completion.label},`
|
||||
: `${completion.label}) `;
|
||||
const cursorPos = from + insertText.length; // Use insertText.length instead of hardcoded values
|
||||
return attributeKeys.map((attributeKey) => ({
|
||||
label: attributeKey.name,
|
||||
type: 'variable',
|
||||
info: attributeKey.fieldDataType,
|
||||
apply: (
|
||||
view: EditorView,
|
||||
completion: Completion,
|
||||
from: number,
|
||||
to: number,
|
||||
): void => {
|
||||
const text = view.state.sliceDoc(0, from);
|
||||
const funcName = getFunctionContextAtCursor(text, from);
|
||||
const multiple = funcName ? operatorArgMeta[funcName]?.multiple : false;
|
||||
|
||||
view.dispatch({
|
||||
changes: { from, to, insert: insertText },
|
||||
selection: { anchor: cursorPos },
|
||||
});
|
||||
// Insert the selected key followed by either a comma or closing parenthesis
|
||||
const insertText = multiple
|
||||
? `${completion.label},`
|
||||
: `${completion.label}) `;
|
||||
const cursorPos = from + insertText.length; // Use insertText.length instead of hardcoded values
|
||||
|
||||
// Trigger next suggestions after a small delay
|
||||
setTimeout(() => {
|
||||
safeStartCompletion();
|
||||
}, 50);
|
||||
},
|
||||
})),
|
||||
[fieldKeys, safeStartCompletion],
|
||||
view.dispatch({
|
||||
changes: { from, to, insert: insertText },
|
||||
selection: { anchor: cursorPos },
|
||||
});
|
||||
|
||||
// Trigger next suggestions after a small delay
|
||||
setTimeout(() => {
|
||||
safeStartCompletion();
|
||||
}, 50);
|
||||
},
|
||||
}));
|
||||
}) || [],
|
||||
[aggregateAttributeData, safeStartCompletion],
|
||||
);
|
||||
|
||||
const aggregatorAutocomplete = useMemo(
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
import {
|
||||
GetAIObservabilityFieldsKeys200,
|
||||
GetFieldsKeys200,
|
||||
TelemetrytypesFieldContextDTO,
|
||||
TelemetrytypesFieldDataTypeDTO,
|
||||
TelemetrytypesSignalDTO,
|
||||
TelemetrytypesTelemetryFieldKeyDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import type { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { QueryBuilderV2Provider } from '../../QueryBuilderV2Context';
|
||||
import QueryAggregationSelect from '../QueryAggregation/QueryAggregationSelect';
|
||||
import { mockCodeMirrorDomApis } from './codemirrorDomMocks';
|
||||
|
||||
const EDITOR_TEST_ID = 'query-aggregation-editor';
|
||||
// CodeMirror's suggestion popup is slow to open under a parallel jest run.
|
||||
const SUGGESTIONS_TIMEOUT_MS = 5000;
|
||||
const TEST_TIMEOUT_MS = 15000;
|
||||
|
||||
const fieldKey = (
|
||||
name: string,
|
||||
fieldContext: TelemetrytypesFieldContextDTO,
|
||||
fieldDataType: TelemetrytypesFieldDataTypeDTO,
|
||||
): TelemetrytypesTelemetryFieldKeyDTO => ({
|
||||
name,
|
||||
fieldContext,
|
||||
fieldDataType,
|
||||
signal: TelemetrytypesSignalDTO.traces,
|
||||
});
|
||||
|
||||
const durationNano = fieldKey(
|
||||
'duration_nano',
|
||||
TelemetrytypesFieldContextDTO.span,
|
||||
TelemetrytypesFieldDataTypeDTO.float64,
|
||||
);
|
||||
|
||||
const serviceName = fieldKey(
|
||||
'service.name',
|
||||
TelemetrytypesFieldContextDTO.resource,
|
||||
TelemetrytypesFieldDataTypeDTO.string,
|
||||
);
|
||||
|
||||
const totalTokens = fieldKey(
|
||||
'total_tokens',
|
||||
TelemetrytypesFieldContextDTO.trace,
|
||||
TelemetrytypesFieldDataTypeDTO.float64,
|
||||
);
|
||||
|
||||
const aiKeysResponse = (
|
||||
keys: TelemetrytypesTelemetryFieldKeyDTO[],
|
||||
): GetAIObservabilityFieldsKeys200 => ({
|
||||
status: 'success',
|
||||
data: {
|
||||
complete: true,
|
||||
keys: Object.fromEntries(keys.map((key) => [key.name, [key]])),
|
||||
},
|
||||
});
|
||||
|
||||
// Assumes the AI endpoint's number filter also returns the float64 trace aggregates.
|
||||
const aiKeysFor = (
|
||||
params: URLSearchParams,
|
||||
): TelemetrytypesTelemetryFieldKeyDTO[] =>
|
||||
params.get('fieldDataType') === TelemetrytypesFieldDataTypeDTO.number
|
||||
? [durationNano, totalTokens]
|
||||
: [durationNano, serviceName, totalTokens];
|
||||
|
||||
const keysResponse: GetFieldsKeys200 = {
|
||||
status: 'success',
|
||||
data: { complete: true, keys: { duration_nano: [durationNano] } },
|
||||
};
|
||||
|
||||
const aiQuery: IBuilderQuery = {
|
||||
...(initialQueriesMap.traces.builder.queryData[0] as IBuilderQuery),
|
||||
builderQueryType: 'builder_ai_query',
|
||||
aggregations: [],
|
||||
};
|
||||
|
||||
const aiKeysRequests: URLSearchParams[] = [];
|
||||
const keysRequests: URLSearchParams[] = [];
|
||||
|
||||
beforeAll(() => {
|
||||
mockCodeMirrorDomApis();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
aiKeysRequests.length = 0;
|
||||
keysRequests.length = 0;
|
||||
server.use(
|
||||
rest.get(
|
||||
`${ENVIRONMENT.baseURL}/api/v1/ai_observability/fields/keys`,
|
||||
(req, res, ctx) => {
|
||||
aiKeysRequests.push(req.url.searchParams);
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(aiKeysResponse(aiKeysFor(req.url.searchParams))),
|
||||
);
|
||||
},
|
||||
),
|
||||
rest.get(`${ENVIRONMENT.baseURL}/api/v1/fields/keys`, (req, res, ctx) => {
|
||||
keysRequests.push(req.url.searchParams);
|
||||
return res(ctx.status(200), ctx.json(keysResponse));
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
async function enterAggregation(text: string): Promise<void> {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(
|
||||
<QueryBuilderV2Provider>
|
||||
<QueryAggregationSelect queryData={aiQuery} />
|
||||
</QueryBuilderV2Provider>,
|
||||
);
|
||||
|
||||
const content = (
|
||||
await screen.findByTestId(EDITOR_TEST_ID)
|
||||
).querySelector<HTMLElement>('.cm-content');
|
||||
if (!content) {
|
||||
throw new Error('CodeMirror content element not rendered');
|
||||
}
|
||||
await user.click(content);
|
||||
// Typed keys drop under load: a re-render's dispatch discards DOM input CodeMirror hasn't read yet.
|
||||
await user.paste(text);
|
||||
await user.keyboard('{Control>} {/Control}');
|
||||
}
|
||||
|
||||
function suggestionLabels(): string[] {
|
||||
return Array.from(
|
||||
screen.getByTestId(EDITOR_TEST_ID).querySelectorAll('.cm-completionLabel'),
|
||||
(label) => label.textContent ?? '',
|
||||
).sort();
|
||||
}
|
||||
|
||||
describe('QueryAggregationSelect field suggestions for an AI query', () => {
|
||||
it.each([
|
||||
{
|
||||
aggregation: 'avg(',
|
||||
fieldDataType: TelemetrytypesFieldDataTypeDTO.number,
|
||||
labels: ['duration_nano', 'trace.total_tokens'],
|
||||
},
|
||||
{
|
||||
aggregation: 'count_distinct(',
|
||||
fieldDataType: null,
|
||||
labels: ['duration_nano', 'service.name', 'trace.total_tokens'],
|
||||
},
|
||||
])(
|
||||
'suggests keys from the AI keys endpoint, with trace aggregates as trace.<name>, for $aggregation',
|
||||
async ({ aggregation, fieldDataType, labels }) => {
|
||||
await enterAggregation(aggregation);
|
||||
|
||||
await waitFor(() => expect(suggestionLabels()).toStrictEqual(labels), {
|
||||
timeout: SUGGESTIONS_TIMEOUT_MS,
|
||||
});
|
||||
expect(keysRequests).toHaveLength(0);
|
||||
expect(
|
||||
aiKeysRequests.map((params) => ({
|
||||
signal: params.get('signal'),
|
||||
fieldDataType: params.get('fieldDataType'),
|
||||
fieldContext: params.get('fieldContext'),
|
||||
})),
|
||||
).toStrictEqual([{ signal: 'traces', fieldDataType, fieldContext: null }]);
|
||||
},
|
||||
TEST_TIMEOUT_MS,
|
||||
);
|
||||
});
|
||||
@@ -5,11 +5,13 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (provider *provider) addOrgRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v2/orgs/me", handler.New(provider.authzMiddleware.AdminAccess(provider.orgHandler.Get), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v2/orgs/me", handler.New(provider.authzMiddleware.CheckResources(provider.orgHandler.Get, authtypes.SigNozAdminRoleName), handler.OpenAPIDef{
|
||||
ID: "GetMyOrganization",
|
||||
Tags: []string{"orgs"},
|
||||
Summary: "Get my organization",
|
||||
@@ -21,12 +23,18 @@ func (provider *provider) addOrgRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceOrganization.Scope(coretypes.VerbRead)}),
|
||||
}, handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceOrganization,
|
||||
Verb: coretypes.VerbRead,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
ID: provider.orgIDExtractor(),
|
||||
Selector: coretypes.IDSelector,
|
||||
}))).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/orgs/me", handler.New(provider.authzMiddleware.AdminAccess(provider.orgHandler.Update), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v2/orgs/me", handler.New(provider.authzMiddleware.CheckResources(provider.orgHandler.Update, authtypes.SigNozAdminRoleName), handler.OpenAPIDef{
|
||||
ID: "UpdateMyOrganization",
|
||||
Tags: []string{"orgs"},
|
||||
Summary: "Update my organization",
|
||||
@@ -38,10 +46,31 @@ func (provider *provider) addOrgRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusConflict, http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceOrganization.Scope(coretypes.VerbUpdate)}),
|
||||
}, handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceOrganization,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: coretypes.ActionCategoryConfigurationChange,
|
||||
ID: provider.orgIDExtractor(),
|
||||
Selector: coretypes.IDSelector,
|
||||
}))).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *provider) orgIDExtractor() coretypes.ResourceIDExtractor {
|
||||
return coretypes.NewResourceIDExtractor(coretypes.PhaseRequest, func(ec coretypes.ExtractorContext) (string, error) {
|
||||
if ec.Request == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ec.Request.Context())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return claims.OrgID, nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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/preferencetypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
@@ -61,7 +63,7 @@ func (provider *provider) addPreferenceRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/org/preferences", handler.New(provider.authzMiddleware.AdminAccess(provider.preferenceHandler.ListByOrg), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v1/org/preferences", handler.New(provider.authzMiddleware.CheckResources(provider.preferenceHandler.ListByOrg, authtypes.SigNozAdminRoleName), handler.OpenAPIDef{
|
||||
ID: "ListOrgPreferences",
|
||||
Tags: []string{"preferences"},
|
||||
Summary: "List org preferences",
|
||||
@@ -73,12 +75,17 @@ func (provider *provider) addPreferenceRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceOrgPreference.Scope(coretypes.VerbList)}),
|
||||
}, handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceOrgPreference,
|
||||
Verb: coretypes.VerbList,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
Selector: coretypes.WildcardSelector,
|
||||
}))).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/org/preferences/{name}", handler.New(provider.authzMiddleware.AdminAccess(provider.preferenceHandler.GetByOrg), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v1/org/preferences/{name}", handler.New(provider.authzMiddleware.CheckResources(provider.preferenceHandler.GetByOrg, authtypes.SigNozAdminRoleName), handler.OpenAPIDef{
|
||||
ID: "GetOrgPreference",
|
||||
Tags: []string{"preferences"},
|
||||
Summary: "Get org preference",
|
||||
@@ -90,12 +97,18 @@ func (provider *provider) addPreferenceRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceOrgPreference.Scope(coretypes.VerbRead)}),
|
||||
}, handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceOrgPreference,
|
||||
Verb: coretypes.VerbRead,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
ID: coretypes.PathParam("name"),
|
||||
Selector: coretypes.WildcardSelector,
|
||||
}))).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/org/preferences/{name}", handler.New(provider.authzMiddleware.AdminAccess(provider.preferenceHandler.UpdateByOrg), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v1/org/preferences/{name}", handler.New(provider.authzMiddleware.CheckResources(provider.preferenceHandler.UpdateByOrg, authtypes.SigNozAdminRoleName), handler.OpenAPIDef{
|
||||
ID: "UpdateOrgPreference",
|
||||
Tags: []string{"preferences"},
|
||||
Summary: "Update org preference",
|
||||
@@ -107,8 +120,14 @@ func (provider *provider) addPreferenceRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceOrgPreference.Scope(coretypes.VerbUpdate)}),
|
||||
}, handler.WithResourceDefs(handler.BasicResourceDef{
|
||||
Resource: coretypes.ResourceMetaResourceOrgPreference,
|
||||
Verb: coretypes.VerbUpdate,
|
||||
Category: coretypes.ActionCategoryConfigurationChange,
|
||||
ID: coretypes.PathParam("name"),
|
||||
Selector: coretypes.WildcardSelector,
|
||||
}))).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -275,6 +275,8 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewMigrateRulesV1ToV2Alpha1Factory(sqlstore),
|
||||
sqlmigration.NewAddLLMPricingRuleTuplesFactory(sqlstore),
|
||||
sqlmigration.NewRemoveUnsupportedQuickFiltersFactory(),
|
||||
sqlmigration.NewAddOrganizationTuplesFactory(sqlstore),
|
||||
sqlmigration.NewAddOrgPreferenceTuplesFactory(sqlstore),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
158
pkg/sqlmigration/148_add_organization_tuples.go
Normal file
158
pkg/sqlmigration/148_add_organization_tuples.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type addOrganizationTuples struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
}
|
||||
|
||||
func NewAddOrganizationTuplesFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_organization_tuples"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &addOrganizationTuples{sqlstore: sqlstore}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (migration *addOrganizationTuples) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
func (migration *addOrganizationTuples) 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
|
||||
|
||||
tuples := []migrationTuple{
|
||||
{authtypes.SigNozAdminRoleName, "organization", "organization", "read"},
|
||||
{authtypes.SigNozAdminRoleName, "organization", "organization", "update"},
|
||||
}
|
||||
|
||||
for _, orgID := range orgIDs {
|
||||
for _, tuple := range tuples {
|
||||
entropy := ulid.DefaultEntropy()
|
||||
now := time.Now().UTC()
|
||||
tupleID := ulid.MustNew(ulid.Timestamp(now), entropy).String()
|
||||
|
||||
// The organization object has no org prefix: organization:organization/<selector>.
|
||||
objectID := 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 *addOrganizationTuples) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
133
pkg/sqlmigration/149_add_org_preference_tuples.go
Normal file
133
pkg/sqlmigration/149_add_org_preference_tuples.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
)
|
||||
|
||||
type addOrgPreferenceTuples struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
}
|
||||
|
||||
func NewAddOrgPreferenceTuplesFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_org_preference_tuples"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &addOrgPreferenceTuples{sqlstore: sqlstore}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (migration *addOrgPreferenceTuples) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
func (migration *addOrgPreferenceTuples) 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
|
||||
|
||||
tuples := []migrationTuple{
|
||||
{"signoz-admin", "metaresource", "org-preference", "read"},
|
||||
{"signoz-admin", "metaresource", "org-preference", "update"},
|
||||
{"signoz-admin", "metaresource", "org-preference", "delete"},
|
||||
{"signoz-admin", "metaresource", "org-preference", "create"},
|
||||
{"signoz-admin", "metaresource", "org-preference", "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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *addOrgPreferenceTuples) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
@@ -88,6 +88,9 @@ var ManagedRoleToTransactions = map[string][]Transaction{
|
||||
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindOrgPreference}, WildCardSelectorString)},
|
||||
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindOrgPreference}, WildCardSelectorString)},
|
||||
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindOrgPreference}, WildCardSelectorString)},
|
||||
// organization: admin only
|
||||
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeOrganization, Kind: KindOrganization}, WildCardSelectorString)},
|
||||
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeOrganization, Kind: KindOrganization}, 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)},
|
||||
|
||||
@@ -138,6 +138,14 @@
|
||||
"list"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "organization",
|
||||
"kind": "organization",
|
||||
"verbs": [
|
||||
"read",
|
||||
"update"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "metaresource",
|
||||
"kind": "public-dashboard",
|
||||
|
||||
Reference in New Issue
Block a user