mirror of
https://github.com/SigNoz/signoz.git
synced 2026-10-14 14:11:11 +01:00
Compare commits
4 Commits
issue_nerv
...
keystone-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e5d295545 | ||
|
|
42beea849c | ||
|
|
08d69b1f26 | ||
|
|
3f51075634 |
@@ -494,6 +494,11 @@ authz:
|
||||
openfga:
|
||||
# maximum tuples allowed per openfga write operation.
|
||||
max_tuples_per_write: 300
|
||||
telemetry:
|
||||
# Maximum number of IN value combinations one builder query is split into
|
||||
# for the access check. Beyond it the query is checked as one resource and
|
||||
# an IN is only covered by its key wildcard grant.
|
||||
max_query_disjuncts: 64
|
||||
|
||||
##################### Meter Reporter #####################
|
||||
meterreporter:
|
||||
|
||||
@@ -146,13 +146,96 @@ describe('PermissionEditor - TelemetrySelectorWizard', () => {
|
||||
await expect(screen.findByText('promql/*')).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hardcodes the key and does not let it be edited', async () => {
|
||||
it('defaults the key to signoz.workspace.key.id', async () => {
|
||||
const user = userEvent.setup();
|
||||
await openLogsWizard(user);
|
||||
|
||||
const keyInput = screen.getByTestId('wizard-key-input-logs-read');
|
||||
expect(keyInput).toHaveValue('signoz.workspace.key.id');
|
||||
expect(keyInput).toBeDisabled();
|
||||
expect(screen.getByTestId('wizard-key-select-logs-read')).toHaveTextContent(
|
||||
'signoz.workspace.key.id',
|
||||
);
|
||||
});
|
||||
|
||||
it('rewrites the selector when another key is picked', async () => {
|
||||
const user = userEvent.setup();
|
||||
await openLogsWizard(user);
|
||||
|
||||
await user.type(
|
||||
screen.getByTestId('wizard-value-input-logs-read'),
|
||||
'checkout',
|
||||
);
|
||||
await user.click(screen.getByTestId('wizard-key-select-logs-read'));
|
||||
await user.click(
|
||||
await screen.findByTestId(
|
||||
'wizard-key-option-resource.service.name-logs-read',
|
||||
),
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('wizard-selector-input-logs-read')).toHaveValue(
|
||||
'builder_query/resource.service.name/checkout',
|
||||
);
|
||||
|
||||
await user.click(screen.getByTestId('wizard-add-btn-logs-read'));
|
||||
|
||||
await expect(
|
||||
screen.findByText('builder_query/resource.service.name/checkout'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('folds the resource prefix of the workspace key from a typed selector', async () => {
|
||||
const user = userEvent.setup();
|
||||
await openLogsWizard(user);
|
||||
|
||||
const selectorInput = screen.getByTestId('wizard-selector-input-logs-read');
|
||||
await user.clear(selectorInput);
|
||||
await user.type(
|
||||
selectorInput,
|
||||
'builder_query/resource.signoz.workspace.key.id/key-a',
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('wizard-key-select-logs-read')).toHaveTextContent(
|
||||
'signoz.workspace.key.id',
|
||||
);
|
||||
|
||||
await user.type(screen.getByTestId('wizard-value-input-logs-read'), '2');
|
||||
|
||||
expect(selectorInput).toHaveValue(
|
||||
'builder_query/signoz.workspace.key.id/key-a2',
|
||||
);
|
||||
});
|
||||
|
||||
it('selects the key from a typed selector', async () => {
|
||||
const user = userEvent.setup();
|
||||
await openLogsWizard(user);
|
||||
|
||||
const selectorInput = screen.getByTestId('wizard-selector-input-logs-read');
|
||||
await user.clear(selectorInput);
|
||||
await user.type(
|
||||
selectorInput,
|
||||
'builder_query/resource.deployment.environment.name/prod',
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('wizard-key-select-logs-read')).toHaveTextContent(
|
||||
'resource.deployment.environment.name',
|
||||
);
|
||||
expect(screen.getByTestId('wizard-value-input-logs-read')).toHaveValue(
|
||||
'prod',
|
||||
);
|
||||
});
|
||||
|
||||
it('hints when a typed selector uses an unsupported key', async () => {
|
||||
const user = userEvent.setup();
|
||||
await openLogsWizard(user);
|
||||
|
||||
const selectorInput = screen.getByTestId('wizard-selector-input-logs-read');
|
||||
await user.clear(selectorInput);
|
||||
await user.type(selectorInput, 'builder_query/host.name/web-1');
|
||||
|
||||
expect(screen.getByTestId('wizard-key-select-logs-read')).toHaveTextContent(
|
||||
'signoz.workspace.key.id',
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId('wizard-selector-hint-logs-read'),
|
||||
).toHaveTextContent('"host.name" is not a supported key.');
|
||||
});
|
||||
|
||||
it('hides Key field for query types that do not support key scoping', async () => {
|
||||
@@ -163,7 +246,7 @@ describe('PermissionEditor - TelemetrySelectorWizard', () => {
|
||||
await user.click(await screen.findByText('ClickHouse SQL'));
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('wizard-key-input-logs-read'),
|
||||
screen.queryByTestId('wizard-key-select-logs-read'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -285,35 +368,18 @@ describe('PermissionEditor - TelemetrySelectorWizard', () => {
|
||||
expect(screen.getByLabelText('Any value')).toBeChecked();
|
||||
});
|
||||
|
||||
it('keeps the key input hardcoded when the selector uses another key', async () => {
|
||||
it('restores the selected key in the selector once the value changes', async () => {
|
||||
const user = userEvent.setup();
|
||||
await openLogsWizard(user);
|
||||
|
||||
const selectorInput = screen.getByTestId('wizard-selector-input-logs-read');
|
||||
await user.clear(selectorInput);
|
||||
await user.type(selectorInput, 'builder_query/service.name/frontend');
|
||||
|
||||
expect(screen.getByTestId('wizard-key-input-logs-read')).toHaveValue(
|
||||
'signoz.workspace.key.id',
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId('wizard-selector-hint-logs-read'),
|
||||
).toHaveTextContent('Allow service.name=frontend for Builder Query queries.');
|
||||
expect(screen.getByTestId('wizard-add-btn-logs-read')).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('restores the hardcoded key in the selector once the value changes', async () => {
|
||||
const user = userEvent.setup();
|
||||
await openLogsWizard(user);
|
||||
|
||||
const selectorInput = screen.getByTestId('wizard-selector-input-logs-read');
|
||||
await user.clear(selectorInput);
|
||||
await user.type(selectorInput, 'builder_query/service.name/frontend');
|
||||
await user.type(selectorInput, 'builder_query/host.name/web-1');
|
||||
|
||||
await user.type(screen.getByTestId('wizard-value-input-logs-read'), '2');
|
||||
|
||||
expect(selectorInput).toHaveValue(
|
||||
'builder_query/signoz.workspace.key.id/frontend2',
|
||||
'builder_query/signoz.workspace.key.id/web-12',
|
||||
);
|
||||
expect(screen.getByTestId('wizard-add-btn-logs-read')).not.toBeDisabled();
|
||||
});
|
||||
|
||||
@@ -30,17 +30,26 @@ export const QUERY_TYPES: readonly QueryTypeOption[] = [
|
||||
|
||||
export const DEFAULT_QUERY_TYPE: QueryTypeId = 'builder_query';
|
||||
|
||||
export const SUPPORTED_GRANT_KEY = 'signoz.workspace.key.id';
|
||||
// mirrors telemetryGrantKeys in pkg/types/telemetrytypes/selector.go
|
||||
export const SUPPORTED_GRANT_KEYS: readonly string[] = [
|
||||
'signoz.workspace.key.id',
|
||||
'resource.service.name',
|
||||
'resource.deployment.environment.name',
|
||||
];
|
||||
|
||||
export const DEFAULT_GRANT_KEY = 'signoz.workspace.key.id';
|
||||
|
||||
export const ANY_RESOURCE_VALUE = '*';
|
||||
|
||||
export interface SelectorDraft {
|
||||
queryType: QueryTypeId;
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ParsedSelector {
|
||||
queryType?: QueryTypeId;
|
||||
key?: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Typography } from '@signozhq/ui/typography';
|
||||
import {
|
||||
ANY_RESOURCE_VALUE,
|
||||
QUERY_TYPES,
|
||||
SUPPORTED_GRANT_KEY,
|
||||
SUPPORTED_GRANT_KEYS,
|
||||
} from './TelemetrySelectorWizard.constants';
|
||||
import { isQueryTypeAvailable } from './TelemetrySelectorWizard.utils';
|
||||
import useTelemetrySelectorWizard from './useTelemetrySelectorWizard';
|
||||
@@ -38,6 +38,7 @@ function TelemetrySelectorWizard({
|
||||
open,
|
||||
queryType,
|
||||
selectedQueryType,
|
||||
grantKey,
|
||||
value,
|
||||
selector,
|
||||
isAnyResource,
|
||||
@@ -46,6 +47,7 @@ function TelemetrySelectorWizard({
|
||||
canAdd,
|
||||
handleOpenChange,
|
||||
handleQueryTypeChange,
|
||||
handleKeyChange,
|
||||
handleValueChange,
|
||||
handleAnyResourceChange,
|
||||
handleSelectorChange,
|
||||
@@ -125,12 +127,22 @@ function TelemetrySelectorWizard({
|
||||
<Typography as="label" weight="medium">
|
||||
Key
|
||||
</Typography>
|
||||
<Input
|
||||
value={SUPPORTED_GRANT_KEY}
|
||||
readOnly
|
||||
disabled
|
||||
testId={`wizard-key-input-${testId}`}
|
||||
/>
|
||||
<Select value={grantKey} onChange={handleKeyChange}>
|
||||
<SelectTrigger data-testid={`wizard-key-select-${testId}`}>
|
||||
<SelectValue>{grantKey}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent withPortal={false} className={styles.selectContent}>
|
||||
{SUPPORTED_GRANT_KEYS.map((supportedKey) => (
|
||||
<SelectItem
|
||||
key={supportedKey}
|
||||
value={supportedKey}
|
||||
testId={`wizard-key-option-${supportedKey}-${testId}`}
|
||||
>
|
||||
{supportedKey}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -2,13 +2,14 @@ import { AuthZResource } from 'lib/authz/hooks/useAuthZ/types';
|
||||
|
||||
import {
|
||||
ANY_RESOURCE_VALUE,
|
||||
DEFAULT_GRANT_KEY,
|
||||
ParsedSelector,
|
||||
QUERY_TYPES,
|
||||
QueryTypeId,
|
||||
QueryTypeOption,
|
||||
SelectorDraft,
|
||||
SelectorValidation,
|
||||
SUPPORTED_GRANT_KEY,
|
||||
SUPPORTED_GRANT_KEYS,
|
||||
} from './TelemetrySelectorWizard.constants';
|
||||
|
||||
const METRIC_RESOURCES: ReadonlySet<AuthZResource> = new Set<AuthZResource>([
|
||||
@@ -37,6 +38,20 @@ export function isAnyResourceValue(value: string): boolean {
|
||||
return value.trim() === ANY_RESOURCE_VALUE;
|
||||
}
|
||||
|
||||
export function isSupportedGrantKey(key: string): boolean {
|
||||
return SUPPORTED_GRANT_KEYS.includes(key);
|
||||
}
|
||||
|
||||
const RESOURCE_PREFIX = 'resource.';
|
||||
|
||||
// the backend folds resource.<key> to <key> when <key> is itself supported
|
||||
function foldGrantKey(key: string): string {
|
||||
const stripped = key.startsWith(RESOURCE_PREFIX)
|
||||
? key.slice(RESOURCE_PREFIX.length)
|
||||
: key;
|
||||
return isSupportedGrantKey(stripped) ? stripped : key;
|
||||
}
|
||||
|
||||
function splitSelector(selector: string): string[] {
|
||||
const parts = selector.split('/');
|
||||
|
||||
@@ -47,14 +62,18 @@ function splitSelector(selector: string): string[] {
|
||||
return [parts[0], parts[1], parts.slice(2).join('/')];
|
||||
}
|
||||
|
||||
export function buildSelector({ queryType, value }: SelectorDraft): string {
|
||||
export function buildSelector({
|
||||
queryType,
|
||||
key,
|
||||
value,
|
||||
}: SelectorDraft): string {
|
||||
const trimmedValue = value.trim();
|
||||
|
||||
if (!supportsKeyScoping(queryType) || !trimmedValue) {
|
||||
return `${queryType}/${ANY_RESOURCE_VALUE}`;
|
||||
}
|
||||
|
||||
return `${queryType}/${SUPPORTED_GRANT_KEY}/${trimmedValue}`;
|
||||
return `${queryType}/${key}/${trimmedValue}`;
|
||||
}
|
||||
|
||||
export function parseSelector(selector: string): ParsedSelector {
|
||||
@@ -62,6 +81,7 @@ export function parseSelector(selector: string): ParsedSelector {
|
||||
|
||||
return {
|
||||
queryType: getQueryTypeOption(parts[0])?.id,
|
||||
key: parts.length >= 3 ? foldGrantKey(parts[1]) : undefined,
|
||||
value: parts.length >= 3 ? parts[2] : '',
|
||||
};
|
||||
}
|
||||
@@ -101,7 +121,9 @@ export function validateSelector(selector: string): SelectorValidation {
|
||||
}
|
||||
|
||||
return {
|
||||
message: `Use <query-type>/${ANY_RESOURCE_VALUE} or <query-type>/${SUPPORTED_GRANT_KEY}/<value>.`,
|
||||
message: `Use <query-type>/${ANY_RESOURCE_VALUE} or <query-type>/<key>/<value> with one of: ${SUPPORTED_GRANT_KEYS.join(
|
||||
', ',
|
||||
)}.`,
|
||||
isError: false, // intentionally not an error
|
||||
};
|
||||
}
|
||||
@@ -128,6 +150,15 @@ export function validateSelector(selector: string): SelectorValidation {
|
||||
};
|
||||
}
|
||||
|
||||
if (!isSupportedGrantKey(key)) {
|
||||
return {
|
||||
message: `"${key}" is not a supported key. Use one of: ${SUPPORTED_GRANT_KEYS.join(
|
||||
', ',
|
||||
)}.`,
|
||||
isError: false, // intentionally not an error
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
message: `Allow ${key}=${value} for ${option.label} queries.`,
|
||||
isError: false,
|
||||
@@ -135,5 +166,5 @@ export function validateSelector(selector: string): SelectorValidation {
|
||||
}
|
||||
|
||||
export function getDefaultSelector(queryType: QueryTypeId): string {
|
||||
return buildSelector({ queryType, value: '' });
|
||||
return buildSelector({ queryType, key: DEFAULT_GRANT_KEY, value: '' });
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
ANY_RESOURCE_VALUE,
|
||||
DEFAULT_GRANT_KEY,
|
||||
DEFAULT_QUERY_TYPE,
|
||||
QueryTypeId,
|
||||
QueryTypeOption,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
getDefaultSelector,
|
||||
getQueryTypeOption,
|
||||
isAnyResourceValue,
|
||||
isSupportedGrantKey,
|
||||
parseSelector,
|
||||
validateSelector,
|
||||
} from './TelemetrySelectorWizard.utils';
|
||||
@@ -24,6 +26,7 @@ interface UseTelemetrySelectorWizardResult {
|
||||
open: boolean;
|
||||
queryType: QueryTypeId;
|
||||
selectedQueryType: QueryTypeOption | undefined;
|
||||
grantKey: string;
|
||||
value: string;
|
||||
selector: string;
|
||||
isAnyResource: boolean;
|
||||
@@ -32,6 +35,7 @@ interface UseTelemetrySelectorWizardResult {
|
||||
canAdd: boolean;
|
||||
handleOpenChange: (nextOpen: boolean) => void;
|
||||
handleQueryTypeChange: (value: string | string[]) => void;
|
||||
handleKeyChange: (value: string | string[]) => void;
|
||||
handleValueChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
handleAnyResourceChange: (checked: boolean) => void;
|
||||
handleSelectorChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
@@ -44,6 +48,7 @@ function useTelemetrySelectorWizard({
|
||||
}: UseTelemetrySelectorWizardParams): UseTelemetrySelectorWizardResult {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [queryType, setQueryType] = useState<QueryTypeId>(DEFAULT_QUERY_TYPE);
|
||||
const [grantKey, setGrantKey] = useState(DEFAULT_GRANT_KEY);
|
||||
const [value, setValue] = useState('');
|
||||
const [selector, setSelector] = useState(() =>
|
||||
getDefaultSelector(DEFAULT_QUERY_TYPE),
|
||||
@@ -58,10 +63,17 @@ function useTelemetrySelectorWizard({
|
||||
const validation = useMemo(() => validateSelector(selector), [selector]);
|
||||
|
||||
const applyDraft = useCallback(
|
||||
(nextQueryType: QueryTypeId, nextValue: string): void => {
|
||||
(nextQueryType: QueryTypeId, nextKey: string, nextValue: string): void => {
|
||||
setQueryType(nextQueryType);
|
||||
setGrantKey(nextKey);
|
||||
setValue(nextValue);
|
||||
setSelector(buildSelector({ queryType: nextQueryType, value: nextValue }));
|
||||
setSelector(
|
||||
buildSelector({
|
||||
queryType: nextQueryType,
|
||||
key: nextKey,
|
||||
value: nextValue,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
@@ -71,23 +83,30 @@ function useTelemetrySelectorWizard({
|
||||
const selected = (Array.isArray(next) ? next[0] : next) as QueryTypeId;
|
||||
const keepsValue = getQueryTypeOption(selected)?.supportsKeyScoping ?? false;
|
||||
|
||||
applyDraft(selected, keepsValue ? value : '');
|
||||
applyDraft(selected, grantKey, keepsValue ? value : '');
|
||||
},
|
||||
[applyDraft, value],
|
||||
[applyDraft, grantKey, value],
|
||||
);
|
||||
|
||||
const handleKeyChange = useCallback(
|
||||
(next: string | string[]): void => {
|
||||
applyDraft(queryType, Array.isArray(next) ? next[0] : next, value);
|
||||
},
|
||||
[applyDraft, queryType, value],
|
||||
);
|
||||
|
||||
const handleValueChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
applyDraft(queryType, event.target.value);
|
||||
applyDraft(queryType, grantKey, event.target.value);
|
||||
},
|
||||
[applyDraft, queryType],
|
||||
[applyDraft, queryType, grantKey],
|
||||
);
|
||||
|
||||
const handleAnyResourceChange = useCallback(
|
||||
(checked: boolean): void => {
|
||||
applyDraft(queryType, checked ? ANY_RESOURCE_VALUE : '');
|
||||
applyDraft(queryType, grantKey, checked ? ANY_RESOURCE_VALUE : '');
|
||||
},
|
||||
[applyDraft, queryType],
|
||||
[applyDraft, queryType, grantKey],
|
||||
);
|
||||
|
||||
const handleSelectorChange = useCallback(
|
||||
@@ -99,6 +118,9 @@ function useTelemetrySelectorWizard({
|
||||
if (parsed.queryType) {
|
||||
setQueryType(parsed.queryType);
|
||||
}
|
||||
if (parsed.key && isSupportedGrantKey(parsed.key)) {
|
||||
setGrantKey(parsed.key);
|
||||
}
|
||||
setValue(parsed.value);
|
||||
},
|
||||
[],
|
||||
@@ -109,6 +131,7 @@ function useTelemetrySelectorWizard({
|
||||
|
||||
if (!nextOpen) {
|
||||
setQueryType(DEFAULT_QUERY_TYPE);
|
||||
setGrantKey(DEFAULT_GRANT_KEY);
|
||||
setValue('');
|
||||
setSelector(getDefaultSelector(DEFAULT_QUERY_TYPE));
|
||||
}
|
||||
@@ -138,6 +161,7 @@ function useTelemetrySelectorWizard({
|
||||
open,
|
||||
queryType,
|
||||
selectedQueryType,
|
||||
grantKey,
|
||||
value,
|
||||
selector,
|
||||
isAnyResource: isAnyResourceValue(value),
|
||||
@@ -146,6 +170,7 @@ function useTelemetrySelectorWizard({
|
||||
canAdd: !validation.isError,
|
||||
handleOpenChange,
|
||||
handleQueryTypeChange,
|
||||
handleKeyChange,
|
||||
handleValueChange,
|
||||
handleAnyResourceChange,
|
||||
handleSelectorChange,
|
||||
|
||||
@@ -53,6 +53,7 @@ import (
|
||||
|
||||
type provider struct {
|
||||
globalConfig global.Config
|
||||
authzConfig authz.Config
|
||||
web web.Web
|
||||
router *mux.Router
|
||||
httpServer *httpserver.Server
|
||||
@@ -143,6 +144,7 @@ func NewFactory(
|
||||
statsHandler statsreporter.Handler,
|
||||
savedViewHandler savedview.Handler,
|
||||
globalConfig global.Config,
|
||||
authzConfig authz.Config,
|
||||
identNResolver identn.IdentNResolver,
|
||||
sharder sharder.Sharder,
|
||||
auditor auditor.Auditor,
|
||||
@@ -196,6 +198,7 @@ func NewFactory(
|
||||
statsHandler,
|
||||
savedViewHandler,
|
||||
globalConfig,
|
||||
authzConfig,
|
||||
identNResolver,
|
||||
sharder,
|
||||
auditor,
|
||||
@@ -251,6 +254,7 @@ func newProvider(
|
||||
statsHandler statsreporter.Handler,
|
||||
savedViewHandler savedview.Handler,
|
||||
globalConfig global.Config,
|
||||
authzConfig authz.Config,
|
||||
identNResolver identn.IdentNResolver,
|
||||
sharder sharder.Sharder,
|
||||
auditor auditor.Auditor,
|
||||
@@ -263,6 +267,7 @@ func newProvider(
|
||||
|
||||
provider := &provider{
|
||||
globalConfig: globalConfig,
|
||||
authzConfig: authzConfig,
|
||||
web: web,
|
||||
router: router,
|
||||
healthyC: make(chan struct{}),
|
||||
|
||||
@@ -464,7 +464,7 @@ func (provider *provider) addQuerierRoutes(router *mux.Router) error {
|
||||
Verb: coretypes.VerbRead,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
Selector: querybuilder.TelemetrySelector,
|
||||
Resources: querybuilder.QueryRangeResources,
|
||||
Resources: querybuilder.NewQueryRangeResources(provider.authzConfig.Telemetry.MaxQueryDisjuncts),
|
||||
}))).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -486,7 +486,7 @@ func (provider *provider) addQuerierRoutes(router *mux.Router) error {
|
||||
Verb: coretypes.VerbRead,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
Selector: querybuilder.TelemetrySelector,
|
||||
Resources: querybuilder.QueryRangeResources,
|
||||
Resources: querybuilder.NewQueryRangeResources(provider.authzConfig.Telemetry.MaxQueryDisjuncts),
|
||||
}))).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package authz
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
)
|
||||
|
||||
@@ -10,6 +11,9 @@ type Config struct {
|
||||
|
||||
// OpenFGA is the configuration specific to the OpenFGA authorization provider.
|
||||
OpenFGA OpenFGAConfig `mapstructure:"openfga"`
|
||||
|
||||
// Telemetry is the configuration for the telemetry query access check.
|
||||
Telemetry TelemetryConfig `mapstructure:"telemetry"`
|
||||
}
|
||||
|
||||
type OpenFGAConfig struct {
|
||||
@@ -17,6 +21,12 @@ type OpenFGAConfig struct {
|
||||
MaxTuplesPerWrite int `mapstructure:"max_tuples_per_write"`
|
||||
}
|
||||
|
||||
type TelemetryConfig struct {
|
||||
// MaxQueryDisjuncts is the maximum number of IN value combinations one builder query is split
|
||||
// into for the access check; beyond it the query is checked as one resource.
|
||||
MaxQueryDisjuncts int `mapstructure:"max_query_disjuncts"`
|
||||
}
|
||||
|
||||
func NewConfigFactory() factory.ConfigFactory {
|
||||
return factory.NewConfigFactory(factory.MustNewName("authz"), newConfig)
|
||||
}
|
||||
@@ -27,9 +37,16 @@ func newConfig() factory.Config {
|
||||
OpenFGA: OpenFGAConfig{
|
||||
MaxTuplesPerWrite: 300,
|
||||
},
|
||||
Telemetry: TelemetryConfig{
|
||||
MaxQueryDisjuncts: 64,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c Config) Validate() error {
|
||||
if c.Telemetry.MaxQueryDisjuncts < 1 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "authz.telemetry.max_query_disjuncts must be at least 1, got %d", c.Telemetry.MaxQueryDisjuncts)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@ import (
|
||||
)
|
||||
|
||||
func TelemetrySelector(_ context.Context, resource coretypes.Resource, id string, _ valuer.UUID) ([]coretypes.Selector, error) {
|
||||
values := telemetrytypes.NewTelemetryGrantSelectors(id)
|
||||
values, err := telemetrySelectorValues(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
selectors := make([]coretypes.Selector, 0, len(values))
|
||||
for _, value := range values {
|
||||
@@ -28,7 +31,13 @@ func TelemetrySelector(_ context.Context, resource coretypes.Resource, id string
|
||||
return selectors, nil
|
||||
}
|
||||
|
||||
func QueryRangeResources(ec coretypes.ExtractorContext) ([]coretypes.ResourceWithID, error) {
|
||||
func NewQueryRangeResources(maxDisjuncts int) coretypes.ResourceExtractor {
|
||||
return func(ec coretypes.ExtractorContext) ([]coretypes.ResourceWithID, error) {
|
||||
return queryRangeResources(ec, maxDisjuncts)
|
||||
}
|
||||
}
|
||||
|
||||
func queryRangeResources(ec coretypes.ExtractorContext, maxDisjuncts int) ([]coretypes.ResourceWithID, error) {
|
||||
queries := gjson.GetBytes(ec.RequestBody, "compositeQuery.queries")
|
||||
if !queries.IsArray() || len(queries.Array()) == 0 {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "atleast one query is required")
|
||||
@@ -42,7 +51,7 @@ func QueryRangeResources(ec coretypes.ExtractorContext) ([]coretypes.ResourceWit
|
||||
refs := make([]coretypes.ResourceWithID, 0, len(queries.Array()))
|
||||
seen := make(map[string]struct{})
|
||||
for _, query := range queries.Array() {
|
||||
queryRefs, err := resourcesForQuery(query, variables)
|
||||
queryRefs, err := resourcesForQuery(query, variables, maxDisjuncts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -85,16 +94,16 @@ func PromQLResources(coretypes.ExtractorContext) ([]coretypes.ResourceWithID, er
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func resourcesForQuery(query gjson.Result, variables map[string]qbtypes.VariableItem) ([]coretypes.ResourceWithID, error) {
|
||||
func resourcesForQuery(query gjson.Result, variables map[string]qbtypes.VariableItem, maxDisjuncts int) ([]coretypes.ResourceWithID, error) {
|
||||
queryType := query.Get("type").String()
|
||||
typeWildcard := queryType + "/" + coretypes.WildCardSelectorString
|
||||
|
||||
switch queryType {
|
||||
case qbtypes.QueryTypeBuilder.StringValue(), qbtypes.QueryTypeSubQuery.StringValue():
|
||||
return resourcesForBuilderQuery(queryType, query.Get("spec"), variables)
|
||||
return resourcesForBuilderQuery(queryType, query.Get("spec"), variables, maxDisjuncts)
|
||||
case qbtypes.QueryTypeBuilderAI.StringValue():
|
||||
// always a traces query; the signal may be absent from the payload
|
||||
return builderQueryResourceRefs(queryType, coretypes.ResourceTelemetryResourceTraces, query.Get("spec"), variables)
|
||||
return builderQueryResourceRefs(queryType, coretypes.ResourceTelemetryResourceTraces, query.Get("spec"), variables, maxDisjuncts)
|
||||
case qbtypes.QueryTypePromQL.StringValue():
|
||||
return []coretypes.ResourceWithID{{Resource: coretypes.ResourceTelemetryResourceMetrics, ID: typeWildcard}}, nil
|
||||
case qbtypes.QueryTypeClickHouseSQL.StringValue():
|
||||
@@ -111,16 +120,16 @@ func resourcesForQuery(query gjson.Result, variables map[string]qbtypes.Variable
|
||||
}
|
||||
}
|
||||
|
||||
func resourcesForBuilderQuery(queryType string, spec gjson.Result, variables map[string]qbtypes.VariableItem) ([]coretypes.ResourceWithID, error) {
|
||||
func resourcesForBuilderQuery(queryType string, spec gjson.Result, variables map[string]qbtypes.VariableItem, maxDisjuncts int) ([]coretypes.ResourceWithID, error) {
|
||||
resource, err := builderQueryResource(spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return builderQueryResourceRefs(queryType, resource, spec, variables)
|
||||
return builderQueryResourceRefs(queryType, resource, spec, variables, maxDisjuncts)
|
||||
}
|
||||
|
||||
func builderQueryResourceRefs(queryType string, resource coretypes.Resource, spec gjson.Result, variables map[string]qbtypes.VariableItem) ([]coretypes.ResourceWithID, error) {
|
||||
ids, err := builderQuerySelectors(queryType, spec.Get("filter.expression").String(), variables)
|
||||
func builderQueryResourceRefs(queryType string, resource coretypes.Resource, spec gjson.Result, variables map[string]qbtypes.VariableItem, maxDisjuncts int) ([]coretypes.ResourceWithID, error) {
|
||||
ids, err := builderQuerySelectors(queryType, spec.Get("filter.expression").String(), variables, maxDisjuncts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -154,7 +163,7 @@ func builderQueryResource(spec gjson.Result) (coretypes.Resource, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func builderQuerySelectors(queryType, expression string, variables map[string]qbtypes.VariableItem) ([]string, error) {
|
||||
func builderQuerySelectors(queryType, expression string, variables map[string]qbtypes.VariableItem, maxDisjuncts int) ([]string, error) {
|
||||
typeWildcard := queryType + "/" + coretypes.WildCardSelectorString
|
||||
|
||||
if strings.TrimSpace(expression) == "" {
|
||||
@@ -165,28 +174,101 @@ func builderQuerySelectors(queryType, expression string, variables map[string]qb
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if normalized.Expression == "" {
|
||||
return []string{typeWildcard}, nil
|
||||
}
|
||||
|
||||
ids := make([]string, 0)
|
||||
lists := make([]WhereClauseCondition, 0)
|
||||
seenKeys := make(map[string]struct{})
|
||||
expandable := true
|
||||
disjuncts := 1
|
||||
for _, condition := range normalized.Conditions {
|
||||
if !condition.TopLevel {
|
||||
if !condition.TopLevel || condition.Operator != "IN" {
|
||||
continue
|
||||
}
|
||||
|
||||
key, ok := telemetrytypes.NewTelemetryGrantKey(condition.Key)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
_, dup := seenKeys[key]
|
||||
seenKeys[key] = struct{}{}
|
||||
lists = append(lists, condition)
|
||||
if dup || len(condition.Values) == 0 || disjuncts > maxDisjuncts/len(condition.Values) {
|
||||
expandable = false
|
||||
continue
|
||||
}
|
||||
disjuncts *= len(condition.Values)
|
||||
}
|
||||
|
||||
if condition.Operator == "=" || condition.Operator == "IN" {
|
||||
for _, value := range condition.Values {
|
||||
ids = append(ids, queryType+"/"+key+"/"+value)
|
||||
if len(lists) == 0 || !expandable {
|
||||
return []string{queryType + "/" + normalized.Expression}, nil
|
||||
}
|
||||
|
||||
ids := make([]string, 0, disjuncts)
|
||||
indices := make([]int, len(lists))
|
||||
for {
|
||||
pins := make(map[string]string, len(lists))
|
||||
for i, list := range lists {
|
||||
pins[list.Key] = list.Values[indices[i]]
|
||||
}
|
||||
pinned, err := NormalizeWhereClauseWithPins(expression, variables, pins)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids = append(ids, queryType+"/"+pinned.Expression)
|
||||
|
||||
position := len(indices) - 1
|
||||
for ; position >= 0; position-- {
|
||||
indices[position]++
|
||||
if indices[position] < len(lists[position].Values) {
|
||||
break
|
||||
}
|
||||
indices[position] = 0
|
||||
}
|
||||
if position < 0 {
|
||||
return ids, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(ids) == 0 {
|
||||
return []string{typeWildcard}, nil
|
||||
func telemetrySelectorValues(id string) ([]string, error) {
|
||||
queryType, filter, found := strings.Cut(id, "/")
|
||||
if !found || filter == coretypes.WildCardSelectorString {
|
||||
return telemetrytypes.NewTelemetryGrantSelectors(id), nil
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
normalized, err := NormalizeWhereClause(filter, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
values := make([]string, 0)
|
||||
for _, condition := range normalized.Conditions {
|
||||
if !condition.TopLevel {
|
||||
continue
|
||||
}
|
||||
key, ok := telemetrytypes.NewTelemetryGrantKey(condition.Key)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch condition.Operator {
|
||||
case "=":
|
||||
values = append(values, queryType+"/"+key+"/"+condition.Values[0], queryType+"/"+key+"/"+coretypes.WildCardSelectorString)
|
||||
case "IN":
|
||||
values = append(values, queryType+"/"+key+"/"+coretypes.WildCardSelectorString)
|
||||
}
|
||||
}
|
||||
values = append(values, queryType+"/"+coretypes.WildCardSelectorString, coretypes.WildCardSelectorString)
|
||||
|
||||
seen := make(map[string]struct{}, len(values))
|
||||
unique := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
if _, ok := seen[value]; ok {
|
||||
continue
|
||||
}
|
||||
seen[value] = struct{}{}
|
||||
unique = append(unique, value)
|
||||
}
|
||||
|
||||
return unique, nil
|
||||
}
|
||||
|
||||
@@ -24,30 +24,90 @@ func TestQueryRangeResources(t *testing.T) {
|
||||
name: "top level key equality",
|
||||
body: builderQueryBody("logs", "signoz.workspace.key.id = 'checkout' AND status = 500"),
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/signoz.workspace.key.id/checkout"},
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/signoz.workspace.key.id = 'checkout' AND status = 500"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "resource prefixed key",
|
||||
body: builderQueryBody("traces", "resource.signoz.workspace.key.id = 'checkout'"),
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceTraces, ID: "builder_query/signoz.workspace.key.id/checkout"},
|
||||
{Resource: coretypes.ResourceTelemetryResourceTraces, ID: "builder_query/resource.signoz.workspace.key.id = 'checkout'"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "in atom requires every value",
|
||||
name: "in atom is a union of one resource per value",
|
||||
body: builderQueryBody("logs", "signoz.workspace.key.id IN ('b', 'a')"),
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/signoz.workspace.key.id/a"},
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/signoz.workspace.key.id/b"},
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/signoz.workspace.key.id = 'a'"},
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/signoz.workspace.key.id = 'b'"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple equality atoms each require a grant",
|
||||
name: "conjunction is one resource",
|
||||
body: builderQueryBody("logs", "signoz.workspace.key.id = 'b' AND signoz.workspace.key.id = 'a'"),
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/signoz.workspace.key.id/a"},
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/signoz.workspace.key.id/b"},
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/signoz.workspace.key.id = 'a' AND signoz.workspace.key.id = 'b'"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "atoms on different keys are one resource",
|
||||
body: builderQueryBody("logs", "resource.service.name = 'checkout' AND resource.deployment.environment.name = 'prod'"),
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/resource.deployment.environment.name = 'prod' AND resource.service.name = 'checkout'"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list with another atom expands into disjuncts",
|
||||
body: builderQueryBody("logs", "resource.service.name IN ('b', 'a') AND signoz.workspace.key.id = 'k'"),
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/resource.service.name = 'a' AND signoz.workspace.key.id = 'k'"},
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/resource.service.name = 'b' AND signoz.workspace.key.id = 'k'"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "two lists expand into every combination",
|
||||
body: builderQueryBody("logs", "resource.service.name IN ('a', 'b') AND signoz.workspace.key.id IN ('y', 'z')"),
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/resource.service.name = 'a' AND signoz.workspace.key.id = 'y'"},
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/resource.service.name = 'a' AND signoz.workspace.key.id = 'z'"},
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/resource.service.name = 'b' AND signoz.workspace.key.id = 'y'"},
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/resource.service.name = 'b' AND signoz.workspace.key.id = 'z'"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "repeated list expands like a single one",
|
||||
body: builderQueryBody("logs", "resource.service.name IN ('checkout') AND resource.service.name IN ('checkout')"),
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/resource.service.name = 'checkout'"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "two lists on one key stay one resource",
|
||||
body: builderQueryBody("logs", "signoz.workspace.key.id IN ('a', 'b') AND resource.signoz.workspace.key.id IN ('b', 'c')"),
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/resource.signoz.workspace.key.id IN ('b', 'c') AND signoz.workspace.key.id IN ('a', 'b')"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "old environment spelling is part of the id but not a selector",
|
||||
body: builderQueryBody("traces", "resource.deployment.environment = 'prod'"),
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceTraces, ID: "builder_query/resource.deployment.environment = 'prod'"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "service values in list",
|
||||
body: builderQueryBody("logs", "resource.service.name IN ('frontend', 'checkout')"),
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/resource.service.name = 'checkout'"},
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/resource.service.name = 'frontend'"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bare customer key is part of the id but not a selector",
|
||||
body: builderQueryBody("logs", "service.name = 'checkout'"),
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/service.name = 'checkout'"},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -58,45 +118,38 @@ func TestQueryRangeResources(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "key atom under or does not qualify",
|
||||
name: "or keeps the whole filter as the id",
|
||||
body: builderQueryBody("logs", "signoz.workspace.key.id = 'a' OR status = 500"),
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/*"},
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/signoz.workspace.key.id = 'a' OR status = 500"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "negated key atom does not qualify",
|
||||
body: builderQueryBody("logs", "NOT signoz.workspace.key.id = 'a'"),
|
||||
name: "list under or is not expanded",
|
||||
body: builderQueryBody("logs", "signoz.workspace.key.id IN ('a', 'b') OR status = 500"),
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/*"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "key inequality does not qualify",
|
||||
body: builderQueryBody("logs", "signoz.workspace.key.id != 'a'"),
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/*"},
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/signoz.workspace.key.id IN ('a', 'b') OR status = 500"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "value with spaces and slashes stays plaintext in the id",
|
||||
body: builderQueryBody("logs", "signoz.workspace.key.id = 'check out/2'"),
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/signoz.workspace.key.id/check out/2"},
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/signoz.workspace.key.id = 'check out/2'"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "audit source maps to audit logs resource",
|
||||
body: `{"compositeQuery":{"queries":[{"type":"builder_query","spec":{"signal":"logs","source":"audit","filter":{"expression":"signoz.workspace.key.id = 'a'"}}}]}}`,
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceAuditLogs, ID: "builder_query/signoz.workspace.key.id/a"},
|
||||
{Resource: coretypes.ResourceTelemetryResourceAuditLogs, ID: "builder_query/signoz.workspace.key.id = 'a'"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ai builder query maps to traces resource without a signal",
|
||||
body: `{"compositeQuery":{"queries":[{"type":"builder_ai_query","spec":{"filter":{"expression":"signoz.workspace.key.id = 'checkout'"}}}]}}`,
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceTraces, ID: "builder_ai_query/signoz.workspace.key.id/checkout"},
|
||||
{Resource: coretypes.ResourceTelemetryResourceTraces, ID: "builder_ai_query/signoz.workspace.key.id = 'checkout'"},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -132,28 +185,29 @@ func TestQueryRangeResources(t *testing.T) {
|
||||
name: "trace operator rides on its referenced queries",
|
||||
body: `{"compositeQuery":{"queries":[{"type":"builder_query","spec":{"name":"A","signal":"traces","disabled":true,"filter":{"expression":"signoz.workspace.key.id = 'checkout'"}}},{"type":"builder_query","spec":{"name":"B","signal":"traces","disabled":true,"filter":{"expression":"signoz.workspace.key.id = 'checkout' AND has_error = true"}}},{"type":"builder_trace_operator","spec":{"name":"T1","expression":"A => B","returnSpansFrom":"A"}}]}}`,
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceTraces, ID: "builder_query/signoz.workspace.key.id/checkout"},
|
||||
{Resource: coretypes.ResourceTelemetryResourceTraces, ID: "builder_query/signoz.workspace.key.id = 'checkout'"},
|
||||
{Resource: coretypes.ResourceTelemetryResourceTraces, ID: "builder_query/has_error = true AND signoz.workspace.key.id = 'checkout'"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "variable substitution qualifies",
|
||||
body: `{"variables":{"key":{"value":"checkout"}},"compositeQuery":{"queries":[{"type":"builder_query","spec":{"signal":"logs","filter":{"expression":"signoz.workspace.key.id = $key"}}}]}}`,
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/signoz.workspace.key.id/checkout"},
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/signoz.workspace.key.id = 'checkout'"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "duplicate queries dedupe",
|
||||
body: `{"compositeQuery":{"queries":[{"type":"builder_query","spec":{"signal":"logs","filter":{"expression":"signoz.workspace.key.id = 'a'"}}},{"type":"builder_query","spec":{"signal":"logs","filter":{"expression":"signoz.workspace.key.id='a'"}}}]}}`,
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/signoz.workspace.key.id/a"},
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/signoz.workspace.key.id = 'a'"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
refs, err := QueryRangeResources(coretypes.ExtractorContext{RequestBody: []byte(testCase.body)})
|
||||
refs, err := NewQueryRangeResources(64)(coretypes.ExtractorContext{RequestBody: []byte(testCase.body)})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testCase.expected, refs)
|
||||
})
|
||||
@@ -170,7 +224,7 @@ func TestQueryRangeResourcesErrors(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, body := range bodies {
|
||||
_, err := QueryRangeResources(coretypes.ExtractorContext{RequestBody: []byte(body)})
|
||||
_, err := NewQueryRangeResources(64)(coretypes.ExtractorContext{RequestBody: []byte(body)})
|
||||
assert.Error(t, err, "body %s", body)
|
||||
}
|
||||
}
|
||||
@@ -188,10 +242,43 @@ func TestTelemetrySelector(t *testing.T) {
|
||||
return values
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"builder_query/signoz.workspace.key.id/a", "builder_query/signoz.workspace.key.id/*", "builder_query/*", "*"}, selectorValues("builder_query/signoz.workspace.key.id/a"))
|
||||
assert.Equal(t, []string{"builder_query/signoz.workspace.key.id/a", "builder_query/signoz.workspace.key.id/*", "builder_query/*", "*"}, selectorValues("builder_query/signoz.workspace.key.id = 'a'"))
|
||||
assert.Equal(t, []string{"builder_query/signoz.workspace.key.id/a", "builder_query/signoz.workspace.key.id/*", "builder_query/*", "*"}, selectorValues("builder_query/resource.signoz.workspace.key.id = 'a' AND status = 500"))
|
||||
assert.Equal(t, []string{
|
||||
"builder_query/resource.service.name/x",
|
||||
"builder_query/resource.service.name/*",
|
||||
"builder_query/signoz.workspace.key.id/z",
|
||||
"builder_query/signoz.workspace.key.id/*",
|
||||
"builder_query/*",
|
||||
"*",
|
||||
}, selectorValues("builder_query/resource.service.name = 'x' AND signoz.workspace.key.id = 'z'"))
|
||||
assert.Equal(t, []string{"builder_query/signoz.workspace.key.id/*", "builder_query/*", "*"}, selectorValues("builder_query/signoz.workspace.key.id IN ('a', 'b')"))
|
||||
assert.Equal(t, []string{"builder_query/*", "*"}, selectorValues("builder_query/signoz.workspace.key.id = 'a' OR status = 500"))
|
||||
assert.Equal(t, []string{"builder_query/*", "*"}, selectorValues("builder_query/service.name = 'x'"))
|
||||
assert.Equal(t, []string{"builder_query/signoz.workspace.key.id/a/b", "builder_query/signoz.workspace.key.id/*", "builder_query/*", "*"}, selectorValues("builder_query/signoz.workspace.key.id = 'a/b'"))
|
||||
assert.Equal(t, []string{"builder_query/*", "*"}, selectorValues("builder_query/*"))
|
||||
assert.Equal(t, []string{"builder_query/*", "*"}, selectorValues("builder_query"))
|
||||
assert.Equal(t, []string{"promql/*", "*"}, selectorValues("promql"))
|
||||
assert.Equal(t, []string{"promql/*", "*"}, selectorValues("promql/*"))
|
||||
assert.Equal(t, []string{"*"}, selectorValues("*"))
|
||||
// a value containing "/" stays one logical segment (SplitN 3).
|
||||
assert.Equal(t, []string{"builder_query/signoz.workspace.key.id/a/b", "builder_query/signoz.workspace.key.id/*", "builder_query/*", "*"}, selectorValues("builder_query/signoz.workspace.key.id/a/b"))
|
||||
|
||||
_, err := TelemetrySelector(context.Background(), coretypes.ResourceTelemetryResourceLogs, "builder_query/signoz.workspace.key.id = ", orgID)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestQueryRangeResourcesDisjunctCap(t *testing.T) {
|
||||
body := []byte(builderQueryBody("logs", "resource.service.name IN ('a', 'b') AND signoz.workspace.key.id IN ('y', 'z')"))
|
||||
|
||||
expanded, err := NewQueryRangeResources(4)(coretypes.ExtractorContext{RequestBody: body})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, expanded, 4)
|
||||
|
||||
capped, err := NewQueryRangeResources(3)(coretypes.ExtractorContext{RequestBody: body})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, capped, 1)
|
||||
assert.Equal(t, "builder_query/resource.service.name IN ('a', 'b') AND signoz.workspace.key.id IN ('y', 'z')", capped[0].ID)
|
||||
|
||||
tight, err := NewQueryRangeResources(1)(coretypes.ExtractorContext{RequestBody: []byte(builderQueryBody("logs", "resource.service.name IN ('a', 'b')"))})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, tight, 1)
|
||||
assert.Equal(t, "builder_query/resource.service.name IN ('a', 'b')", tight[0].ID)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package querybuilder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -39,6 +40,7 @@ const (
|
||||
type normalizedPart struct {
|
||||
text string
|
||||
join joinKind
|
||||
negated bool
|
||||
skipped bool
|
||||
}
|
||||
|
||||
@@ -49,6 +51,7 @@ type normalizedValue struct {
|
||||
|
||||
type whereClauseNormalizer struct {
|
||||
variables map[string]qbtypes.VariableItem
|
||||
pins map[string]string
|
||||
conditions []WhereClauseCondition
|
||||
negated bool
|
||||
orDepth int
|
||||
@@ -56,6 +59,15 @@ type whereClauseNormalizer struct {
|
||||
}
|
||||
|
||||
func NormalizeWhereClause(expression string, variables map[string]qbtypes.VariableItem) (*NormalizedWhereClause, error) {
|
||||
return normalizeWhereClause(expression, variables, nil)
|
||||
}
|
||||
|
||||
// NormalizeWhereClauseWithPins renders a top-level IN on a pinned key as an equality on the pinned value.
|
||||
func NormalizeWhereClauseWithPins(expression string, variables map[string]qbtypes.VariableItem, pins map[string]string) (*NormalizedWhereClause, error) {
|
||||
return normalizeWhereClause(expression, variables, pins)
|
||||
}
|
||||
|
||||
func normalizeWhereClause(expression string, variables map[string]qbtypes.VariableItem, pins map[string]string) (*NormalizedWhereClause, error) {
|
||||
input := antlr.NewInputStream(expression)
|
||||
lexer := grammar.NewFilterQueryLexer(input)
|
||||
|
||||
@@ -92,6 +104,7 @@ func NormalizeWhereClause(expression string, variables map[string]qbtypes.Variab
|
||||
|
||||
visitor := &whereClauseNormalizer{
|
||||
variables: variables,
|
||||
pins: pins,
|
||||
conditions: make([]WhereClauseCondition, 0),
|
||||
}
|
||||
part := visitor.visitQuery(tree)
|
||||
@@ -113,8 +126,11 @@ func NormalizeWhereClause(expression string, variables map[string]qbtypes.Variab
|
||||
sort.Slice(visitor.conditions, func(i, j int) bool {
|
||||
return visitor.conditions[i].sortKey() < visitor.conditions[j].sortKey()
|
||||
})
|
||||
conditions := slices.CompactFunc(visitor.conditions, func(left, right WhereClauseCondition) bool {
|
||||
return left.sortKey() == right.sortKey()
|
||||
})
|
||||
|
||||
return &NormalizedWhereClause{Expression: part.text, Conditions: visitor.conditions}, nil
|
||||
return &NormalizedWhereClause{Expression: part.text, Conditions: conditions}, nil
|
||||
}
|
||||
|
||||
func (condition WhereClauseCondition) sortKey() string {
|
||||
@@ -207,10 +223,10 @@ func (visitor *whereClauseNormalizer) visitUnaryExpression(ctx grammar.IUnaryExp
|
||||
if part.skipped {
|
||||
return part
|
||||
}
|
||||
if part.join != joinKindNone {
|
||||
return normalizedPart{text: "NOT (" + part.text + ")", join: joinKindNone}
|
||||
if part.join != joinKindNone || part.negated {
|
||||
return normalizedPart{text: "NOT (" + part.text + ")", negated: true}
|
||||
}
|
||||
return normalizedPart{text: "NOT " + part.text, join: joinKindNone}
|
||||
return normalizedPart{text: "NOT " + part.text, negated: true}
|
||||
}
|
||||
|
||||
return part
|
||||
@@ -315,6 +331,11 @@ func (visitor *whereClauseNormalizer) visitInComparison(key, operator string, va
|
||||
return normalizedPart{skipped: true}
|
||||
}
|
||||
|
||||
if pinned, ok := visitor.pins[key]; ok && operator == "IN" && visitor.orDepth == 0 && !visitor.negated {
|
||||
visitor.appendCondition(key, "=", []string{pinned})
|
||||
return normalizedPart{text: key + " = " + FilterStringLiteral(pinned)}
|
||||
}
|
||||
|
||||
sort.Slice(values, func(i, j int) bool { return values[i].text < values[j].text })
|
||||
|
||||
texts := make([]string, 0, len(values))
|
||||
|
||||
@@ -25,6 +25,15 @@ func TestNormalizeWhereClauseEquivalenceClasses(t *testing.T) {
|
||||
},
|
||||
expected: "service.name = 'frontend' AND status = 200",
|
||||
},
|
||||
{
|
||||
name: "nested not keeps its parentheses",
|
||||
expressions: []string{
|
||||
"NOT (NOT a = 1)",
|
||||
"NOT(NOT a=1)",
|
||||
"not (not a = 1)",
|
||||
},
|
||||
expected: "NOT (NOT a = 1)",
|
||||
},
|
||||
{
|
||||
name: "operand order",
|
||||
expressions: []string{
|
||||
@@ -327,6 +336,8 @@ func TestNormalizeWhereClauseIdempotence(t *testing.T) {
|
||||
"a IN [3, 1, 2] AND hasAll(tags, ['a', 'b']) AND body CONTAINS 'x'",
|
||||
"duration BETWEEN 1 AND 10 OR duration > 100",
|
||||
`msg = 'with \'escapes\' and "quotes"'`,
|
||||
"NOT (NOT resource.region = 'us-east')",
|
||||
"NOT (NOT (a = 1 OR b = 2)) AND NOT c EXISTS",
|
||||
}
|
||||
|
||||
for _, expression := range expressions {
|
||||
@@ -439,3 +450,33 @@ func TestFilterStringLiteral(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeWhereClauseDedupesRepeatedConditions(t *testing.T) {
|
||||
normalized, err := NormalizeWhereClause("service.name IN ('checkout') AND service.name IN ('checkout') AND status = 500 AND status = 500", nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "service.name IN ('checkout') AND status = 500", normalized.Expression)
|
||||
assert.Equal(t, []WhereClauseCondition{
|
||||
{Key: "service.name", Operator: "IN", Values: []string{"checkout"}, TopLevel: true},
|
||||
{Key: "status", Operator: "=", Values: []string{"500"}, TopLevel: true},
|
||||
}, normalized.Conditions)
|
||||
}
|
||||
|
||||
func TestNormalizeWhereClauseWithPins(t *testing.T) {
|
||||
pinned, err := NormalizeWhereClauseWithPins(`service.name IN ('b', 'it\'s a') AND status = 500`, nil, map[string]string{"service.name": "it's a"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `service.name = 'it\'s a' AND status = 500`, pinned.Expression)
|
||||
require.Len(t, pinned.Conditions, 2)
|
||||
assert.Equal(t, WhereClauseCondition{Key: "service.name", Operator: "=", Values: []string{"it's a"}, TopLevel: true}, pinned.Conditions[0])
|
||||
|
||||
unpinned, err := NormalizeWhereClauseWithPins("service.name IN ('a', 'b') OR status = 500", nil, map[string]string{"service.name": "a"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "service.name IN ('a', 'b') OR status = 500", unpinned.Expression)
|
||||
|
||||
negated, err := NormalizeWhereClauseWithPins("NOT service.name IN ('a', 'b')", nil, map[string]string{"service.name": "a"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "NOT service.name IN ('a', 'b')", negated.Expression)
|
||||
|
||||
otherKey, err := NormalizeWhereClauseWithPins("host.name IN ('a', 'b')", nil, map[string]string{"service.name": "a"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "host.name IN ('a', 'b')", otherKey.Expression)
|
||||
}
|
||||
|
||||
@@ -107,6 +107,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
|
||||
struct{ statsreporter.Handler }{},
|
||||
struct{ savedview.Handler }{},
|
||||
global.Config{},
|
||||
authz.Config{},
|
||||
struct{ identn.IdentNResolver }{},
|
||||
struct{ sharder.Sharder }{},
|
||||
struct{ auditor.Auditor }{},
|
||||
|
||||
@@ -323,7 +323,7 @@ func NewQuerierProviderFactories(telemetryStore telemetrystore.TelemetryStore, p
|
||||
)
|
||||
}
|
||||
|
||||
func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.AuthZ, modules Modules, handlers Handlers, globalConfig global.Config, gatewayService gateway.Gateway, identNResolver identn.IdentNResolver, sharder sharder.Sharder, auditor auditor.Auditor, web web.Web) factory.NamedMap[factory.ProviderFactory[apiserver.APIServer, apiserver.Config]] {
|
||||
func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.AuthZ, modules Modules, handlers Handlers, globalConfig global.Config, authzConfig authz.Config, gatewayService gateway.Gateway, identNResolver identn.IdentNResolver, sharder sharder.Sharder, auditor auditor.Auditor, web web.Web) factory.NamedMap[factory.ProviderFactory[apiserver.APIServer, apiserver.Config]] {
|
||||
return factory.MustNewNamedMap(
|
||||
signozapiserver.NewFactory(
|
||||
orgGetter,
|
||||
@@ -367,6 +367,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
|
||||
handlers.StatsHandler,
|
||||
handlers.SavedView,
|
||||
globalConfig,
|
||||
authzConfig,
|
||||
identNResolver,
|
||||
sharder,
|
||||
auditor,
|
||||
|
||||
@@ -2,6 +2,7 @@ package signoz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
@@ -101,6 +102,7 @@ func TestNewProviderFactories(t *testing.T) {
|
||||
Modules{},
|
||||
Handlers{},
|
||||
global.Config{},
|
||||
authz.Config{},
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
|
||||
@@ -622,7 +622,7 @@ func New(
|
||||
ctx,
|
||||
providerSettings,
|
||||
config.APIServer,
|
||||
NewAPIServerProviderFactories(orgGetter, authz, modules, handlers, config.Global, gateway, identNResolver, sharder, auditor, web),
|
||||
NewAPIServerProviderFactories(orgGetter, authz, modules, handlers, config.Global, config.Authz, gateway, identNResolver, sharder, auditor, web),
|
||||
"signoz",
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package telemetrytypes
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
@@ -16,8 +17,12 @@ var telemetryGrantQueryTypes = map[string]bool{
|
||||
"clickhouse_sql": false,
|
||||
}
|
||||
|
||||
var telemetryGrantKeys = map[string]struct{}{
|
||||
"signoz.workspace.key.id": {},
|
||||
// signoz.workspace.key.id is stamped on the resource by ingestion and never arrives as an
|
||||
// attribute, so its bare spelling is unambiguous; customer-controlled keys must say resource.
|
||||
var telemetryGrantKeys = map[string]string{
|
||||
"signoz.workspace.key.id": "signoz.workspace.key.id",
|
||||
"service.name": "resource.service.name",
|
||||
"deployment.environment.name": "resource.deployment.environment.name",
|
||||
}
|
||||
|
||||
func NewTelemetryGrantKey(keyText string) (string, bool) {
|
||||
@@ -26,11 +31,16 @@ func NewTelemetryGrantKey(keyText string) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
if _, ok := telemetryGrantKeys[fieldKey.Name]; !ok {
|
||||
canonical, ok := telemetryGrantKeys[fieldKey.Name]
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return fieldKey.Name, true
|
||||
if fieldKey.FieldContext == FieldContextUnspecified && canonical != fieldKey.Name {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return canonical, true
|
||||
}
|
||||
|
||||
func NewTelemetryGrantSelector(input string) (string, error) {
|
||||
@@ -104,8 +114,9 @@ func NewTelemetryGrantSelectors(selector string) []string {
|
||||
|
||||
func telemetryGrantKeyNames() []string {
|
||||
names := make([]string, 0, len(telemetryGrantKeys))
|
||||
for name := range telemetryGrantKeys {
|
||||
names = append(names, name)
|
||||
for _, canonical := range telemetryGrantKeys {
|
||||
names = append(names, canonical)
|
||||
}
|
||||
slices.Sort(names)
|
||||
return names
|
||||
}
|
||||
|
||||
@@ -14,11 +14,14 @@ func TestNewTelemetryGrantSelector(t *testing.T) {
|
||||
"builder_query/*": "builder_query/*",
|
||||
"promql": "promql/*",
|
||||
"clickhouse_sql": "clickhouse_sql/*",
|
||||
"builder_query/signoz.workspace.key.id/*": "builder_query/signoz.workspace.key.id/*",
|
||||
"builder_query/signoz.workspace.key.id/key-a": "builder_query/signoz.workspace.key.id/key-a",
|
||||
"builder_query/resource.signoz.workspace.key.id/key-a": "builder_query/signoz.workspace.key.id/key-a",
|
||||
"builder_query/signoz.workspace.key.id/key a": "builder_query/signoz.workspace.key.id/key a",
|
||||
"builder_query/signoz.workspace.key.id/a/b": "builder_query/signoz.workspace.key.id/a/b",
|
||||
"builder_query/signoz.workspace.key.id/*": "builder_query/signoz.workspace.key.id/*",
|
||||
"builder_query/signoz.workspace.key.id/key-a": "builder_query/signoz.workspace.key.id/key-a",
|
||||
"builder_query/resource.signoz.workspace.key.id/key-a": "builder_query/signoz.workspace.key.id/key-a",
|
||||
"builder_query/signoz.workspace.key.id/key a": "builder_query/signoz.workspace.key.id/key a",
|
||||
"builder_query/signoz.workspace.key.id/a/b": "builder_query/signoz.workspace.key.id/a/b",
|
||||
"builder_query/resource.service.name/frontend": "builder_query/resource.service.name/frontend",
|
||||
"builder_query/resource.service.name/*": "builder_query/resource.service.name/*",
|
||||
"builder_query/resource.deployment.environment.name/prod": "builder_query/resource.deployment.environment.name/prod",
|
||||
}
|
||||
for input, expected := range valid {
|
||||
canonical, err := NewTelemetryGrantSelector(input)
|
||||
@@ -32,6 +35,10 @@ func TestNewTelemetryGrantSelector(t *testing.T) {
|
||||
"signoz.workspace.key.id = 'key-a'",
|
||||
"builder_trace_operator/signoz.workspace.key.id/key-a",
|
||||
"builder_query/service.name/frontend",
|
||||
"builder_query/deployment.environment.name/prod",
|
||||
"builder_query/resource.deployment.environment/prod",
|
||||
"builder_query/attribute.service.name/frontend",
|
||||
"builder_query/host.name/frontend",
|
||||
"builder_query/signoz.workspace.key.id/",
|
||||
"builder_query/signoz.workspace.key.id/$svc",
|
||||
"*/signoz.workspace.key.id/key-a",
|
||||
@@ -48,8 +55,10 @@ func TestNewTelemetryGrantSelector(t *testing.T) {
|
||||
|
||||
func TestNewTelemetryGrantKey(t *testing.T) {
|
||||
valid := map[string]string{
|
||||
"signoz.workspace.key.id": "signoz.workspace.key.id",
|
||||
"resource.signoz.workspace.key.id": "signoz.workspace.key.id",
|
||||
"signoz.workspace.key.id": "signoz.workspace.key.id",
|
||||
"resource.signoz.workspace.key.id": "signoz.workspace.key.id",
|
||||
"resource.service.name": "resource.service.name",
|
||||
"resource.deployment.environment.name": "resource.deployment.environment.name",
|
||||
}
|
||||
for keyText, expected := range valid {
|
||||
key, ok := NewTelemetryGrantKey(keyText)
|
||||
@@ -57,7 +66,7 @@ func TestNewTelemetryGrantKey(t *testing.T) {
|
||||
assert.Equal(t, expected, key, keyText)
|
||||
}
|
||||
|
||||
for _, keyText := range []string{"service.name", "attribute.signoz.workspace.key.id", "body.signoz.workspace.key.id"} {
|
||||
for _, keyText := range []string{"service.name", "deployment.environment.name", "resource.deployment.environment", "host.name", "attribute.signoz.workspace.key.id", "attribute.service.name", "body.signoz.workspace.key.id"} {
|
||||
_, ok := NewTelemetryGrantKey(keyText)
|
||||
assert.False(t, ok, keyText)
|
||||
}
|
||||
|
||||
@@ -50,7 +50,9 @@ def test_setup(
|
||||
[
|
||||
"signoz.workspace.key.id = 'key-a'", # expression form, not the wire form
|
||||
"unknown_query_type/signoz.workspace.key.id/key-a", # unsupported query type
|
||||
"builder_query/service.name/frontend", # service.name is not a supported grant key
|
||||
"builder_query/service.name/frontend", # customer keys must carry the resource prefix
|
||||
"builder_query/attribute.service.name/frontend", # grant keys are resource attributes only
|
||||
"builder_query/host.name/frontend", # host.name is not a supported grant key
|
||||
"*/signoz.workspace.key.id/key-a", # non-prefix wildcard
|
||||
"builder_query/signoz.workspace.key.id/", # empty value
|
||||
"builder_query/signoz.workspace.key.id", # missing value, not a wildcard
|
||||
@@ -153,7 +155,7 @@ def test_denied_message_names_resource(
|
||||
request_type="raw",
|
||||
)
|
||||
assert response.status_code == HTTPStatus.FORBIDDEN, response.text
|
||||
assert "builder_query/signoz.workspace.key.id/key-b" in response.text
|
||||
assert "builder_query/signoz.workspace.key.id = 'key-b'" in response.text
|
||||
|
||||
|
||||
def test_variables_resolve_into_gate(
|
||||
|
||||
126
tests/integration/tests/querierauthz/06_multi_key.py
Normal file
126
tests/integration/tests/querierauthz/06_multi_key.py
Normal file
@@ -0,0 +1,126 @@
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD, change_user_role, create_active_user
|
||||
from fixtures.logs import Logs
|
||||
from fixtures.querier import build_raw_query, make_query_request
|
||||
from fixtures.role import transaction_group
|
||||
|
||||
user_password = "password123Z$"
|
||||
service_role = "telemetry-scope-service"
|
||||
service_email = "scope-service@telemetry.test"
|
||||
env_role = "telemetry-scope-env"
|
||||
env_email = "scope-env@telemetry.test"
|
||||
two_services_role = "telemetry-scope-two-services"
|
||||
two_services_email = "scope-two-services@telemetry.test"
|
||||
two_keys_role = "telemetry-scope-two-keys"
|
||||
two_keys_email = "scope-two-keys@telemetry.test"
|
||||
mixed_role = "telemetry-scope-mixed"
|
||||
mixed_email = "scope-mixed@telemetry.test"
|
||||
|
||||
two_lists = "resource.service.name IN ('checkout', 'frontend') AND signoz.workspace.key.id IN ('k1', 'k2')"
|
||||
|
||||
|
||||
def test_setup(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
create_role: Callable[..., str],
|
||||
) -> None:
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
for role, email, selectors in [
|
||||
(service_role, service_email, ["builder_query/resource.service.name/checkout"]),
|
||||
(env_role, env_email, ["builder_query/resource.deployment.environment.name/prod"]),
|
||||
(two_services_role, two_services_email, ["builder_query/resource.service.name/checkout", "builder_query/resource.service.name/frontend"]),
|
||||
(two_keys_role, two_keys_email, ["builder_query/signoz.workspace.key.id/k1", "builder_query/signoz.workspace.key.id/k2"]),
|
||||
(mixed_role, mixed_email, ["builder_query/resource.service.name/checkout", "builder_query/signoz.workspace.key.id/k2"]),
|
||||
]:
|
||||
create_role(admin_token, role, [transaction_group("read", "telemetryresource", "logs", selectors)])
|
||||
user_id = create_active_user(signoz, admin_token, email=email, role="signoz-viewer", password=user_password)
|
||||
change_user_role(signoz, admin_token, user_id, "signoz-viewer", role)
|
||||
|
||||
|
||||
@pytest.fixture(name="seed_logs")
|
||||
def seed_logs_fixture(insert_logs: Callable[[list[Logs]], None]) -> datetime:
|
||||
now = datetime.now(tz=UTC)
|
||||
insert_logs(
|
||||
[
|
||||
Logs(timestamp=now - timedelta(seconds=1), resources={"service.name": "checkout", "signoz.workspace.key.id": "k1", "deployment.environment.name": "prod"}, body="checkout-k1"),
|
||||
Logs(timestamp=now - timedelta(seconds=2), resources={"service.name": "frontend", "signoz.workspace.key.id": "k2", "deployment.environment.name": "prod"}, body="frontend-k2"),
|
||||
]
|
||||
)
|
||||
return now
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("email", "expression"),
|
||||
[
|
||||
(service_email, "resource.service.name = 'checkout'"),
|
||||
(service_email, "resource.service.name IN ('checkout')"),
|
||||
(service_email, "resource.service.name = 'checkout' AND severity_text = 'ERROR'"),
|
||||
# any granted atom selects the query; the other atoms only narrow it
|
||||
(service_email, "resource.service.name = 'checkout' AND resource.deployment.environment.name = 'prod'"),
|
||||
(env_email, "resource.service.name = 'checkout' AND resource.deployment.environment.name = 'prod'"),
|
||||
(service_email, "resource.service.name IN ('checkout') AND signoz.workspace.key.id IN ('k1', 'k2')"),
|
||||
# a list is a union: every value, or every value of the other list, must be granted
|
||||
(two_services_email, two_lists),
|
||||
(two_keys_email, two_lists),
|
||||
],
|
||||
)
|
||||
def test_allowed(
|
||||
signoz: types.SigNoz,
|
||||
get_token: Callable[[str, str], str],
|
||||
seed_logs: datetime,
|
||||
email: str,
|
||||
expression: str,
|
||||
) -> None:
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
get_token(email, user_password),
|
||||
int((seed_logs - timedelta(minutes=10)).timestamp() * 1000),
|
||||
int(seed_logs.timestamp() * 1000),
|
||||
[build_raw_query("A", "logs", limit=50, filter_expression=expression)],
|
||||
request_type="raw",
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("email", "expression", "denied_resource"),
|
||||
[
|
||||
(service_email, "service.name = 'checkout'", "builder_query/service.name = 'checkout'"), # bare customer key never selects
|
||||
(service_email, "resource.deployment.environment.name = 'prod'", "builder_query/resource.deployment.environment.name = 'prod'"),
|
||||
(service_email, "resource.service.name = 'frontend'", "builder_query/resource.service.name = 'frontend'"),
|
||||
(env_email, "resource.service.name = 'checkout'", "builder_query/resource.service.name = 'checkout'"),
|
||||
(
|
||||
service_email,
|
||||
"resource.service.name IN ('checkout', 'frontend') AND resource.deployment.environment.name = 'prod'",
|
||||
"builder_query/resource.deployment.environment.name = 'prod' AND resource.service.name = 'frontend'",
|
||||
),
|
||||
# one service and one key leave the (frontend, k1) slice unselected
|
||||
(mixed_email, two_lists, "builder_query/resource.service.name = 'frontend' AND signoz.workspace.key.id = 'k1'"),
|
||||
],
|
||||
)
|
||||
def test_denied(
|
||||
signoz: types.SigNoz,
|
||||
get_token: Callable[[str, str], str],
|
||||
seed_logs: datetime,
|
||||
email: str,
|
||||
expression: str,
|
||||
denied_resource: str,
|
||||
) -> None:
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
get_token(email, user_password),
|
||||
int((seed_logs - timedelta(minutes=10)).timestamp() * 1000),
|
||||
int(seed_logs.timestamp() * 1000),
|
||||
[build_raw_query("A", "logs", limit=50, filter_expression=expression)],
|
||||
request_type="raw",
|
||||
)
|
||||
assert response.status_code == HTTPStatus.FORBIDDEN, response.text
|
||||
assert denied_resource in response.text
|
||||
Reference in New Issue
Block a user