mirror of
https://github.com/SigNoz/signoz.git
synced 2026-10-04 17:20:40 +01:00
Compare commits
1 Commits
main
...
keystone-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f51075634 |
@@ -146,13 +146,69 @@ 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-service.name-logs-read'),
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('wizard-selector-input-logs-read')).toHaveValue(
|
||||
'builder_query/service.name/checkout',
|
||||
);
|
||||
|
||||
await user.click(screen.getByTestId('wizard-add-btn-logs-read'));
|
||||
|
||||
await expect(
|
||||
screen.findByText('builder_query/service.name/checkout'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
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/deployment.environment/prod');
|
||||
|
||||
expect(screen.getByTestId('wizard-key-select-logs-read')).toHaveTextContent(
|
||||
'deployment.environment',
|
||||
);
|
||||
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 +219,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 +341,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,27 @@ 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',
|
||||
'service.name',
|
||||
'deployment.environment',
|
||||
'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,10 @@ export function isAnyResourceValue(value: string): boolean {
|
||||
return value.trim() === ANY_RESOURCE_VALUE;
|
||||
}
|
||||
|
||||
export function isSupportedGrantKey(key: string): boolean {
|
||||
return SUPPORTED_GRANT_KEYS.includes(key);
|
||||
}
|
||||
|
||||
function splitSelector(selector: string): string[] {
|
||||
const parts = selector.split('/');
|
||||
|
||||
@@ -47,14 +52,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 +71,7 @@ export function parseSelector(selector: string): ParsedSelector {
|
||||
|
||||
return {
|
||||
queryType: getQueryTypeOption(parts[0])?.id,
|
||||
key: parts.length >= 3 ? parts[1] : undefined,
|
||||
value: parts.length >= 3 ? parts[2] : '',
|
||||
};
|
||||
}
|
||||
@@ -101,7 +111,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 +140,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 +156,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,
|
||||
|
||||
@@ -50,6 +50,29 @@ func TestQueryRangeResources(t *testing.T) {
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/signoz.workspace.key.id/b"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "atoms on different keys each require a grant",
|
||||
body: builderQueryBody("logs", "service.name = 'checkout' AND deployment.environment = 'prod'"),
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/deployment.environment/prod"},
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/service.name/checkout"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "environment keys are independent",
|
||||
body: builderQueryBody("traces", "deployment.environment.name = 'prod'"),
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceTraces, ID: "builder_query/deployment.environment.name/prod"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "resource prefixed service in list",
|
||||
body: builderQueryBody("logs", "resource.service.name IN ('frontend', 'checkout')"),
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/service.name/checkout"},
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/service.name/frontend"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no filter expression",
|
||||
body: `{"compositeQuery":{"queries":[{"type":"builder_query","spec":{"signal":"logs"}}]}}`,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package telemetrytypes
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
@@ -17,7 +18,10 @@ var telemetryGrantQueryTypes = map[string]bool{
|
||||
}
|
||||
|
||||
var telemetryGrantKeys = map[string]struct{}{
|
||||
"signoz.workspace.key.id": {},
|
||||
"signoz.workspace.key.id": {},
|
||||
"service.name": {},
|
||||
"deployment.environment": {},
|
||||
"deployment.environment.name": {},
|
||||
}
|
||||
|
||||
func NewTelemetryGrantKey(keyText string) (string, bool) {
|
||||
@@ -107,5 +111,6 @@ func telemetryGrantKeyNames() []string {
|
||||
for name := range telemetryGrantKeys {
|
||||
names = append(names, name)
|
||||
}
|
||||
slices.Sort(names)
|
||||
return names
|
||||
}
|
||||
|
||||
@@ -19,6 +19,12 @@ func TestNewTelemetryGrantSelector(t *testing.T) {
|
||||
"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/service.name/frontend": "builder_query/service.name/frontend",
|
||||
"builder_query/resource.service.name/frontend": "builder_query/service.name/frontend",
|
||||
"builder_query/service.name/*": "builder_query/service.name/*",
|
||||
"builder_query/deployment.environment/prod": "builder_query/deployment.environment/prod",
|
||||
"builder_query/deployment.environment.name/prod": "builder_query/deployment.environment.name/prod",
|
||||
"builder_query/resource.deployment.environment/prod": "builder_query/deployment.environment/prod",
|
||||
}
|
||||
for input, expected := range valid {
|
||||
canonical, err := NewTelemetryGrantSelector(input)
|
||||
@@ -31,7 +37,8 @@ func TestNewTelemetryGrantSelector(t *testing.T) {
|
||||
"key-a",
|
||||
"signoz.workspace.key.id = 'key-a'",
|
||||
"builder_trace_operator/signoz.workspace.key.id/key-a",
|
||||
"builder_query/service.name/frontend",
|
||||
"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,13 @@ 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",
|
||||
"service.name": "service.name",
|
||||
"resource.service.name": "service.name",
|
||||
"deployment.environment": "deployment.environment",
|
||||
"deployment.environment.name": "deployment.environment.name",
|
||||
"resource.deployment.environment.name": "deployment.environment.name",
|
||||
}
|
||||
for keyText, expected := range valid {
|
||||
key, ok := NewTelemetryGrantKey(keyText)
|
||||
@@ -57,7 +69,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{"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,8 @@ 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/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
tests/integration/tests/querierauthz/06_multi_key.py
Normal file
153
tests/integration/tests/querierauthz/06_multi_key.py
Normal file
@@ -0,0 +1,153 @@
|
||||
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"
|
||||
service_env_role = "telemetry-scope-service-env"
|
||||
service_env_email = "scope-service-env@telemetry.test"
|
||||
env_name_role = "telemetry-scope-env-name"
|
||||
env_name_email = "scope-env-name@telemetry.test"
|
||||
|
||||
seed_resources = {"service.name": "checkout", "deployment.environment": "prod", "deployment.environment.name": "prod"}
|
||||
|
||||
|
||||
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)
|
||||
|
||||
create_role(admin_token, service_role, [transaction_group("read", "telemetryresource", "logs", ["builder_query/service.name/checkout"])])
|
||||
service_user = create_active_user(signoz, admin_token, email=service_email, role="signoz-viewer", password=user_password)
|
||||
change_user_role(signoz, admin_token, service_user, "signoz-viewer", service_role)
|
||||
|
||||
create_role(
|
||||
admin_token,
|
||||
service_env_role,
|
||||
[transaction_group("read", "telemetryresource", "logs", ["builder_query/service.name/checkout", "builder_query/deployment.environment/prod"])],
|
||||
)
|
||||
service_env_user = create_active_user(signoz, admin_token, email=service_env_email, role="signoz-viewer", password=user_password)
|
||||
change_user_role(signoz, admin_token, service_env_user, "signoz-viewer", service_env_role)
|
||||
|
||||
create_role(admin_token, env_name_role, [transaction_group("read", "telemetryresource", "logs", ["builder_query/deployment.environment.name/prod"])])
|
||||
env_name_user = create_active_user(signoz, admin_token, email=env_name_email, role="signoz-viewer", password=user_password)
|
||||
change_user_role(signoz, admin_token, env_name_user, "signoz-viewer", env_name_role)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"expression",
|
||||
[
|
||||
"service.name = 'checkout'",
|
||||
"resource.service.name = 'checkout'",
|
||||
"service.name IN ('checkout')",
|
||||
"service.name = 'checkout' AND severity_text = 'ERROR'",
|
||||
],
|
||||
)
|
||||
def test_service_grant_allows_service_filter(
|
||||
signoz: types.SigNoz,
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[list[Logs]], None],
|
||||
expression: str,
|
||||
) -> None:
|
||||
now = datetime.now(tz=UTC)
|
||||
insert_logs([Logs(timestamp=now - timedelta(seconds=1), resources=seed_resources, body="checkout-0")])
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
get_token(service_email, user_password),
|
||||
int((now - timedelta(minutes=10)).timestamp() * 1000),
|
||||
int(now.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(
|
||||
("expression", "denied_resource"),
|
||||
[
|
||||
("deployment.environment = 'prod'", "builder_query/deployment.environment/prod"),
|
||||
# every top-level grant-key atom needs its own grant, so narrowing by a second key is denied
|
||||
("service.name = 'checkout' AND deployment.environment = 'prod'", "builder_query/deployment.environment/prod"),
|
||||
("service.name = 'frontend'", "builder_query/service.name/frontend"),
|
||||
],
|
||||
)
|
||||
def test_service_grant_denies_other_keys(
|
||||
signoz: types.SigNoz,
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[list[Logs]], None],
|
||||
expression: str,
|
||||
denied_resource: str,
|
||||
) -> None:
|
||||
now = datetime.now(tz=UTC)
|
||||
insert_logs([Logs(timestamp=now - timedelta(seconds=1), resources=seed_resources, body="checkout-0")])
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
get_token(service_email, user_password),
|
||||
int((now - timedelta(minutes=10)).timestamp() * 1000),
|
||||
int(now.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
|
||||
|
||||
|
||||
def test_grants_on_both_keys_allow_conjunction(
|
||||
signoz: types.SigNoz,
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[list[Logs]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=UTC)
|
||||
insert_logs([Logs(timestamp=now - timedelta(seconds=1), resources=seed_resources, body="checkout-0")])
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
get_token(service_env_email, user_password),
|
||||
int((now - timedelta(minutes=10)).timestamp() * 1000),
|
||||
int(now.timestamp() * 1000),
|
||||
[build_raw_query("A", "logs", limit=50, filter_expression="service.name = 'checkout' AND deployment.environment = 'prod'")],
|
||||
request_type="raw",
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("expression", "status"),
|
||||
[
|
||||
("deployment.environment.name = 'prod'", HTTPStatus.OK),
|
||||
("deployment.environment = 'prod'", HTTPStatus.FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_environment_keys_are_independent(
|
||||
signoz: types.SigNoz,
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[list[Logs]], None],
|
||||
expression: str,
|
||||
status: HTTPStatus,
|
||||
) -> None:
|
||||
now = datetime.now(tz=UTC)
|
||||
insert_logs([Logs(timestamp=now - timedelta(seconds=1), resources=seed_resources, body="checkout-0")])
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
get_token(env_name_email, user_password),
|
||||
int((now - timedelta(minutes=10)).timestamp() * 1000),
|
||||
int(now.timestamp() * 1000),
|
||||
[build_raw_query("A", "logs", limit=50, filter_expression=expression)],
|
||||
request_type="raw",
|
||||
)
|
||||
assert response.status_code == status, response.text
|
||||
Reference in New Issue
Block a user