mirror of
https://github.com/SigNoz/signoz.git
synced 2026-08-12 16:00:46 +01:00
Compare commits
5 Commits
feat/impro
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16849967c5 | ||
|
|
c8e7685f06 | ||
|
|
a3caaaf7f2 | ||
|
|
a355996a5d | ||
|
|
eea11972a9 |
@@ -10,6 +10,10 @@ jest.mock('providers/Timezone', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('providers/App/App', () => ({
|
||||
useAppContext: (): { featureFlags: [] } => ({ featureFlags: [] }),
|
||||
}));
|
||||
|
||||
const field = (name: string, type = ''): IField => ({
|
||||
name,
|
||||
type,
|
||||
|
||||
@@ -2,13 +2,15 @@ import type { ReactElement } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import {
|
||||
getBodyDisplayString,
|
||||
getSanitizedLogBody,
|
||||
} from 'container/LogDetailedView/utils';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import { getLogFieldValue } from 'lib/logs/flatLogData';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
@@ -26,6 +28,10 @@ export function useLogsTableColumns({
|
||||
fontSize,
|
||||
}: UseLogsTableColumnsProps): TableColumnDef<ILog>[] {
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
const { featureFlags } = useAppContext();
|
||||
const isBodyJsonEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.USE_JSON_BODY)
|
||||
?.active || false;
|
||||
|
||||
return useMemo<TableColumnDef<ILog>[]>(() => {
|
||||
const stateIndicatorCol: TableColumnDef<ILog> = {
|
||||
@@ -88,7 +94,8 @@ export function useLogsTableColumns({
|
||||
const makeUserFieldCol = (f: IField): TableColumnDef<ILog> => ({
|
||||
id: buildCompositeKey(f.name, f.type),
|
||||
header: f.name,
|
||||
accessorFn: (log): unknown => FlatLogData(log)[f.name],
|
||||
accessorFn: (log): unknown =>
|
||||
getLogFieldValue(log, f.name, isBodyJsonEnabled),
|
||||
enableRemove: true,
|
||||
width: { min: 192 },
|
||||
cell: ({ value }): ReactElement => (
|
||||
@@ -115,5 +122,5 @@ export function useLogsTableColumns({
|
||||
.filter((c): c is TableColumnDef<ILog> => c !== null);
|
||||
|
||||
return [stateIndicatorCol, ...fieldCols];
|
||||
}, [fields, fontSize, formatTimezoneAdjustedTimestamp]);
|
||||
}, [fields, fontSize, formatTimezoneAdjustedTimestamp, isBodyJsonEnabled]);
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ import {
|
||||
dedupeOptionsByLabel,
|
||||
getFieldContextPrefix,
|
||||
getRecentOptions,
|
||||
isSupportedFunction,
|
||||
renderRecentDeleteButton,
|
||||
} from './utils';
|
||||
|
||||
@@ -1275,11 +1276,13 @@ function QuerySearch({
|
||||
}
|
||||
|
||||
if (queryContext.isInFunction) {
|
||||
options = Object.values(QUERY_BUILDER_FUNCTIONS).map((option) => ({
|
||||
label: option,
|
||||
apply: `${option}()`,
|
||||
type: 'function',
|
||||
}));
|
||||
options = Object.values(QUERY_BUILDER_FUNCTIONS)
|
||||
.filter((option) => isSupportedFunction(option, dataSource))
|
||||
.map((option) => ({
|
||||
label: option,
|
||||
apply: `${option}()`,
|
||||
type: 'function',
|
||||
}));
|
||||
|
||||
// Add space after selection for functions
|
||||
const optionsWithSpace = addSpaceToOptions(options);
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { QUERY_BUILDER_FUNCTIONS } from 'constants/antlrQueryConstants';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
combineInitialAndUserExpression,
|
||||
dedupeOptionsByLabel,
|
||||
getFieldContextPrefix,
|
||||
getUserExpressionFromCombined,
|
||||
isSupportedFunction,
|
||||
} from '../utils';
|
||||
|
||||
describe('entityLogsExpression', () => {
|
||||
@@ -118,3 +122,19 @@ describe('dedupeOptionsByLabel', () => {
|
||||
expect(dedupeOptionsByLabel([])).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSupportedFunction', () => {
|
||||
const { HASANY, SEARCH } = QUERY_BUILDER_FUNCTIONS;
|
||||
|
||||
it('allows the has family on every signal', () => {
|
||||
[DataSource.LOGS, DataSource.TRACES, DataSource.METRICS].forEach((signal) => {
|
||||
expect(isSupportedFunction(HASANY, signal)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('allows search on logs only', () => {
|
||||
expect(isSupportedFunction(SEARCH, DataSource.LOGS)).toBe(true);
|
||||
expect(isSupportedFunction(SEARCH, DataSource.TRACES)).toBe(false);
|
||||
expect(isSupportedFunction(SEARCH, DataSource.METRICS)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { closeCompletion, startCompletion } from '@codemirror/autocomplete';
|
||||
import type { Completion } from '@codemirror/autocomplete';
|
||||
import type { EditorView } from '@uiw/react-codemirror';
|
||||
import { QUERY_BUILDER_FUNCTIONS } from 'constants/antlrQueryConstants';
|
||||
import dayjs from 'dayjs';
|
||||
import { normalizeFilterExpression } from 'lib/recentQueries/normalize';
|
||||
import * as recentQueriesStore from 'lib/recentQueries/recentQueriesStore';
|
||||
@@ -15,6 +16,15 @@ import {
|
||||
RECENTS_SECTION,
|
||||
} from './constants';
|
||||
|
||||
// search() lives in the logs condition builder only; traces and metrics reject it
|
||||
// as an unsupported operator. Every other function is implemented for all signals.
|
||||
export function isSupportedFunction(
|
||||
functionName: string,
|
||||
signal: SignalType,
|
||||
): boolean {
|
||||
return functionName !== QUERY_BUILDER_FUNCTIONS.SEARCH || signal === 'logs';
|
||||
}
|
||||
|
||||
export interface FieldContextPrefixMatch {
|
||||
context: string;
|
||||
remainder: string;
|
||||
|
||||
@@ -41,6 +41,7 @@ export const QUERY_BUILDER_FUNCTIONS = {
|
||||
HASANY: 'hasAny',
|
||||
HASALL: 'hasAll',
|
||||
HASTOKEN: 'hasToken',
|
||||
SEARCH: 'search',
|
||||
};
|
||||
|
||||
export function negateOperator(operatorOrFunction: string): string {
|
||||
|
||||
55
frontend/src/lib/logs/flatLogData.test.ts
Normal file
55
frontend/src/lib/logs/flatLogData.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import { getLogFieldValue } from './flatLogData';
|
||||
|
||||
const asLog = (partial: Partial<ILog>): ILog => partial as unknown as ILog;
|
||||
|
||||
describe('getLogFieldValue', () => {
|
||||
it('resolves a nested body field by dotted key when use_json_body is on', () => {
|
||||
const log = asLog({ body: { a: { b: { c: 'deep' } } } });
|
||||
expect(getLogFieldValue(log, 'a.b.c', true)).toBe('deep');
|
||||
});
|
||||
|
||||
it('ignores body when use_json_body is off', () => {
|
||||
const log = asLog({ body: { a: { b: { c: 'deep' } } } });
|
||||
expect(getLogFieldValue(log, 'a.b.c', false)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('ignores a stringified body even when use_json_body is on', () => {
|
||||
const log = asLog({ body: '{"a":{"b":1}}' });
|
||||
expect(getLogFieldValue(log, 'a.b', true)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('prefers the body value over attributes when the key exists in both (body first)', () => {
|
||||
const log = asLog({
|
||||
attributes_string: { 'a.b': 'attr' } as never,
|
||||
body: { a: { b: 'bodyval' } },
|
||||
});
|
||||
expect(getLogFieldValue(log, 'a.b', true)).toBe('bodyval');
|
||||
});
|
||||
|
||||
it('falls back to attributes when the key is not in the body', () => {
|
||||
const log = asLog({
|
||||
attributes_string: { 'x.y': 'attr' } as never,
|
||||
body: { other: 1 },
|
||||
});
|
||||
expect(getLogFieldValue(log, 'x.y', true)).toBe('attr');
|
||||
});
|
||||
|
||||
it('preserves falsy body values (0, false, empty string)', () => {
|
||||
const log = asLog({ body: { n: 0, flag: false, s: '' } });
|
||||
expect(getLogFieldValue(log, 'n', true)).toBe(0);
|
||||
expect(getLogFieldValue(log, 'flag', true)).toBe(false);
|
||||
expect(getLogFieldValue(log, 's', true)).toBe('');
|
||||
});
|
||||
|
||||
it('returns undefined when the body path is missing', () => {
|
||||
const log = asLog({ body: { x: 1 } });
|
||||
expect(getLogFieldValue(log, 'nope', true)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined when a mid path segment is not an object', () => {
|
||||
const log = asLog({ body: { a: { b: 'leaf' } } });
|
||||
expect(getLogFieldValue(log, 'a.b.c', true)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defaultTo } from 'lodash-es';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { ILog, ILogBody } from 'types/api/logs/log';
|
||||
|
||||
export function FlatLogData(log: ILog): Record<string, string> {
|
||||
const flattenLogObject: Record<string, string> = {};
|
||||
@@ -15,3 +15,29 @@ export function FlatLogData(log: ILog): Record<string, string> {
|
||||
});
|
||||
return flattenLogObject;
|
||||
}
|
||||
|
||||
function getBodyFieldValue(body: ILogBody, key: string): unknown {
|
||||
return key.split('.').reduce<unknown>((acc, segment) => {
|
||||
if (acc && typeof acc === 'object' && !Array.isArray(acc)) {
|
||||
return (acc as Record<string, unknown>)[segment];
|
||||
}
|
||||
return undefined;
|
||||
}, body);
|
||||
}
|
||||
|
||||
// Resolve one field for the logs table. A JSON body is checked first (use_json_body
|
||||
// only), splitting the key on `.`; otherwise fall back to FlatLogData
|
||||
// (attributes/resources/scope/top-level).
|
||||
export function getLogFieldValue(
|
||||
log: ILog,
|
||||
fieldName: string,
|
||||
isBodyJsonEnabled: boolean,
|
||||
): unknown {
|
||||
if (isBodyJsonEnabled && log.body && typeof log.body === 'object') {
|
||||
const bodyValue = getBodyFieldValue(log.body, fieldName);
|
||||
if (bodyValue !== undefined) {
|
||||
return bodyValue;
|
||||
}
|
||||
}
|
||||
return FlatLogData(log)[fieldName];
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('normalizeFilterExpression', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('lowercases HAS / HASANY / HASALL / HASTOKEN function names', () => {
|
||||
it('lowercases HAS / HASANY / HASALL / HASTOKEN / SEARCH function names', () => {
|
||||
expect(normalizeFilterExpression('HAS(tags, "x")')).toBe(
|
||||
normalizeFilterExpression('has(tags, "x")'),
|
||||
);
|
||||
@@ -47,6 +47,9 @@ describe('normalizeFilterExpression', () => {
|
||||
expect(normalizeFilterExpression('HASTOKEN(msg, "err")')).toBe(
|
||||
normalizeFilterExpression('hasToken(msg, "err")'),
|
||||
);
|
||||
expect(normalizeFilterExpression('SEARCH("err")')).toBe(
|
||||
normalizeFilterExpression('search("err")'),
|
||||
);
|
||||
});
|
||||
|
||||
it('lowercases TRUE / FALSE boolean literals', () => {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -24,12 +24,13 @@ HASTOKEN=23
|
||||
HAS=24
|
||||
HASANY=25
|
||||
HASALL=26
|
||||
BOOL=27
|
||||
NUMBER=28
|
||||
QUOTED_TEXT=29
|
||||
KEY=30
|
||||
WS=31
|
||||
FREETEXT=32
|
||||
SEARCH=27
|
||||
BOOL=28
|
||||
NUMBER=29
|
||||
QUOTED_TEXT=30
|
||||
KEY=31
|
||||
WS=32
|
||||
FREETEXT=33
|
||||
'('=1
|
||||
')'=2
|
||||
'['=3
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -24,12 +24,13 @@ HASTOKEN=23
|
||||
HAS=24
|
||||
HASANY=25
|
||||
HASALL=26
|
||||
BOOL=27
|
||||
NUMBER=28
|
||||
QUOTED_TEXT=29
|
||||
KEY=30
|
||||
WS=31
|
||||
FREETEXT=32
|
||||
SEARCH=27
|
||||
BOOL=28
|
||||
NUMBER=29
|
||||
QUOTED_TEXT=30
|
||||
KEY=31
|
||||
WS=32
|
||||
FREETEXT=33
|
||||
'('=1
|
||||
')'=2
|
||||
'['=3
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Generated from FilterQuery.g4 by ANTLR 4.13.1
|
||||
// Generated from FilterQuery.g4 by ANTLR 4.13.2
|
||||
// noinspection ES6UnusedImports,JSUnusedGlobalSymbols,JSUnusedLocalSymbols
|
||||
import {
|
||||
ATN,
|
||||
@@ -38,12 +38,13 @@ export default class FilterQueryLexer extends Lexer {
|
||||
public static readonly HAS = 24;
|
||||
public static readonly HASANY = 25;
|
||||
public static readonly HASALL = 26;
|
||||
public static readonly BOOL = 27;
|
||||
public static readonly NUMBER = 28;
|
||||
public static readonly QUOTED_TEXT = 29;
|
||||
public static readonly KEY = 30;
|
||||
public static readonly WS = 31;
|
||||
public static readonly FREETEXT = 32;
|
||||
public static readonly SEARCH = 27;
|
||||
public static readonly BOOL = 28;
|
||||
public static readonly NUMBER = 29;
|
||||
public static readonly QUOTED_TEXT = 30;
|
||||
public static readonly KEY = 31;
|
||||
public static readonly WS = 32;
|
||||
public static readonly FREETEXT = 33;
|
||||
public static readonly EOF = Token.EOF;
|
||||
|
||||
public static readonly channelNames: string[] = [ "DEFAULT_TOKEN_CHANNEL", "HIDDEN" ];
|
||||
@@ -68,8 +69,9 @@ export default class FilterQueryLexer extends Lexer {
|
||||
"AND", "OR",
|
||||
"HASTOKEN",
|
||||
"HAS", "HASANY",
|
||||
"HASALL", "BOOL",
|
||||
"NUMBER", "QUOTED_TEXT",
|
||||
"HASALL", "SEARCH",
|
||||
"BOOL", "NUMBER",
|
||||
"QUOTED_TEXT",
|
||||
"KEY", "WS",
|
||||
"FREETEXT" ];
|
||||
public static readonly modeNames: string[] = [ "DEFAULT_MODE", ];
|
||||
@@ -78,8 +80,8 @@ export default class FilterQueryLexer extends Lexer {
|
||||
"LPAREN", "RPAREN", "LBRACK", "RBRACK", "COMMA", "EQUALS", "NOT_EQUALS",
|
||||
"NEQ", "LT", "LE", "GT", "GE", "LIKE", "ILIKE", "BETWEEN", "EXISTS", "REGEXP",
|
||||
"CONTAINS", "IN", "NOT", "AND", "OR", "HASTOKEN", "HAS", "HASANY", "HASALL",
|
||||
"BOOL", "SIGN", "NUMBER", "QUOTED_TEXT", "SEGMENT", "EMPTY_BRACKS", "OLD_JSON_BRACKS",
|
||||
"KEY", "WS", "DIGIT", "FREETEXT",
|
||||
"SEARCH", "BOOL", "SIGN", "NUMBER", "QUOTED_TEXT", "SEGMENT", "EMPTY_BRACKS",
|
||||
"OLD_JSON_BRACKS", "KEY", "WS", "DIGIT", "FREETEXT",
|
||||
];
|
||||
|
||||
|
||||
@@ -100,119 +102,122 @@ export default class FilterQueryLexer extends Lexer {
|
||||
|
||||
public get modeNames(): string[] { return FilterQueryLexer.modeNames; }
|
||||
|
||||
public static readonly _serializedATN: number[] = [4,0,32,320,6,-1,2,0,
|
||||
public static readonly _serializedATN: number[] = [4,0,33,329,6,-1,2,0,
|
||||
7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2,7,7,7,2,8,7,8,2,9,
|
||||
7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13,2,14,7,14,2,15,7,15,2,16,7,
|
||||
16,2,17,7,17,2,18,7,18,2,19,7,19,2,20,7,20,2,21,7,21,2,22,7,22,2,23,7,23,
|
||||
2,24,7,24,2,25,7,25,2,26,7,26,2,27,7,27,2,28,7,28,2,29,7,29,2,30,7,30,2,
|
||||
31,7,31,2,32,7,32,2,33,7,33,2,34,7,34,2,35,7,35,2,36,7,36,1,0,1,0,1,1,1,
|
||||
1,1,2,1,2,1,3,1,3,1,4,1,4,1,5,1,5,1,5,3,5,89,8,5,1,6,1,6,1,6,1,7,1,7,1,
|
||||
7,1,8,1,8,1,9,1,9,1,9,1,10,1,10,1,11,1,11,1,11,1,12,1,12,1,12,1,12,1,12,
|
||||
1,13,1,13,1,13,1,13,1,13,1,13,1,14,1,14,1,14,1,14,1,14,1,14,1,14,1,14,1,
|
||||
15,1,15,1,15,1,15,1,15,1,15,3,15,132,8,15,1,16,1,16,1,16,1,16,1,16,1,16,
|
||||
1,16,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,3,17,149,8,17,1,18,1,18,1,
|
||||
18,1,19,1,19,1,19,1,19,1,20,1,20,1,20,1,20,1,21,1,21,1,21,1,22,1,22,1,22,
|
||||
1,22,1,22,1,22,1,22,1,22,1,22,1,23,1,23,1,23,1,23,1,24,1,24,1,24,1,24,1,
|
||||
24,1,24,1,24,1,25,1,25,1,25,1,25,1,25,1,25,1,25,1,26,1,26,1,26,1,26,1,26,
|
||||
1,26,1,26,1,26,1,26,3,26,201,8,26,1,27,1,27,1,28,3,28,206,8,28,1,28,4,28,
|
||||
209,8,28,11,28,12,28,210,1,28,1,28,5,28,215,8,28,10,28,12,28,218,9,28,3,
|
||||
28,220,8,28,1,28,1,28,3,28,224,8,28,1,28,4,28,227,8,28,11,28,12,28,228,
|
||||
3,28,231,8,28,1,28,3,28,234,8,28,1,28,1,28,4,28,238,8,28,11,28,12,28,239,
|
||||
1,28,1,28,3,28,244,8,28,1,28,4,28,247,8,28,11,28,12,28,248,3,28,251,8,28,
|
||||
3,28,253,8,28,1,29,1,29,1,29,1,29,5,29,259,8,29,10,29,12,29,262,9,29,1,
|
||||
29,1,29,1,29,1,29,1,29,5,29,269,8,29,10,29,12,29,272,9,29,1,29,3,29,275,
|
||||
8,29,1,30,1,30,5,30,279,8,30,10,30,12,30,282,9,30,1,31,1,31,1,31,1,32,1,
|
||||
32,1,32,1,32,1,33,1,33,1,33,1,33,1,33,1,33,1,33,4,33,298,8,33,11,33,12,
|
||||
33,299,5,33,302,8,33,10,33,12,33,305,9,33,1,34,4,34,308,8,34,11,34,12,34,
|
||||
309,1,34,1,34,1,35,1,35,1,36,4,36,317,8,36,11,36,12,36,318,0,0,37,1,1,3,
|
||||
2,5,3,7,4,9,5,11,6,13,7,15,8,17,9,19,10,21,11,23,12,25,13,27,14,29,15,31,
|
||||
16,33,17,35,18,37,19,39,20,41,21,43,22,45,23,47,24,49,25,51,26,53,27,55,
|
||||
0,57,28,59,29,61,0,63,0,65,0,67,30,69,31,71,0,73,32,1,0,29,2,0,76,76,108,
|
||||
108,2,0,73,73,105,105,2,0,75,75,107,107,2,0,69,69,101,101,2,0,66,66,98,
|
||||
98,2,0,84,84,116,116,2,0,87,87,119,119,2,0,78,78,110,110,2,0,88,88,120,
|
||||
120,2,0,83,83,115,115,2,0,82,82,114,114,2,0,71,71,103,103,2,0,80,80,112,
|
||||
112,2,0,67,67,99,99,2,0,79,79,111,111,2,0,65,65,97,97,2,0,68,68,100,100,
|
||||
2,0,72,72,104,104,2,0,89,89,121,121,2,0,85,85,117,117,2,0,70,70,102,102,
|
||||
2,0,43,43,45,45,2,0,34,34,92,92,2,0,39,39,92,92,4,0,35,36,64,90,95,95,97,
|
||||
123,7,0,35,36,45,45,47,58,64,90,95,95,97,123,125,125,3,0,9,10,13,13,32,
|
||||
32,1,0,48,57,8,0,9,10,13,13,32,34,39,41,44,44,60,62,91,91,93,93,344,0,1,
|
||||
1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,9,1,0,0,0,0,11,1,0,0,0,0,
|
||||
13,1,0,0,0,0,15,1,0,0,0,0,17,1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0,23,1,0,
|
||||
0,0,0,25,1,0,0,0,0,27,1,0,0,0,0,29,1,0,0,0,0,31,1,0,0,0,0,33,1,0,0,0,0,
|
||||
35,1,0,0,0,0,37,1,0,0,0,0,39,1,0,0,0,0,41,1,0,0,0,0,43,1,0,0,0,0,45,1,0,
|
||||
0,0,0,47,1,0,0,0,0,49,1,0,0,0,0,51,1,0,0,0,0,53,1,0,0,0,0,57,1,0,0,0,0,
|
||||
59,1,0,0,0,0,67,1,0,0,0,0,69,1,0,0,0,0,73,1,0,0,0,1,75,1,0,0,0,3,77,1,0,
|
||||
0,0,5,79,1,0,0,0,7,81,1,0,0,0,9,83,1,0,0,0,11,88,1,0,0,0,13,90,1,0,0,0,
|
||||
15,93,1,0,0,0,17,96,1,0,0,0,19,98,1,0,0,0,21,101,1,0,0,0,23,103,1,0,0,0,
|
||||
25,106,1,0,0,0,27,111,1,0,0,0,29,117,1,0,0,0,31,125,1,0,0,0,33,133,1,0,
|
||||
0,0,35,140,1,0,0,0,37,150,1,0,0,0,39,153,1,0,0,0,41,157,1,0,0,0,43,161,
|
||||
1,0,0,0,45,164,1,0,0,0,47,173,1,0,0,0,49,177,1,0,0,0,51,184,1,0,0,0,53,
|
||||
200,1,0,0,0,55,202,1,0,0,0,57,252,1,0,0,0,59,274,1,0,0,0,61,276,1,0,0,0,
|
||||
63,283,1,0,0,0,65,286,1,0,0,0,67,290,1,0,0,0,69,307,1,0,0,0,71,313,1,0,
|
||||
0,0,73,316,1,0,0,0,75,76,5,40,0,0,76,2,1,0,0,0,77,78,5,41,0,0,78,4,1,0,
|
||||
0,0,79,80,5,91,0,0,80,6,1,0,0,0,81,82,5,93,0,0,82,8,1,0,0,0,83,84,5,44,
|
||||
0,0,84,10,1,0,0,0,85,89,5,61,0,0,86,87,5,61,0,0,87,89,5,61,0,0,88,85,1,
|
||||
0,0,0,88,86,1,0,0,0,89,12,1,0,0,0,90,91,5,33,0,0,91,92,5,61,0,0,92,14,1,
|
||||
0,0,0,93,94,5,60,0,0,94,95,5,62,0,0,95,16,1,0,0,0,96,97,5,60,0,0,97,18,
|
||||
1,0,0,0,98,99,5,60,0,0,99,100,5,61,0,0,100,20,1,0,0,0,101,102,5,62,0,0,
|
||||
102,22,1,0,0,0,103,104,5,62,0,0,104,105,5,61,0,0,105,24,1,0,0,0,106,107,
|
||||
7,0,0,0,107,108,7,1,0,0,108,109,7,2,0,0,109,110,7,3,0,0,110,26,1,0,0,0,
|
||||
111,112,7,1,0,0,112,113,7,0,0,0,113,114,7,1,0,0,114,115,7,2,0,0,115,116,
|
||||
7,3,0,0,116,28,1,0,0,0,117,118,7,4,0,0,118,119,7,3,0,0,119,120,7,5,0,0,
|
||||
120,121,7,6,0,0,121,122,7,3,0,0,122,123,7,3,0,0,123,124,7,7,0,0,124,30,
|
||||
1,0,0,0,125,126,7,3,0,0,126,127,7,8,0,0,127,128,7,1,0,0,128,129,7,9,0,0,
|
||||
129,131,7,5,0,0,130,132,7,9,0,0,131,130,1,0,0,0,131,132,1,0,0,0,132,32,
|
||||
1,0,0,0,133,134,7,10,0,0,134,135,7,3,0,0,135,136,7,11,0,0,136,137,7,3,0,
|
||||
0,137,138,7,8,0,0,138,139,7,12,0,0,139,34,1,0,0,0,140,141,7,13,0,0,141,
|
||||
142,7,14,0,0,142,143,7,7,0,0,143,144,7,5,0,0,144,145,7,15,0,0,145,146,7,
|
||||
1,0,0,146,148,7,7,0,0,147,149,7,9,0,0,148,147,1,0,0,0,148,149,1,0,0,0,149,
|
||||
36,1,0,0,0,150,151,7,1,0,0,151,152,7,7,0,0,152,38,1,0,0,0,153,154,7,7,0,
|
||||
0,154,155,7,14,0,0,155,156,7,5,0,0,156,40,1,0,0,0,157,158,7,15,0,0,158,
|
||||
159,7,7,0,0,159,160,7,16,0,0,160,42,1,0,0,0,161,162,7,14,0,0,162,163,7,
|
||||
10,0,0,163,44,1,0,0,0,164,165,7,17,0,0,165,166,7,15,0,0,166,167,7,9,0,0,
|
||||
167,168,7,5,0,0,168,169,7,14,0,0,169,170,7,2,0,0,170,171,7,3,0,0,171,172,
|
||||
7,7,0,0,172,46,1,0,0,0,173,174,7,17,0,0,174,175,7,15,0,0,175,176,7,9,0,
|
||||
0,176,48,1,0,0,0,177,178,7,17,0,0,178,179,7,15,0,0,179,180,7,9,0,0,180,
|
||||
181,7,15,0,0,181,182,7,7,0,0,182,183,7,18,0,0,183,50,1,0,0,0,184,185,7,
|
||||
17,0,0,185,186,7,15,0,0,186,187,7,9,0,0,187,188,7,15,0,0,188,189,7,0,0,
|
||||
0,189,190,7,0,0,0,190,52,1,0,0,0,191,192,7,5,0,0,192,193,7,10,0,0,193,194,
|
||||
7,19,0,0,194,201,7,3,0,0,195,196,7,20,0,0,196,197,7,15,0,0,197,198,7,0,
|
||||
0,0,198,199,7,9,0,0,199,201,7,3,0,0,200,191,1,0,0,0,200,195,1,0,0,0,201,
|
||||
54,1,0,0,0,202,203,7,21,0,0,203,56,1,0,0,0,204,206,3,55,27,0,205,204,1,
|
||||
0,0,0,205,206,1,0,0,0,206,208,1,0,0,0,207,209,3,71,35,0,208,207,1,0,0,0,
|
||||
209,210,1,0,0,0,210,208,1,0,0,0,210,211,1,0,0,0,211,219,1,0,0,0,212,216,
|
||||
5,46,0,0,213,215,3,71,35,0,214,213,1,0,0,0,215,218,1,0,0,0,216,214,1,0,
|
||||
0,0,216,217,1,0,0,0,217,220,1,0,0,0,218,216,1,0,0,0,219,212,1,0,0,0,219,
|
||||
220,1,0,0,0,220,230,1,0,0,0,221,223,7,3,0,0,222,224,3,55,27,0,223,222,1,
|
||||
0,0,0,223,224,1,0,0,0,224,226,1,0,0,0,225,227,3,71,35,0,226,225,1,0,0,0,
|
||||
227,228,1,0,0,0,228,226,1,0,0,0,228,229,1,0,0,0,229,231,1,0,0,0,230,221,
|
||||
1,0,0,0,230,231,1,0,0,0,231,253,1,0,0,0,232,234,3,55,27,0,233,232,1,0,0,
|
||||
0,233,234,1,0,0,0,234,235,1,0,0,0,235,237,5,46,0,0,236,238,3,71,35,0,237,
|
||||
236,1,0,0,0,238,239,1,0,0,0,239,237,1,0,0,0,239,240,1,0,0,0,240,250,1,0,
|
||||
0,0,241,243,7,3,0,0,242,244,3,55,27,0,243,242,1,0,0,0,243,244,1,0,0,0,244,
|
||||
246,1,0,0,0,245,247,3,71,35,0,246,245,1,0,0,0,247,248,1,0,0,0,248,246,1,
|
||||
0,0,0,248,249,1,0,0,0,249,251,1,0,0,0,250,241,1,0,0,0,250,251,1,0,0,0,251,
|
||||
253,1,0,0,0,252,205,1,0,0,0,252,233,1,0,0,0,253,58,1,0,0,0,254,260,5,34,
|
||||
0,0,255,259,8,22,0,0,256,257,5,92,0,0,257,259,9,0,0,0,258,255,1,0,0,0,258,
|
||||
256,1,0,0,0,259,262,1,0,0,0,260,258,1,0,0,0,260,261,1,0,0,0,261,263,1,0,
|
||||
0,0,262,260,1,0,0,0,263,275,5,34,0,0,264,270,5,39,0,0,265,269,8,23,0,0,
|
||||
266,267,5,92,0,0,267,269,9,0,0,0,268,265,1,0,0,0,268,266,1,0,0,0,269,272,
|
||||
1,0,0,0,270,268,1,0,0,0,270,271,1,0,0,0,271,273,1,0,0,0,272,270,1,0,0,0,
|
||||
273,275,5,39,0,0,274,254,1,0,0,0,274,264,1,0,0,0,275,60,1,0,0,0,276,280,
|
||||
7,24,0,0,277,279,7,25,0,0,278,277,1,0,0,0,279,282,1,0,0,0,280,278,1,0,0,
|
||||
0,280,281,1,0,0,0,281,62,1,0,0,0,282,280,1,0,0,0,283,284,5,91,0,0,284,285,
|
||||
5,93,0,0,285,64,1,0,0,0,286,287,5,91,0,0,287,288,5,42,0,0,288,289,5,93,
|
||||
0,0,289,66,1,0,0,0,290,303,3,61,30,0,291,292,5,46,0,0,292,302,3,61,30,0,
|
||||
293,302,3,63,31,0,294,302,3,65,32,0,295,297,5,46,0,0,296,298,3,71,35,0,
|
||||
297,296,1,0,0,0,298,299,1,0,0,0,299,297,1,0,0,0,299,300,1,0,0,0,300,302,
|
||||
1,0,0,0,301,291,1,0,0,0,301,293,1,0,0,0,301,294,1,0,0,0,301,295,1,0,0,0,
|
||||
302,305,1,0,0,0,303,301,1,0,0,0,303,304,1,0,0,0,304,68,1,0,0,0,305,303,
|
||||
1,0,0,0,306,308,7,26,0,0,307,306,1,0,0,0,308,309,1,0,0,0,309,307,1,0,0,
|
||||
0,309,310,1,0,0,0,310,311,1,0,0,0,311,312,6,34,0,0,312,70,1,0,0,0,313,314,
|
||||
7,27,0,0,314,72,1,0,0,0,315,317,8,28,0,0,316,315,1,0,0,0,317,318,1,0,0,
|
||||
0,318,316,1,0,0,0,318,319,1,0,0,0,319,74,1,0,0,0,29,0,88,131,148,200,205,
|
||||
210,216,219,223,228,230,233,239,243,248,250,252,258,260,268,270,274,280,
|
||||
299,301,303,309,318,1,6,0,0];
|
||||
31,7,31,2,32,7,32,2,33,7,33,2,34,7,34,2,35,7,35,2,36,7,36,2,37,7,37,1,0,
|
||||
1,0,1,1,1,1,1,2,1,2,1,3,1,3,1,4,1,4,1,5,1,5,1,5,3,5,91,8,5,1,6,1,6,1,6,
|
||||
1,7,1,7,1,7,1,8,1,8,1,9,1,9,1,9,1,10,1,10,1,11,1,11,1,11,1,12,1,12,1,12,
|
||||
1,12,1,12,1,13,1,13,1,13,1,13,1,13,1,13,1,14,1,14,1,14,1,14,1,14,1,14,1,
|
||||
14,1,14,1,15,1,15,1,15,1,15,1,15,1,15,3,15,134,8,15,1,16,1,16,1,16,1,16,
|
||||
1,16,1,16,1,16,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,3,17,151,8,17,1,
|
||||
18,1,18,1,18,1,19,1,19,1,19,1,19,1,20,1,20,1,20,1,20,1,21,1,21,1,21,1,22,
|
||||
1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,23,1,23,1,23,1,23,1,24,1,24,1,
|
||||
24,1,24,1,24,1,24,1,24,1,25,1,25,1,25,1,25,1,25,1,25,1,25,1,26,1,26,1,26,
|
||||
1,26,1,26,1,26,1,26,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,3,27,210,
|
||||
8,27,1,28,1,28,1,29,3,29,215,8,29,1,29,4,29,218,8,29,11,29,12,29,219,1,
|
||||
29,1,29,5,29,224,8,29,10,29,12,29,227,9,29,3,29,229,8,29,1,29,1,29,3,29,
|
||||
233,8,29,1,29,4,29,236,8,29,11,29,12,29,237,3,29,240,8,29,1,29,3,29,243,
|
||||
8,29,1,29,1,29,4,29,247,8,29,11,29,12,29,248,1,29,1,29,3,29,253,8,29,1,
|
||||
29,4,29,256,8,29,11,29,12,29,257,3,29,260,8,29,3,29,262,8,29,1,30,1,30,
|
||||
1,30,1,30,5,30,268,8,30,10,30,12,30,271,9,30,1,30,1,30,1,30,1,30,1,30,5,
|
||||
30,278,8,30,10,30,12,30,281,9,30,1,30,3,30,284,8,30,1,31,1,31,5,31,288,
|
||||
8,31,10,31,12,31,291,9,31,1,32,1,32,1,32,1,33,1,33,1,33,1,33,1,34,1,34,
|
||||
1,34,1,34,1,34,1,34,1,34,4,34,307,8,34,11,34,12,34,308,5,34,311,8,34,10,
|
||||
34,12,34,314,9,34,1,35,4,35,317,8,35,11,35,12,35,318,1,35,1,35,1,36,1,36,
|
||||
1,37,4,37,326,8,37,11,37,12,37,327,0,0,38,1,1,3,2,5,3,7,4,9,5,11,6,13,7,
|
||||
15,8,17,9,19,10,21,11,23,12,25,13,27,14,29,15,31,16,33,17,35,18,37,19,39,
|
||||
20,41,21,43,22,45,23,47,24,49,25,51,26,53,27,55,28,57,0,59,29,61,30,63,
|
||||
0,65,0,67,0,69,31,71,32,73,0,75,33,1,0,29,2,0,76,76,108,108,2,0,73,73,105,
|
||||
105,2,0,75,75,107,107,2,0,69,69,101,101,2,0,66,66,98,98,2,0,84,84,116,116,
|
||||
2,0,87,87,119,119,2,0,78,78,110,110,2,0,88,88,120,120,2,0,83,83,115,115,
|
||||
2,0,82,82,114,114,2,0,71,71,103,103,2,0,80,80,112,112,2,0,67,67,99,99,2,
|
||||
0,79,79,111,111,2,0,65,65,97,97,2,0,68,68,100,100,2,0,72,72,104,104,2,0,
|
||||
89,89,121,121,2,0,85,85,117,117,2,0,70,70,102,102,2,0,43,43,45,45,2,0,34,
|
||||
34,92,92,2,0,39,39,92,92,4,0,35,36,64,90,95,95,97,123,7,0,35,36,45,45,47,
|
||||
58,64,90,95,95,97,123,125,125,3,0,9,10,13,13,32,32,1,0,48,57,8,0,9,10,13,
|
||||
13,32,34,39,41,44,44,60,62,91,91,93,93,353,0,1,1,0,0,0,0,3,1,0,0,0,0,5,
|
||||
1,0,0,0,0,7,1,0,0,0,0,9,1,0,0,0,0,11,1,0,0,0,0,13,1,0,0,0,0,15,1,0,0,0,
|
||||
0,17,1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0,23,1,0,0,0,0,25,1,0,0,0,0,27,1,
|
||||
0,0,0,0,29,1,0,0,0,0,31,1,0,0,0,0,33,1,0,0,0,0,35,1,0,0,0,0,37,1,0,0,0,
|
||||
0,39,1,0,0,0,0,41,1,0,0,0,0,43,1,0,0,0,0,45,1,0,0,0,0,47,1,0,0,0,0,49,1,
|
||||
0,0,0,0,51,1,0,0,0,0,53,1,0,0,0,0,55,1,0,0,0,0,59,1,0,0,0,0,61,1,0,0,0,
|
||||
0,69,1,0,0,0,0,71,1,0,0,0,0,75,1,0,0,0,1,77,1,0,0,0,3,79,1,0,0,0,5,81,1,
|
||||
0,0,0,7,83,1,0,0,0,9,85,1,0,0,0,11,90,1,0,0,0,13,92,1,0,0,0,15,95,1,0,0,
|
||||
0,17,98,1,0,0,0,19,100,1,0,0,0,21,103,1,0,0,0,23,105,1,0,0,0,25,108,1,0,
|
||||
0,0,27,113,1,0,0,0,29,119,1,0,0,0,31,127,1,0,0,0,33,135,1,0,0,0,35,142,
|
||||
1,0,0,0,37,152,1,0,0,0,39,155,1,0,0,0,41,159,1,0,0,0,43,163,1,0,0,0,45,
|
||||
166,1,0,0,0,47,175,1,0,0,0,49,179,1,0,0,0,51,186,1,0,0,0,53,193,1,0,0,0,
|
||||
55,209,1,0,0,0,57,211,1,0,0,0,59,261,1,0,0,0,61,283,1,0,0,0,63,285,1,0,
|
||||
0,0,65,292,1,0,0,0,67,295,1,0,0,0,69,299,1,0,0,0,71,316,1,0,0,0,73,322,
|
||||
1,0,0,0,75,325,1,0,0,0,77,78,5,40,0,0,78,2,1,0,0,0,79,80,5,41,0,0,80,4,
|
||||
1,0,0,0,81,82,5,91,0,0,82,6,1,0,0,0,83,84,5,93,0,0,84,8,1,0,0,0,85,86,5,
|
||||
44,0,0,86,10,1,0,0,0,87,91,5,61,0,0,88,89,5,61,0,0,89,91,5,61,0,0,90,87,
|
||||
1,0,0,0,90,88,1,0,0,0,91,12,1,0,0,0,92,93,5,33,0,0,93,94,5,61,0,0,94,14,
|
||||
1,0,0,0,95,96,5,60,0,0,96,97,5,62,0,0,97,16,1,0,0,0,98,99,5,60,0,0,99,18,
|
||||
1,0,0,0,100,101,5,60,0,0,101,102,5,61,0,0,102,20,1,0,0,0,103,104,5,62,0,
|
||||
0,104,22,1,0,0,0,105,106,5,62,0,0,106,107,5,61,0,0,107,24,1,0,0,0,108,109,
|
||||
7,0,0,0,109,110,7,1,0,0,110,111,7,2,0,0,111,112,7,3,0,0,112,26,1,0,0,0,
|
||||
113,114,7,1,0,0,114,115,7,0,0,0,115,116,7,1,0,0,116,117,7,2,0,0,117,118,
|
||||
7,3,0,0,118,28,1,0,0,0,119,120,7,4,0,0,120,121,7,3,0,0,121,122,7,5,0,0,
|
||||
122,123,7,6,0,0,123,124,7,3,0,0,124,125,7,3,0,0,125,126,7,7,0,0,126,30,
|
||||
1,0,0,0,127,128,7,3,0,0,128,129,7,8,0,0,129,130,7,1,0,0,130,131,7,9,0,0,
|
||||
131,133,7,5,0,0,132,134,7,9,0,0,133,132,1,0,0,0,133,134,1,0,0,0,134,32,
|
||||
1,0,0,0,135,136,7,10,0,0,136,137,7,3,0,0,137,138,7,11,0,0,138,139,7,3,0,
|
||||
0,139,140,7,8,0,0,140,141,7,12,0,0,141,34,1,0,0,0,142,143,7,13,0,0,143,
|
||||
144,7,14,0,0,144,145,7,7,0,0,145,146,7,5,0,0,146,147,7,15,0,0,147,148,7,
|
||||
1,0,0,148,150,7,7,0,0,149,151,7,9,0,0,150,149,1,0,0,0,150,151,1,0,0,0,151,
|
||||
36,1,0,0,0,152,153,7,1,0,0,153,154,7,7,0,0,154,38,1,0,0,0,155,156,7,7,0,
|
||||
0,156,157,7,14,0,0,157,158,7,5,0,0,158,40,1,0,0,0,159,160,7,15,0,0,160,
|
||||
161,7,7,0,0,161,162,7,16,0,0,162,42,1,0,0,0,163,164,7,14,0,0,164,165,7,
|
||||
10,0,0,165,44,1,0,0,0,166,167,7,17,0,0,167,168,7,15,0,0,168,169,7,9,0,0,
|
||||
169,170,7,5,0,0,170,171,7,14,0,0,171,172,7,2,0,0,172,173,7,3,0,0,173,174,
|
||||
7,7,0,0,174,46,1,0,0,0,175,176,7,17,0,0,176,177,7,15,0,0,177,178,7,9,0,
|
||||
0,178,48,1,0,0,0,179,180,7,17,0,0,180,181,7,15,0,0,181,182,7,9,0,0,182,
|
||||
183,7,15,0,0,183,184,7,7,0,0,184,185,7,18,0,0,185,50,1,0,0,0,186,187,7,
|
||||
17,0,0,187,188,7,15,0,0,188,189,7,9,0,0,189,190,7,15,0,0,190,191,7,0,0,
|
||||
0,191,192,7,0,0,0,192,52,1,0,0,0,193,194,7,9,0,0,194,195,7,3,0,0,195,196,
|
||||
7,15,0,0,196,197,7,10,0,0,197,198,7,13,0,0,198,199,7,17,0,0,199,54,1,0,
|
||||
0,0,200,201,7,5,0,0,201,202,7,10,0,0,202,203,7,19,0,0,203,210,7,3,0,0,204,
|
||||
205,7,20,0,0,205,206,7,15,0,0,206,207,7,0,0,0,207,208,7,9,0,0,208,210,7,
|
||||
3,0,0,209,200,1,0,0,0,209,204,1,0,0,0,210,56,1,0,0,0,211,212,7,21,0,0,212,
|
||||
58,1,0,0,0,213,215,3,57,28,0,214,213,1,0,0,0,214,215,1,0,0,0,215,217,1,
|
||||
0,0,0,216,218,3,73,36,0,217,216,1,0,0,0,218,219,1,0,0,0,219,217,1,0,0,0,
|
||||
219,220,1,0,0,0,220,228,1,0,0,0,221,225,5,46,0,0,222,224,3,73,36,0,223,
|
||||
222,1,0,0,0,224,227,1,0,0,0,225,223,1,0,0,0,225,226,1,0,0,0,226,229,1,0,
|
||||
0,0,227,225,1,0,0,0,228,221,1,0,0,0,228,229,1,0,0,0,229,239,1,0,0,0,230,
|
||||
232,7,3,0,0,231,233,3,57,28,0,232,231,1,0,0,0,232,233,1,0,0,0,233,235,1,
|
||||
0,0,0,234,236,3,73,36,0,235,234,1,0,0,0,236,237,1,0,0,0,237,235,1,0,0,0,
|
||||
237,238,1,0,0,0,238,240,1,0,0,0,239,230,1,0,0,0,239,240,1,0,0,0,240,262,
|
||||
1,0,0,0,241,243,3,57,28,0,242,241,1,0,0,0,242,243,1,0,0,0,243,244,1,0,0,
|
||||
0,244,246,5,46,0,0,245,247,3,73,36,0,246,245,1,0,0,0,247,248,1,0,0,0,248,
|
||||
246,1,0,0,0,248,249,1,0,0,0,249,259,1,0,0,0,250,252,7,3,0,0,251,253,3,57,
|
||||
28,0,252,251,1,0,0,0,252,253,1,0,0,0,253,255,1,0,0,0,254,256,3,73,36,0,
|
||||
255,254,1,0,0,0,256,257,1,0,0,0,257,255,1,0,0,0,257,258,1,0,0,0,258,260,
|
||||
1,0,0,0,259,250,1,0,0,0,259,260,1,0,0,0,260,262,1,0,0,0,261,214,1,0,0,0,
|
||||
261,242,1,0,0,0,262,60,1,0,0,0,263,269,5,34,0,0,264,268,8,22,0,0,265,266,
|
||||
5,92,0,0,266,268,9,0,0,0,267,264,1,0,0,0,267,265,1,0,0,0,268,271,1,0,0,
|
||||
0,269,267,1,0,0,0,269,270,1,0,0,0,270,272,1,0,0,0,271,269,1,0,0,0,272,284,
|
||||
5,34,0,0,273,279,5,39,0,0,274,278,8,23,0,0,275,276,5,92,0,0,276,278,9,0,
|
||||
0,0,277,274,1,0,0,0,277,275,1,0,0,0,278,281,1,0,0,0,279,277,1,0,0,0,279,
|
||||
280,1,0,0,0,280,282,1,0,0,0,281,279,1,0,0,0,282,284,5,39,0,0,283,263,1,
|
||||
0,0,0,283,273,1,0,0,0,284,62,1,0,0,0,285,289,7,24,0,0,286,288,7,25,0,0,
|
||||
287,286,1,0,0,0,288,291,1,0,0,0,289,287,1,0,0,0,289,290,1,0,0,0,290,64,
|
||||
1,0,0,0,291,289,1,0,0,0,292,293,5,91,0,0,293,294,5,93,0,0,294,66,1,0,0,
|
||||
0,295,296,5,91,0,0,296,297,5,42,0,0,297,298,5,93,0,0,298,68,1,0,0,0,299,
|
||||
312,3,63,31,0,300,301,5,46,0,0,301,311,3,63,31,0,302,311,3,65,32,0,303,
|
||||
311,3,67,33,0,304,306,5,46,0,0,305,307,3,73,36,0,306,305,1,0,0,0,307,308,
|
||||
1,0,0,0,308,306,1,0,0,0,308,309,1,0,0,0,309,311,1,0,0,0,310,300,1,0,0,0,
|
||||
310,302,1,0,0,0,310,303,1,0,0,0,310,304,1,0,0,0,311,314,1,0,0,0,312,310,
|
||||
1,0,0,0,312,313,1,0,0,0,313,70,1,0,0,0,314,312,1,0,0,0,315,317,7,26,0,0,
|
||||
316,315,1,0,0,0,317,318,1,0,0,0,318,316,1,0,0,0,318,319,1,0,0,0,319,320,
|
||||
1,0,0,0,320,321,6,35,0,0,321,72,1,0,0,0,322,323,7,27,0,0,323,74,1,0,0,0,
|
||||
324,326,8,28,0,0,325,324,1,0,0,0,326,327,1,0,0,0,327,325,1,0,0,0,327,328,
|
||||
1,0,0,0,328,76,1,0,0,0,29,0,90,133,150,209,214,219,225,228,232,237,239,
|
||||
242,248,252,257,259,261,267,269,277,279,283,289,308,310,312,318,327,1,6,
|
||||
0,0];
|
||||
|
||||
private static __ATN: ATN;
|
||||
public static get _ATN(): ATN {
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
// Generated from FilterQuery.g4 by ANTLR 4.13.1
|
||||
// Generated from FilterQuery.g4 by ANTLR 4.13.2
|
||||
|
||||
import {ParseTreeListener} from "antlr4";
|
||||
|
||||
|
||||
import { QueryContext } from "./FilterQueryParser";
|
||||
import { ExpressionContext } from "./FilterQueryParser";
|
||||
import { OrExpressionContext } from "./FilterQueryParser";
|
||||
import { AndExpressionContext } from "./FilterQueryParser";
|
||||
import { UnaryExpressionContext } from "./FilterQueryParser";
|
||||
import { PrimaryContext } from "./FilterQueryParser";
|
||||
import { ComparisonContext } from "./FilterQueryParser";
|
||||
import { InClauseContext } from "./FilterQueryParser";
|
||||
import { NotInClauseContext } from "./FilterQueryParser";
|
||||
import { ValueListContext } from "./FilterQueryParser";
|
||||
import { FullTextContext } from "./FilterQueryParser";
|
||||
import { FunctionCallContext } from "./FilterQueryParser";
|
||||
import { FunctionParamListContext } from "./FilterQueryParser";
|
||||
import { FunctionParamContext } from "./FilterQueryParser";
|
||||
import { ArrayContext } from "./FilterQueryParser";
|
||||
import { ValueContext } from "./FilterQueryParser";
|
||||
import { KeyContext } from "./FilterQueryParser";
|
||||
import { QueryContext } from "./FilterQueryParser.js";
|
||||
import { ExpressionContext } from "./FilterQueryParser.js";
|
||||
import { OrExpressionContext } from "./FilterQueryParser.js";
|
||||
import { AndExpressionContext } from "./FilterQueryParser.js";
|
||||
import { UnaryExpressionContext } from "./FilterQueryParser.js";
|
||||
import { PrimaryContext } from "./FilterQueryParser.js";
|
||||
import { ComparisonContext } from "./FilterQueryParser.js";
|
||||
import { InClauseContext } from "./FilterQueryParser.js";
|
||||
import { NotInClauseContext } from "./FilterQueryParser.js";
|
||||
import { ValueListContext } from "./FilterQueryParser.js";
|
||||
import { FullTextContext } from "./FilterQueryParser.js";
|
||||
import { FunctionCallContext } from "./FilterQueryParser.js";
|
||||
import { SearchCallContext } from "./FilterQueryParser.js";
|
||||
import { FunctionParamListContext } from "./FilterQueryParser.js";
|
||||
import { FunctionParamContext } from "./FilterQueryParser.js";
|
||||
import { ArrayContext } from "./FilterQueryParser.js";
|
||||
import { ValueContext } from "./FilterQueryParser.js";
|
||||
import { KeyContext } from "./FilterQueryParser.js";
|
||||
|
||||
|
||||
/**
|
||||
@@ -147,6 +148,16 @@ export default class FilterQueryListener extends ParseTreeListener {
|
||||
* @param ctx the parse tree
|
||||
*/
|
||||
exitFunctionCall?: (ctx: FunctionCallContext) => void;
|
||||
/**
|
||||
* Enter a parse tree produced by `FilterQueryParser.searchCall`.
|
||||
* @param ctx the parse tree
|
||||
*/
|
||||
enterSearchCall?: (ctx: SearchCallContext) => void;
|
||||
/**
|
||||
* Exit a parse tree produced by `FilterQueryParser.searchCall`.
|
||||
* @param ctx the parse tree
|
||||
*/
|
||||
exitSearchCall?: (ctx: SearchCallContext) => void;
|
||||
/**
|
||||
* Enter a parse tree produced by `FilterQueryParser.functionParamList`.
|
||||
* @param ctx the parse tree
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,25 +1,26 @@
|
||||
// Generated from FilterQuery.g4 by ANTLR 4.13.1
|
||||
// Generated from FilterQuery.g4 by ANTLR 4.13.2
|
||||
|
||||
import {ParseTreeVisitor} from 'antlr4';
|
||||
|
||||
|
||||
import { QueryContext } from "./FilterQueryParser";
|
||||
import { ExpressionContext } from "./FilterQueryParser";
|
||||
import { OrExpressionContext } from "./FilterQueryParser";
|
||||
import { AndExpressionContext } from "./FilterQueryParser";
|
||||
import { UnaryExpressionContext } from "./FilterQueryParser";
|
||||
import { PrimaryContext } from "./FilterQueryParser";
|
||||
import { ComparisonContext } from "./FilterQueryParser";
|
||||
import { InClauseContext } from "./FilterQueryParser";
|
||||
import { NotInClauseContext } from "./FilterQueryParser";
|
||||
import { ValueListContext } from "./FilterQueryParser";
|
||||
import { FullTextContext } from "./FilterQueryParser";
|
||||
import { FunctionCallContext } from "./FilterQueryParser";
|
||||
import { FunctionParamListContext } from "./FilterQueryParser";
|
||||
import { FunctionParamContext } from "./FilterQueryParser";
|
||||
import { ArrayContext } from "./FilterQueryParser";
|
||||
import { ValueContext } from "./FilterQueryParser";
|
||||
import { KeyContext } from "./FilterQueryParser";
|
||||
import { QueryContext } from "./FilterQueryParser.js";
|
||||
import { ExpressionContext } from "./FilterQueryParser.js";
|
||||
import { OrExpressionContext } from "./FilterQueryParser.js";
|
||||
import { AndExpressionContext } from "./FilterQueryParser.js";
|
||||
import { UnaryExpressionContext } from "./FilterQueryParser.js";
|
||||
import { PrimaryContext } from "./FilterQueryParser.js";
|
||||
import { ComparisonContext } from "./FilterQueryParser.js";
|
||||
import { InClauseContext } from "./FilterQueryParser.js";
|
||||
import { NotInClauseContext } from "./FilterQueryParser.js";
|
||||
import { ValueListContext } from "./FilterQueryParser.js";
|
||||
import { FullTextContext } from "./FilterQueryParser.js";
|
||||
import { FunctionCallContext } from "./FilterQueryParser.js";
|
||||
import { SearchCallContext } from "./FilterQueryParser.js";
|
||||
import { FunctionParamListContext } from "./FilterQueryParser.js";
|
||||
import { FunctionParamContext } from "./FilterQueryParser.js";
|
||||
import { ArrayContext } from "./FilterQueryParser.js";
|
||||
import { ValueContext } from "./FilterQueryParser.js";
|
||||
import { KeyContext } from "./FilterQueryParser.js";
|
||||
|
||||
|
||||
/**
|
||||
@@ -102,6 +103,12 @@ export default class FilterQueryVisitor<Result> extends ParseTreeVisitor<Result>
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitFunctionCall?: (ctx: FunctionCallContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `FilterQueryParser.searchCall`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitSearchCall?: (ctx: SearchCallContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `FilterQueryParser.functionParamList`.
|
||||
* @param ctx the parse tree
|
||||
|
||||
@@ -380,6 +380,19 @@ describe('extractQueryPairs', () => {
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('does not turn a search() term into a pair', () => {
|
||||
// The bare form lexes as a KEY; left in it becomes a phantom filter item.
|
||||
expect(extractQueryPairs("search('err')")).toStrictEqual([]);
|
||||
expect(extractQueryPairs('search(err)')).toStrictEqual([]);
|
||||
expect(extractQueryPairs('search(')).toStrictEqual([]);
|
||||
|
||||
expect(
|
||||
extractQueryPairs("search(err) AND service.name = 'api'").map(
|
||||
(pair) => pair.key,
|
||||
),
|
||||
).toStrictEqual(['service.name']);
|
||||
});
|
||||
|
||||
it('should treat lowercase exists as non-value operator', () => {
|
||||
const input = 'body exists service.name contains "test"';
|
||||
const result = extractQueryPairs(input);
|
||||
@@ -821,3 +834,17 @@ describe('getQueryContextAtCursor - partial operator', () => {
|
||||
expect(ctx.operatorToken).toBe('k');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getQueryContextAtCursor - function context', () => {
|
||||
// Each function keyword gets its own lexer token, and every one has to be
|
||||
// registered as a function token for the editor to offer the function list.
|
||||
it.each(['has', 'hasAny', 'hasAll', 'hasToken', 'search'])(
|
||||
'resolves %s to function context',
|
||||
(functionName) => {
|
||||
const ctx = getQueryContextAtCursor(functionName, functionName.length);
|
||||
|
||||
expect(ctx.isInFunction).toBe(true);
|
||||
expect(ctx.isInKey).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1279,6 +1279,41 @@ export function getQueryContextAtCursor(
|
||||
}
|
||||
}
|
||||
|
||||
// The grammar skips whitespace outright, so hidden-channel tokens do not reach the
|
||||
// stream today -- but the rest of this file guards against them, so keep the
|
||||
// assumption in one place rather than spread across callers.
|
||||
function nextVisibleIndex(tokens: IToken[], start: number): number {
|
||||
let index = start;
|
||||
while (index < tokens.length && tokens[index].channel !== 0) {
|
||||
index += 1;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
// Returns the token index just past the parenthesised argument list at
|
||||
// argumentStart, or argumentStart when none follows (a call the user is still
|
||||
// typing). An unclosed list consumes the remainder.
|
||||
function indexPastArguments(tokens: IToken[], argumentStart: number): number {
|
||||
let index = nextVisibleIndex(tokens, argumentStart);
|
||||
if (index >= tokens.length || tokens[index].type !== FilterQueryLexer.LPAREN) {
|
||||
return argumentStart;
|
||||
}
|
||||
|
||||
let depth = 0;
|
||||
for (; index < tokens.length; index++) {
|
||||
if (tokens[index].type === FilterQueryLexer.LPAREN) {
|
||||
depth += 1;
|
||||
} else if (tokens[index].type === FilterQueryLexer.RPAREN) {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
return index + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all key-operator-value triplets from a query string
|
||||
* This is useful for getting value suggestions based on the current key and operator
|
||||
@@ -1324,6 +1359,14 @@ export function extractQueryPairs(query: string): IQueryPair[] {
|
||||
continue;
|
||||
}
|
||||
|
||||
// A search() term is free text, not a key: the bare form search(x) lexes
|
||||
// as one, and left in it surfaces as a phantom filter item -- the log
|
||||
// detail drawer rebuilds its filters from these pairs.
|
||||
if (token.type === FilterQueryLexer.SEARCH) {
|
||||
iterator = indexPastArguments(allTokens, iterator);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If token is a KEY, start a new pair
|
||||
if (
|
||||
token.type === FilterQueryLexer.KEY &&
|
||||
|
||||
@@ -77,6 +77,7 @@ export function isFunctionToken(tokenType: number): boolean {
|
||||
FilterQueryLexer.HASANY,
|
||||
FilterQueryLexer.HASALL,
|
||||
FilterQueryLexer.HASTOKEN,
|
||||
FilterQueryLexer.SEARCH,
|
||||
].includes(tokenType);
|
||||
}
|
||||
|
||||
|
||||
@@ -4135,6 +4135,10 @@ func (r *ClickHouseReader) GetTimeSeriesResultV3(ctx context.Context, query stri
|
||||
return readRowsForTimeSeriesResult(rows, vars, columnNames, countOfNumberCols)
|
||||
}
|
||||
|
||||
func isJSONColumn(columnType driver.ColumnType) bool {
|
||||
return strings.HasPrefix(strings.ToUpper(columnType.DatabaseTypeName()), "JSON")
|
||||
}
|
||||
|
||||
// GetListResultV3 runs the query and returns list of rows
|
||||
func (r *ClickHouseReader) GetListResultV3(ctx context.Context, query string) ([]*v3.Row, error) {
|
||||
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
|
||||
@@ -4159,6 +4163,12 @@ func (r *ClickHouseReader) GetListResultV3(ctx context.Context, query string) ([
|
||||
for rows.Next() {
|
||||
var vars = make([]interface{}, len(columnTypes))
|
||||
for i := range columnTypes {
|
||||
if isJSONColumn(columnTypes[i]) {
|
||||
// the driver fails to decode JSON into native Go values, so it is read as raw bytes
|
||||
var raw []byte
|
||||
vars[i] = &raw
|
||||
continue
|
||||
}
|
||||
vars[i] = reflect.New(columnTypes[i].ScanType()).Interface()
|
||||
}
|
||||
if err := rows.Scan(vars...); err != nil {
|
||||
@@ -4167,7 +4177,17 @@ func (r *ClickHouseReader) GetListResultV3(ctx context.Context, query string) ([
|
||||
row := map[string]interface{}{}
|
||||
var t time.Time
|
||||
for idx, v := range vars {
|
||||
if columnNames[idx] == "timestamp" {
|
||||
if isJSONColumn(columnTypes[idx]) {
|
||||
raw, ok := v.(*[]byte)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
var value map[string]interface{}
|
||||
if err := json.Unmarshal(*raw, &value); err != nil {
|
||||
return nil, errors.New(err.Error())
|
||||
}
|
||||
row[columnNames[idx]] = value
|
||||
} else if columnNames[idx] == "timestamp" {
|
||||
switch v := v.(type) {
|
||||
case *uint64:
|
||||
t = time.Unix(0, int64(*v))
|
||||
|
||||
@@ -3803,6 +3803,10 @@ func (aH *APIHandler) QueryRangeV3(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
queryRangeParams.UseJSONBody = aH.Signoz.Flagger.BooleanOrEmpty(
|
||||
r.Context(), flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(orgID),
|
||||
)
|
||||
|
||||
// add temporality for each metric
|
||||
temporalityErr := aH.PopulateTemporality(r.Context(), orgID, queryRangeParams)
|
||||
if temporalityErr != nil {
|
||||
|
||||
@@ -361,7 +361,7 @@ func generateAggregateClause(panelType v3.PanelType, start, end int64, aggOp v3.
|
||||
}
|
||||
}
|
||||
|
||||
func buildLogsQuery(panelType v3.PanelType, start, end, step int64, mq *v3.BuilderQuery, graphLimitQtype string) (string, error) {
|
||||
func buildLogsQuery(panelType v3.PanelType, start, end, step int64, mq *v3.BuilderQuery, graphLimitQtype string, useJSONBody bool) (string, error) {
|
||||
// timerange will be sent in epoch millisecond
|
||||
logsStart := utils.GetEpochNanoSecs(start)
|
||||
logsEnd := utils.GetEpochNanoSecs(end)
|
||||
@@ -405,6 +405,9 @@ func buildLogsQuery(panelType v3.PanelType, start, end, step int64, mq *v3.Build
|
||||
if mq.AggregateOperator == v3.AggregateOperatorNoOp {
|
||||
// with noop any filter or different order by other than ts will use new table
|
||||
sqlSelect := constants.LogsSQLSelectV2
|
||||
if useJSONBody {
|
||||
sqlSelect = constants.LogsSQLSelectV2WithBodyJSON
|
||||
}
|
||||
queryTmpl := sqlSelect + "from signoz_logs.%s where %s%s order by %s"
|
||||
query := fmt.Sprintf(queryTmpl, DISTRIBUTED_LOGS_V2, timeFilter, filterSubQuery, orderBy)
|
||||
return query, nil
|
||||
@@ -517,7 +520,7 @@ func PrepareLogsQuery(start, end int64, queryType v3.QueryType, panelType v3.Pan
|
||||
return query, nil
|
||||
} else if options.GraphLimitQtype == constants.FirstQueryGraphLimit {
|
||||
// give me just the group_by names (no values)
|
||||
query, err := buildLogsQuery(panelType, start, end, mq.StepInterval, mq, options.GraphLimitQtype)
|
||||
query, err := buildLogsQuery(panelType, start, end, mq.StepInterval, mq, options.GraphLimitQtype, options.UseJSONBody)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -525,14 +528,14 @@ func PrepareLogsQuery(start, end int64, queryType v3.QueryType, panelType v3.Pan
|
||||
|
||||
return query, nil
|
||||
} else if options.GraphLimitQtype == constants.SecondQueryGraphLimit {
|
||||
query, err := buildLogsQuery(panelType, start, end, mq.StepInterval, mq, options.GraphLimitQtype)
|
||||
query, err := buildLogsQuery(panelType, start, end, mq.StepInterval, mq, options.GraphLimitQtype, options.UseJSONBody)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return query, nil
|
||||
}
|
||||
|
||||
query, err := buildLogsQuery(panelType, start, end, mq.StepInterval, mq, options.GraphLimitQtype)
|
||||
query, err := buildLogsQuery(panelType, start, end, mq.StepInterval, mq, options.GraphLimitQtype, options.UseJSONBody)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -899,7 +899,7 @@ func Test_buildLogsQuery(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := buildLogsQuery(tt.args.panelType, tt.args.start, tt.args.end, tt.args.step, tt.args.mq, tt.args.graphLimitQtype)
|
||||
got, err := buildLogsQuery(tt.args.panelType, tt.args.start, tt.args.end, tt.args.step, tt.args.mq, tt.args.graphLimitQtype, false)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("buildLogsQuery() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
|
||||
@@ -215,18 +215,18 @@ func (qb *QueryBuilder) PrepareQueries(params *v3.QueryRangeParamsV3) (map[strin
|
||||
case v3.DataSourceLogs:
|
||||
// for ts query with limit replace it as it is already formed
|
||||
if compositeQuery.PanelType == v3.PanelTypeGraph && query.Limit > 0 && len(query.GroupBy) > 0 {
|
||||
limitQuery, err := qb.options.BuildLogQuery(start, end, compositeQuery.QueryType, compositeQuery.PanelType, query, v3.QBOptions{GraphLimitQtype: constants.FirstQueryGraphLimit})
|
||||
limitQuery, err := qb.options.BuildLogQuery(start, end, compositeQuery.QueryType, compositeQuery.PanelType, query, v3.QBOptions{GraphLimitQtype: constants.FirstQueryGraphLimit, UseJSONBody: params.UseJSONBody})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
placeholderQuery, err := qb.options.BuildLogQuery(start, end, compositeQuery.QueryType, compositeQuery.PanelType, query, v3.QBOptions{GraphLimitQtype: constants.SecondQueryGraphLimit})
|
||||
placeholderQuery, err := qb.options.BuildLogQuery(start, end, compositeQuery.QueryType, compositeQuery.PanelType, query, v3.QBOptions{GraphLimitQtype: constants.SecondQueryGraphLimit, UseJSONBody: params.UseJSONBody})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := fmt.Sprintf(placeholderQuery, limitQuery)
|
||||
queries[queryName] = query
|
||||
} else {
|
||||
queryString, err := qb.options.BuildLogQuery(start, end, compositeQuery.QueryType, compositeQuery.PanelType, query, v3.QBOptions{GraphLimitQtype: ""})
|
||||
queryString, err := qb.options.BuildLogQuery(start, end, compositeQuery.QueryType, compositeQuery.PanelType, query, v3.QBOptions{GraphLimitQtype: "", UseJSONBody: params.UseJSONBody})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -196,13 +196,17 @@ const (
|
||||
"CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool," +
|
||||
"CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string," +
|
||||
"CAST((scope_string_key, scope_string_value), 'Map(String, String)') as scope "
|
||||
LogsSQLSelectV2 = "SELECT " +
|
||||
"timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, " +
|
||||
"attributes_string, " +
|
||||
logsSQLSelectV2Head = "SELECT " +
|
||||
"timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, "
|
||||
logsSQLSelectV2Tail = "attributes_string, " +
|
||||
"attributes_number, " +
|
||||
"attributes_bool, " +
|
||||
"resources_string, " +
|
||||
"scope_string "
|
||||
LogsSQLSelectV2 = logsSQLSelectV2Head + "body, " + logsSQLSelectV2Tail
|
||||
// Orgs on JSON bodies keep the body in body_v2 and have the body column written empty.
|
||||
// Selected as JSON so the response carries the same body object v5 returns.
|
||||
LogsSQLSelectV2WithBodyJSON = logsSQLSelectV2Head + "body_v2 as body, " + logsSQLSelectV2Tail
|
||||
TracesExplorerViewSQLSelectWithSubQuery = "(SELECT traceID, durationNano, " +
|
||||
"serviceName, name FROM %s.%s WHERE parentSpanID = '' AND %s ORDER BY durationNano DESC LIMIT 1 BY traceID"
|
||||
TracesExplorerViewSQLSelectBeforeSubQuery = "SELECT subQuery.serviceName as `subQuery.serviceName`, subQuery.name as `subQuery.name`, count() AS " +
|
||||
|
||||
@@ -435,6 +435,8 @@ type QueryRangeParamsV3 struct {
|
||||
NoCache bool `json:"noCache"`
|
||||
Version string `json:"-"`
|
||||
FormatForWeb bool `json:"formatForWeb,omitempty"`
|
||||
// Resolved from the use_json_body feature flag by the handler, never sent by clients.
|
||||
UseJSONBody bool `json:"-"`
|
||||
}
|
||||
|
||||
func (q *QueryRangeParamsV3) Clone() *QueryRangeParamsV3 {
|
||||
@@ -450,6 +452,7 @@ func (q *QueryRangeParamsV3) Clone() *QueryRangeParamsV3 {
|
||||
NoCache: q.NoCache,
|
||||
Version: q.Version,
|
||||
FormatForWeb: q.FormatForWeb,
|
||||
UseJSONBody: q.UseJSONBody,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1469,4 +1472,5 @@ type MetricMetadataResponse struct {
|
||||
type QBOptions struct {
|
||||
GraphLimitQtype string
|
||||
IsLivetailQuery bool
|
||||
UseJSONBody bool
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@ echo "Generating TypeScript parser..."
|
||||
mkdir -p frontend/src/parser
|
||||
|
||||
# Generate TypeScript parser
|
||||
antlr4 -Dlanguage=TypeScript -o frontend/src/parser grammar/FilterQuery.g4 -visitor
|
||||
(cd grammar && antlr4 -Dlanguage=TypeScript -o ../frontend/src/parser FilterQuery.g4 -visitor)
|
||||
|
||||
echo "TypeScript parser generation complete"
|
||||
|
||||
46
tests/fixtures/auth.py
vendored
46
tests/fixtures/auth.py
vendored
@@ -16,6 +16,7 @@ from wiremock.resources.mappings import (
|
||||
|
||||
from fixtures import reuse, types
|
||||
from fixtures.logger import setup_logger
|
||||
from fixtures.role import find_role_by_name
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
@@ -32,6 +33,7 @@ USER_VIEWER_EMAIL = "viewer@integration.test"
|
||||
USER_VIEWER_PASSWORD = "password123Z$"
|
||||
|
||||
USERS_BASE = "/api/v2/users"
|
||||
USER_ROLES_BASE = "/api/v2/user_roles"
|
||||
|
||||
|
||||
def _login(signoz: types.SigNoz, email: str, password: str) -> str:
|
||||
@@ -344,24 +346,38 @@ def create_active_user(
|
||||
password: str,
|
||||
name: str = "",
|
||||
) -> str:
|
||||
"""Invite a user and activate via resetPassword. Returns user ID."""
|
||||
"""Create a pending invite user and activate it by setting a password.
|
||||
|
||||
role is a managed role name, e.g. signoz-viewer. Returns the user ID.
|
||||
"""
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
||||
json={"email": email, "role": role, "name": name},
|
||||
signoz.self.host_configs["8080"].get(USERS_BASE),
|
||||
json={
|
||||
"email": email,
|
||||
"displayName": name,
|
||||
"userRoles": [{"id": find_role_by_name(signoz, admin_token, role)}],
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
user_id = response.json()["data"]["id"]
|
||||
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"{USERS_BASE}/{user_id}/reset_password_tokens"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
invited_user = response.json()["data"]
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/resetPassword"),
|
||||
json={"password": password, "token": invited_user["token"]},
|
||||
signoz.self.host_configs["8080"].get("/api/v2/factor_password/reset"),
|
||||
json={"password": password, "token": response.json()["data"]["token"]},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
|
||||
|
||||
return invited_user["id"]
|
||||
return user_id
|
||||
|
||||
|
||||
def find_user_by_email(signoz: types.SigNoz, token: str, email: str) -> dict:
|
||||
@@ -409,21 +425,21 @@ def change_user_role(
|
||||
|
||||
Role names should be managed role names (e.g. signoz-editor).
|
||||
"""
|
||||
# Get current roles to find the old role's ID
|
||||
# Get current roles to find the old role's user_role entry ID
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{USERS_BASE}/{user_id}/roles"),
|
||||
signoz.self.host_configs["8080"].get(f"{USERS_BASE}/{user_id}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
roles = response.json()["data"]
|
||||
user_roles = response.json()["data"]["userRoles"]
|
||||
|
||||
old_role_entry = next((r for r in roles if r["name"] == old_role), None)
|
||||
old_role_entry = next((ur for ur in user_roles if ur["role"]["name"] == old_role), None)
|
||||
assert old_role_entry is not None, f"User does not have role '{old_role}'"
|
||||
|
||||
# Remove old role
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"{USERS_BASE}/{user_id}/roles/{old_role_entry['id']}"),
|
||||
signoz.self.host_configs["8080"].get(f"{USER_ROLES_BASE}/{old_role_entry['id']}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
@@ -431,9 +447,9 @@ def change_user_role(
|
||||
|
||||
# Assign new role
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(f"{USERS_BASE}/{user_id}/roles"),
|
||||
json={"name": new_role},
|
||||
signoz.self.host_configs["8080"].get(USER_ROLES_BASE),
|
||||
json={"userId": user_id, "roleId": find_role_by_name(signoz, admin_token, new_role)},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
|
||||
12
tests/fixtures/idp.py
vendored
12
tests/fixtures/idp.py
vendored
@@ -634,18 +634,6 @@ def get_oidc_domain(signoz: types.SigNoz, admin_token: str) -> dict:
|
||||
)
|
||||
|
||||
|
||||
def get_user_by_email(signoz: types.SigNoz, admin_token: str, email: str) -> dict:
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/user"),
|
||||
timeout=2,
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
)
|
||||
return next(
|
||||
(user for user in response.json()["data"] if user["email"] == email),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def perform_oidc_login(
|
||||
signoz: types.SigNoz, # pylint: disable=unused-argument
|
||||
idp: types.TestContainerIDP,
|
||||
|
||||
20
tests/fixtures/role.py
vendored
20
tests/fixtures/role.py
vendored
@@ -9,18 +9,14 @@ from fixtures import types
|
||||
from fixtures.fs import get_testdata_file_path
|
||||
|
||||
|
||||
@pytest.fixture(name="find_role_id", scope="function")
|
||||
def find_role_id(signoz: types.SigNoz) -> Callable[[str, str], str]:
|
||||
def _find(token: str, name: str) -> str:
|
||||
resp = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/roles"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.OK, resp.text
|
||||
return next(r["id"] for r in resp.json()["data"] if r["name"] == name)
|
||||
|
||||
return _find
|
||||
def find_role_by_name(signoz: types.SigNoz, token: str, name: str) -> str:
|
||||
resp = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/roles"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.OK, resp.text
|
||||
return next(r["id"] for r in resp.json()["data"] if r["name"] == name)
|
||||
|
||||
|
||||
@pytest.fixture(name="create_role", scope="function")
|
||||
|
||||
11
tests/fixtures/serviceaccount.py
vendored
11
tests/fixtures/serviceaccount.py
vendored
@@ -4,22 +4,13 @@ import requests
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.logger import setup_logger
|
||||
from fixtures.role import find_role_by_name
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
SERVICE_ACCOUNT_BASE = "/api/v1/service_accounts"
|
||||
|
||||
|
||||
def find_role_by_name(signoz: types.SigNoz, token: str, name: str) -> str:
|
||||
resp = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/roles"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.OK, resp.text
|
||||
return next(r["id"] for r in resp.json()["data"] if r["name"] == name)
|
||||
|
||||
|
||||
def create_service_account(signoz: types.SigNoz, token: str, name: str, role: str = "signoz-viewer") -> str:
|
||||
"""Create a service account, assign a role, and return its ID."""
|
||||
resp = requests.post(
|
||||
|
||||
@@ -13,12 +13,14 @@ from fixtures.auth import (
|
||||
USER_ADMIN_PASSWORD,
|
||||
add_license,
|
||||
assert_user_has_role,
|
||||
create_active_user,
|
||||
find_user_with_roles_by_email,
|
||||
)
|
||||
from fixtures.idp import (
|
||||
get_saml_domain,
|
||||
perform_saml_login,
|
||||
)
|
||||
from fixtures.role import find_role_by_name
|
||||
from fixtures.types import Operation, SigNoz, TestContainerDocker, TestContainerIDP
|
||||
|
||||
|
||||
@@ -509,8 +511,12 @@ def test_saml_sso_login_activates_pending_invite_user(
|
||||
|
||||
# Invite user as ADMIN
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
||||
json={"email": email, "role": "ADMIN", "name": "SAML SSO Pending User"},
|
||||
signoz.self.host_configs["8080"].get("/api/v2/users"),
|
||||
json={
|
||||
"email": email,
|
||||
"displayName": "SAML SSO Pending User",
|
||||
"userRoles": [{"id": find_role_by_name(signoz, admin_token, "signoz-admin")}],
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
@@ -547,22 +553,14 @@ def test_saml_sso_deleted_user_gets_new_user_on_login(
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# --- Step 1: Invite and activate via password reset ---
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
||||
json={"email": email, "role": "EDITOR", "name": "SAML SSO Lifecycle User"},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
user_id = create_active_user(
|
||||
signoz,
|
||||
admin_token,
|
||||
email=email,
|
||||
role="signoz-editor",
|
||||
password="password123Z$",
|
||||
name="SAML SSO Lifecycle User",
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED
|
||||
user_id = response.json()["data"]["id"]
|
||||
reset_token = response.json()["data"]["token"]
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/resetPassword"),
|
||||
json={"password": "password123Z$", "token": reset_token},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
|
||||
# --- Step 2: Soft delete via DB using API
|
||||
response = requests.delete(
|
||||
|
||||
@@ -17,6 +17,7 @@ from fixtures.idp import (
|
||||
get_oidc_domain,
|
||||
perform_oidc_login,
|
||||
)
|
||||
from fixtures.role import find_role_by_name
|
||||
from fixtures.types import Operation, SigNoz, TestContainerDocker, TestContainerIDP
|
||||
|
||||
|
||||
@@ -464,8 +465,12 @@ def test_oidc_sso_login_activates_pending_invite_user(
|
||||
|
||||
# Invite user as ADMIN
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
||||
json={"email": email, "role": "ADMIN", "name": "OIDC SSO Pending User"},
|
||||
signoz.self.host_configs["8080"].get("/api/v2/users"),
|
||||
json={
|
||||
"email": email,
|
||||
"displayName": "OIDC SSO Pending User",
|
||||
"userRoles": [{"id": find_role_by_name(signoz, admin_token, "signoz-admin")}],
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ from fixtures.auth import (
|
||||
create_active_user,
|
||||
find_user_by_email,
|
||||
)
|
||||
from fixtures.role import transaction_group
|
||||
from fixtures.role import find_role_by_name, transaction_group
|
||||
from fixtures.types import Operation, SigNoz, TestContainerDocker
|
||||
|
||||
V2_BASE_URL = "/api/v2/dashboards"
|
||||
@@ -64,8 +64,8 @@ def test_setup_managed_role_users(
|
||||
existing_emails = {user["email"] for user in response.json()["data"]}
|
||||
|
||||
for email, role, password, name in (
|
||||
(_EDITOR_EMAIL, "EDITOR", _EDITOR_PASSWORD, "dashboard authz editor"),
|
||||
(_VIEWER_EMAIL, "VIEWER", _VIEWER_PASSWORD, "dashboard authz viewer"),
|
||||
(_EDITOR_EMAIL, "signoz-editor", _EDITOR_PASSWORD, "dashboard authz editor"),
|
||||
(_VIEWER_EMAIL, "signoz-viewer", _VIEWER_PASSWORD, "dashboard authz viewer"),
|
||||
):
|
||||
if email not in existing_emails:
|
||||
create_active_user(signoz, admin_token, email=email, role=role, password=password, name=name)
|
||||
@@ -411,7 +411,7 @@ def test_setup_scoped_actor(
|
||||
],
|
||||
)
|
||||
|
||||
user_id = create_active_user(signoz, admin_token, email=_ACTOR_EMAIL, role="VIEWER", password=_ACTOR_PASSWORD, name="dashboard fga actor")
|
||||
user_id = create_active_user(signoz, admin_token, email=_ACTOR_EMAIL, role="signoz-viewer", password=_ACTOR_PASSWORD, name="dashboard fga actor")
|
||||
change_user_role(signoz, admin_token, user_id, "signoz-viewer", _ACTOR_ROLE_NAME)
|
||||
|
||||
|
||||
@@ -469,10 +469,9 @@ def test_publish_and_unpublish_require_update_on_the_dashboard(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
find_role_id: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
actor_role_id = find_role_id(admin_token, _ACTOR_ROLE_NAME)
|
||||
actor_role_id = find_role_by_name(signoz, admin_token, _ACTOR_ROLE_NAME)
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{V2_BASE_URL}?limit={MAX_LIST_LIMIT}"),
|
||||
@@ -579,7 +578,6 @@ def test_dashboard_authz_cleanup(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
find_role_id: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
actor = find_user_by_email(signoz, admin_token, _ACTOR_EMAIL)
|
||||
@@ -600,7 +598,7 @@ def test_dashboard_authz_cleanup(
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT, f"remove role from user: {response.text}"
|
||||
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/roles/{find_role_id(admin_token, _ACTOR_ROLE_NAME)}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/roles/{find_role_by_name(signoz, admin_token, _ACTOR_ROLE_NAME)}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ from wiremock.client import (
|
||||
)
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD, add_license
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD, add_license, create_active_user
|
||||
from fixtures.gateway import (
|
||||
TEST_KEY_ID,
|
||||
common_gateway_headers,
|
||||
@@ -43,21 +43,13 @@ def test_create_editor_user(
|
||||
"""Invite and register an editor user for gateway API tests."""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
invite_response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
||||
json={"email": GATEWAY_APIS_EDITOR_EMAIL, "role": "EDITOR"},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
create_active_user(
|
||||
signoz,
|
||||
admin_token,
|
||||
email=GATEWAY_APIS_EDITOR_EMAIL,
|
||||
role="signoz-editor",
|
||||
password=GATEWAY_APIS_EDITOR_PASSWORD,
|
||||
)
|
||||
assert invite_response.status_code == HTTPStatus.CREATED
|
||||
reset_token = invite_response.json()["data"]["token"]
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/resetPassword"),
|
||||
json={"password": GATEWAY_APIS_EDITOR_PASSWORD, "token": reset_token},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -15,6 +15,7 @@ from fixtures.auth import (
|
||||
find_user_with_roles_by_email,
|
||||
)
|
||||
from fixtures.logger import setup_logger
|
||||
from fixtures.role import find_role_by_name
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
@@ -82,31 +83,36 @@ def test_register(signoz: types.SigNoz, get_token: Callable[[str, str], str]) ->
|
||||
|
||||
def test_invite(signoz: types.SigNoz, get_token: Callable[[str, str], str]) -> None:
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
# Generate an invite token for the editor user
|
||||
# Create the editor user as a pending invite
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
||||
json={"email": USER_EDITOR_EMAIL, "role": "EDITOR", "name": USER_EDITOR_NAME},
|
||||
signoz.self.host_configs["8080"].get("/api/v2/users"),
|
||||
json={
|
||||
"email": USER_EDITOR_EMAIL,
|
||||
"displayName": USER_EDITOR_NAME,
|
||||
"userRoles": [{"id": find_role_by_name(signoz, admin_token, "signoz-editor")}],
|
||||
},
|
||||
timeout=2,
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
|
||||
invited_user = response.json()["data"]
|
||||
assert invited_user["email"] == USER_EDITOR_EMAIL
|
||||
assert invited_user["role"] == "EDITOR"
|
||||
user_id = response.json()["data"]["id"]
|
||||
|
||||
# Verify the user appears in the users list but as pending_invite status
|
||||
found_user = find_user_with_roles_by_email(signoz, admin_token, USER_EDITOR_EMAIL)
|
||||
assert found_user["status"] == "pending_invite"
|
||||
assert_user_has_role(found_user, "signoz-editor")
|
||||
|
||||
reset_token = invited_user["token"]
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{user_id}/reset_password_tokens"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
|
||||
# Reset the password to complete the invite flow (activates the user and also grants authz)
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/resetPassword"),
|
||||
json={"password": USER_EDITOR_PASSWORD, "token": reset_token},
|
||||
signoz.self.host_configs["8080"].get("/api/v2/factor_password/reset"),
|
||||
json={"password": USER_EDITOR_PASSWORD, "token": response.json()["data"]["token"]},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
@@ -130,18 +136,28 @@ def test_revoke_invite(signoz: types.SigNoz, get_token: Callable[[str, str], str
|
||||
|
||||
# Invite the viewer user
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
||||
json={"email": USER_VIEWER_EMAIL, "role": "VIEWER"},
|
||||
signoz.self.host_configs["8080"].get("/api/v2/users"),
|
||||
json={
|
||||
"email": USER_VIEWER_EMAIL,
|
||||
"userRoles": [{"id": find_role_by_name(signoz, admin_token, "signoz-viewer")}],
|
||||
},
|
||||
timeout=2,
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
invited_user = response.json()["data"]
|
||||
reset_token = invited_user["token"]
|
||||
user_id = response.json()["data"]["id"]
|
||||
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{user_id}/reset_password_tokens"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
reset_token = response.json()["data"]["token"]
|
||||
|
||||
# Delete the pending invite user (revoke the invite)
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{invited_user['id']}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{user_id}"),
|
||||
timeout=2,
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
)
|
||||
@@ -149,7 +165,7 @@ def test_revoke_invite(signoz: types.SigNoz, get_token: Callable[[str, str], str
|
||||
|
||||
# Try to use the reset token — should fail (user deleted)
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/resetPassword"),
|
||||
signoz.self.host_configs["8080"].get("/api/v2/factor_password/reset"),
|
||||
json={"password": "password123Z$", "token": reset_token},
|
||||
timeout=2,
|
||||
)
|
||||
@@ -157,67 +173,83 @@ def test_revoke_invite(signoz: types.SigNoz, get_token: Callable[[str, str], str
|
||||
|
||||
|
||||
def test_provision_user(signoz: types.SigNoz, get_token: Callable[[str, str], str]) -> None:
|
||||
"""
|
||||
Simulates the upstream zeus provisioning flow:
|
||||
1. Invite a user as ADMIN (register already happened via test_register)
|
||||
2. List users to find the invited user's ID
|
||||
3. Get reset password token for that user
|
||||
4. Use the token to set the password and activate the user
|
||||
5. Verify the user can log in
|
||||
"""
|
||||
"""Mirrors the zeus provisioning flow."""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
provisioned_email = "zeus-provisioned@integration.test"
|
||||
provisioned_name = "zeus provisioned user"
|
||||
provisioned_password = "password123Z$"
|
||||
|
||||
# Step 1: Invite user as ADMIN (mirrors zeus inviteUserOnSigNoz)
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/roles"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
role_id = next(role["id"] for role in response.json()["data"] if role["name"] == "signoz-admin")
|
||||
|
||||
create_payload = {
|
||||
"email": provisioned_email,
|
||||
"displayName": provisioned_name,
|
||||
"userRoles": [{"id": role_id}],
|
||||
}
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
||||
json={
|
||||
"email": provisioned_email,
|
||||
"name": provisioned_name,
|
||||
"role": "ADMIN",
|
||||
},
|
||||
signoz.self.host_configs["8080"].get("/api/v2/users"),
|
||||
json=create_payload,
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
user_id = response.json()["data"]["id"]
|
||||
|
||||
# Step 2: List users to find the invited user's ID (mirrors zeus GET /api/v1/user)
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/user"),
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/users"),
|
||||
json=create_payload,
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
users = response.json()["data"]
|
||||
found_user = next((u for u in users if u["email"] == provisioned_email), None)
|
||||
assert found_user is not None
|
||||
user_id = found_user["id"]
|
||||
assert response.status_code == HTTPStatus.CONFLICT, response.text
|
||||
|
||||
# Step 3: Get reset password token (mirrors zeus GET /api/v1/getResetPasswordToken/{id})
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/getResetPasswordToken/{user_id}"),
|
||||
signoz.self.host_configs["8080"].get("/api/v2/users"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
existing_id = next(user["id"] for user in response.json()["data"] if user["email"] == provisioned_email.strip().lower())
|
||||
assert existing_id == user_id
|
||||
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{user_id}/reset_password_tokens"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
reset_token = response.json()["data"]["token"]
|
||||
assert reset_token is not None
|
||||
assert reset_token != ""
|
||||
|
||||
# Step 4: Use the token to set password and activate user
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/resetPassword"),
|
||||
signoz.self.host_configs["8080"].get("/api/v2/factor_password/reset"),
|
||||
json={"password": provisioned_password, "token": reset_token},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
|
||||
|
||||
# Step 5: Verify the provisioned user can log in and is active with admin role
|
||||
user_token = get_token(provisioned_email, provisioned_password)
|
||||
assert user_token is not None
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/sessions/context"),
|
||||
params={"email": provisioned_email, "ref": f"{signoz.self.host_configs['8080'].base()}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
org_id = response.json()["data"]["orgs"][0]["id"]
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/sessions/email_password"),
|
||||
json={"email": provisioned_email, "password": provisioned_password, "orgId": org_id},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.json()["data"]["accessToken"] != ""
|
||||
|
||||
provisioned_user = find_user_with_roles_by_email(signoz, admin_token, provisioned_email)
|
||||
assert provisioned_user["status"] == "active"
|
||||
|
||||
@@ -5,7 +5,7 @@ import requests
|
||||
from sqlalchemy import sql
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD, find_user_by_email
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD, create_active_user, find_user_by_email
|
||||
from fixtures.logger import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
@@ -17,24 +17,13 @@ PASSWORD_USER_PASSWORD = "password123Z$"
|
||||
def test_change_password(signoz: types.SigNoz, get_token: Callable[[str, str], str]) -> None:
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Create another admin user
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
||||
json={"email": PASSWORD_USER_EMAIL, "role": "ADMIN"},
|
||||
timeout=2,
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
create_active_user(
|
||||
signoz,
|
||||
admin_token,
|
||||
email=PASSWORD_USER_EMAIL,
|
||||
role="signoz-admin",
|
||||
password=PASSWORD_USER_PASSWORD,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
invited_user = response.json()["data"]
|
||||
reset_token = invited_user["token"]
|
||||
|
||||
# Reset password to activate user
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/resetPassword"),
|
||||
json={"password": PASSWORD_USER_PASSWORD, "token": reset_token},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
|
||||
# Try logging in with the password
|
||||
token = get_token(PASSWORD_USER_EMAIL, PASSWORD_USER_PASSWORD)
|
||||
@@ -110,7 +99,7 @@ def test_reset_password(signoz: types.SigNoz, get_token: Callable[[str, str], st
|
||||
|
||||
# Reset the password with a bad password which should fail
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/resetPassword"),
|
||||
signoz.self.host_configs["8080"].get("/api/v2/factor_password/reset"),
|
||||
json={"password": "password", "token": token},
|
||||
timeout=2,
|
||||
)
|
||||
@@ -119,51 +108,19 @@ def test_reset_password(signoz: types.SigNoz, get_token: Callable[[str, str], st
|
||||
|
||||
# Reset the password with a good password
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/resetPassword"),
|
||||
signoz.self.host_configs["8080"].get("/api/v2/factor_password/reset"),
|
||||
json={"password": "password123Z$NEWNEW#!", "token": token},
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
|
||||
token = get_token(PASSWORD_USER_EMAIL, "password123Z$NEWNEW#!")
|
||||
assert token is not None
|
||||
|
||||
|
||||
def test_reset_password_v2(signoz: types.SigNoz, get_token: Callable[[str, str], str]) -> None:
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
found_user = find_user_by_email(signoz, admin_token, PASSWORD_USER_EMAIL)
|
||||
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{found_user['id']}/reset_password_tokens"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
token = response.json()["data"]["token"]
|
||||
|
||||
# A password failing the strength policy is rejected without consuming the token
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/factor_password/reset"),
|
||||
json={"password": "password", "token": token},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST, response.text
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/factor_password/reset"),
|
||||
json={"password": "resetV2Password123Z$", "token": token},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
|
||||
|
||||
assert get_token(PASSWORD_USER_EMAIL, "resetV2Password123Z$") is not None
|
||||
assert get_token(PASSWORD_USER_EMAIL, "password123Z$NEWNEW#!") is not None
|
||||
|
||||
# The token is single use, so replaying it no longer resolves
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/factor_password/reset"),
|
||||
json={"password": "resetV2Password456Z$", "token": token},
|
||||
json={"password": "password123Z$REPLAY#!", "token": token},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NOT_FOUND, response.text
|
||||
@@ -204,7 +161,7 @@ def test_reset_password_with_no_password(signoz: types.SigNoz, get_token: Callab
|
||||
|
||||
# Reset the password with a good password
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/resetPassword"),
|
||||
signoz.self.host_configs["8080"].get("/api/v2/factor_password/reset"),
|
||||
json={"password": "FINALPASSword123!#[", "token": token},
|
||||
timeout=2,
|
||||
)
|
||||
@@ -262,28 +219,14 @@ def test_forgot_password_creates_reset_token(signoz: types.SigNoz, get_token: Ca
|
||||
forgot_email = "forgot@integration.test"
|
||||
|
||||
# Create a user specifically for testing forgot password
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
||||
json={
|
||||
"email": forgot_email,
|
||||
"role": "EDITOR",
|
||||
"name": "forgotpassword user",
|
||||
},
|
||||
timeout=2,
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
create_active_user(
|
||||
signoz,
|
||||
admin_token,
|
||||
email=forgot_email,
|
||||
role="signoz-editor",
|
||||
password="originalPassword123Z$",
|
||||
name="forgotpassword user",
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
|
||||
invited_user = response.json()["data"]
|
||||
reset_token = invited_user["token"]
|
||||
|
||||
# Activate user via reset password
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/resetPassword"),
|
||||
json={"password": "originalPassword123Z$", "token": reset_token},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
|
||||
# Get org ID
|
||||
response = requests.get(
|
||||
@@ -326,7 +269,7 @@ def test_forgot_password_creates_reset_token(signoz: types.SigNoz, get_token: Ca
|
||||
|
||||
# Reset password with a valid strong password
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/resetPassword"),
|
||||
signoz.self.host_configs["8080"].get("/api/v2/factor_password/reset"),
|
||||
json={"password": "newSecurePassword123Z$!", "token": reset_token},
|
||||
timeout=2,
|
||||
)
|
||||
@@ -416,7 +359,7 @@ def test_reset_password_with_expired_token(signoz: types.SigNoz, get_token: Call
|
||||
|
||||
# Try to use the expired token - should fail with 401 Unauthorized
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/resetPassword"),
|
||||
signoz.self.host_configs["8080"].get("/api/v2/factor_password/reset"),
|
||||
json={"password": "expiredTokenPassword123Z$!", "token": reset_token},
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@ from fixtures.auth import (
|
||||
change_user_role,
|
||||
create_active_user,
|
||||
)
|
||||
from fixtures.role import find_role_by_name
|
||||
|
||||
ROLECHANGE_USER_EMAIL = "admin+rolechange@integration.test"
|
||||
ROLECHANGE_USER_PASSWORD = "password123Z$"
|
||||
@@ -23,27 +24,14 @@ def test_change_role(
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Create a new user as VIEWER
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
||||
json={"email": ROLECHANGE_USER_EMAIL, "role": "VIEWER"},
|
||||
timeout=2,
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
create_active_user(
|
||||
signoz,
|
||||
admin_token,
|
||||
email=ROLECHANGE_USER_EMAIL,
|
||||
role="signoz-viewer",
|
||||
password=ROLECHANGE_USER_PASSWORD,
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
|
||||
invited_user = response.json()["data"]
|
||||
reset_token = invited_user["token"]
|
||||
|
||||
# Activate user via reset password
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/resetPassword"),
|
||||
json={"password": ROLECHANGE_USER_PASSWORD, "token": reset_token},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
|
||||
# Make some API calls as new user
|
||||
new_user_token = get_token(ROLECHANGE_USER_EMAIL, ROLECHANGE_USER_PASSWORD)
|
||||
|
||||
@@ -133,7 +121,7 @@ def test_assign_role_is_additive(
|
||||
signoz: types.SigNoz,
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
"""Verify POST /api/v2/users/{id}/roles ADDS a role alongside existing ones and is idempotent."""
|
||||
"""Verify POST /api/v2/user_roles ADDS a role alongside existing ones and is idempotent."""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/users/me"),
|
||||
@@ -144,15 +132,17 @@ def test_assign_role_is_additive(
|
||||
me = response.json()["data"]
|
||||
user_id = me["id"]
|
||||
|
||||
editor_role_id = find_role_by_name(signoz, admin_token, "signoz-editor")
|
||||
|
||||
# User currently has signoz-admin from test_change_role.
|
||||
# Assign signoz-editor — should be additive, admin stays.
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{user_id}/roles"),
|
||||
json={"name": "signoz-editor"},
|
||||
signoz.self.host_configs["8080"].get("/api/v2/user_roles"),
|
||||
json={"userId": user_id, "roleId": editor_role_id},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.status_code == HTTPStatus.CREATED
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{user_id}/roles"),
|
||||
@@ -168,12 +158,12 @@ def test_assign_role_is_additive(
|
||||
|
||||
# Idempotency: assigning the same role again succeeds without duplicates
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{user_id}/roles"),
|
||||
json={"name": "signoz-editor"},
|
||||
signoz.self.host_configs["8080"].get("/api/v2/user_roles"),
|
||||
json={"userId": user_id, "roleId": editor_role_id},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.status_code == HTTPStatus.CREATED
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{user_id}/roles"),
|
||||
@@ -225,7 +215,7 @@ def test_remove_role(
|
||||
signoz: types.SigNoz,
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
"""Verify DELETE /api/v2/users/{id}/roles/{roleId} removes only the specified role."""
|
||||
"""Verify DELETE /api/v2/user_roles/{id} removes only the specified role."""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/users/me"),
|
||||
@@ -236,18 +226,11 @@ def test_remove_role(
|
||||
me = response.json()["data"]
|
||||
user_id = me["id"]
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{user_id}/roles"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
roles = response.json()["data"]
|
||||
editor_role_id = next((r for r in roles if r["name"] == "signoz-editor"), None)["id"]
|
||||
assert editor_role_id is not None
|
||||
editor_entry_id = next((ur["id"] for ur in me["userRoles"] if ur["role"]["name"] == "signoz-editor"), None)
|
||||
assert editor_entry_id is not None
|
||||
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{user_id}/roles/{editor_role_id}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/user_roles/{editor_entry_id}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
@@ -296,7 +279,7 @@ def test_admin_cannot_assign_role_to_self(
|
||||
signoz: types.SigNoz,
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
"""Verify POST /api/v2/users/{own_id}/roles is rejected (self-mutation guard)."""
|
||||
"""Verify POST /api/v2/user_roles for the caller's own user is rejected (self-mutation guard)."""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/users/me"),
|
||||
@@ -307,8 +290,8 @@ def test_admin_cannot_assign_role_to_self(
|
||||
admin_data = response.json()["data"]
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{admin_data['id']}/roles"),
|
||||
json={"name": "signoz-editor"},
|
||||
signoz.self.host_configs["8080"].get("/api/v2/user_roles"),
|
||||
json={"userId": admin_data["id"], "roleId": find_role_by_name(signoz, admin_token, "signoz-editor")},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
@@ -319,7 +302,7 @@ def test_admin_cannot_remove_own_role(
|
||||
signoz: types.SigNoz,
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
"""Verify DELETE /api/v2/users/{own_id}/roles/{roleId} is rejected (self-mutation guard)."""
|
||||
"""Verify DELETE /api/v2/user_roles/{id} for the caller's own assignment is rejected (self-mutation guard)."""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/users/me"),
|
||||
@@ -329,18 +312,11 @@ def test_admin_cannot_remove_own_role(
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
admin_data = response.json()["data"]
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{admin_data['id']}/roles"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
roles = response.json()["data"]
|
||||
admin_role_id = next((r for r in roles if r["name"] == "signoz-admin"), None)["id"]
|
||||
assert admin_role_id is not None
|
||||
admin_entry_id = next((ur["id"] for ur in admin_data["userRoles"] if ur["role"]["name"] == "signoz-admin"), None)
|
||||
assert admin_entry_id is not None
|
||||
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{admin_data['id']}/roles/{admin_role_id}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/user_roles/{admin_entry_id}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
@@ -359,7 +335,7 @@ def test_editor_cannot_manage_roles(
|
||||
signoz,
|
||||
admin_token,
|
||||
email="viewer+roleauth@integration.test",
|
||||
role="VIEWER",
|
||||
role="signoz-viewer",
|
||||
password=ROLECHANGE_USER_PASSWORD,
|
||||
name="viewer roleauth",
|
||||
)
|
||||
@@ -376,8 +352,8 @@ def test_editor_cannot_manage_roles(
|
||||
|
||||
# POST assign role — forbidden
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{viewer_id}/roles"),
|
||||
json={"name": "signoz-editor"},
|
||||
signoz.self.host_configs["8080"].get("/api/v2/user_roles"),
|
||||
json={"userId": viewer_id, "roleId": find_role_by_name(signoz, admin_token, "signoz-editor")},
|
||||
headers={"Authorization": f"Bearer {editor_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
@@ -385,16 +361,15 @@ def test_editor_cannot_manage_roles(
|
||||
|
||||
# DELETE remove role — forbidden
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{viewer_id}/roles"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{viewer_id}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
viewer_roles = response.json()["data"]
|
||||
viewer_role_id = next((r for r in viewer_roles if r["name"] == "signoz-viewer"), None)["id"]
|
||||
viewer_entry_id = next(ur["id"] for ur in response.json()["data"]["userRoles"] if ur["role"]["name"] == "signoz-viewer")
|
||||
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{viewer_id}/roles/{viewer_role_id}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/user_roles/{viewer_entry_id}"),
|
||||
headers={"Authorization": f"Bearer {editor_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ from http import HTTPStatus
|
||||
import requests
|
||||
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.role import find_role_by_name
|
||||
from fixtures.types import SigNoz
|
||||
|
||||
DUPLICATE_USER_EMAIL = "duplicate@integration.test"
|
||||
@@ -20,38 +21,49 @@ def test_duplicate_user_invite_rejected(
|
||||
"""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
viewer_role_id = find_role_by_name(signoz, admin_token, "signoz-viewer")
|
||||
|
||||
# Invite a new user
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
||||
json={"email": DUPLICATE_USER_EMAIL, "role": "EDITOR"},
|
||||
signoz.self.host_configs["8080"].get("/api/v2/users"),
|
||||
json={
|
||||
"email": DUPLICATE_USER_EMAIL,
|
||||
"userRoles": [{"id": find_role_by_name(signoz, admin_token, "signoz-editor")}],
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
invited_user = response.json()["data"]
|
||||
reset_token = invited_user["token"]
|
||||
user_id = response.json()["data"]["id"]
|
||||
|
||||
# Invite the same email again — should fail
|
||||
# Invite the same email again while still pending — should fail
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
||||
json={"email": DUPLICATE_USER_EMAIL, "role": "VIEWER"},
|
||||
signoz.self.host_configs["8080"].get("/api/v2/users"),
|
||||
json={"email": DUPLICATE_USER_EMAIL, "userRoles": [{"id": viewer_role_id}]},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CONFLICT
|
||||
|
||||
# activate the user
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{user_id}/reset_password_tokens"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/resetPassword"),
|
||||
json={"password": "password123Z$", "token": reset_token},
|
||||
signoz.self.host_configs["8080"].get("/api/v2/factor_password/reset"),
|
||||
json={"password": "password123Z$", "token": response.json()["data"]["token"]},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
|
||||
# Try to invite the same email again — should fail
|
||||
# Try to invite the same email again once active — should fail
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
||||
json={"email": DUPLICATE_USER_EMAIL, "role": "VIEWER"},
|
||||
signoz.self.host_configs["8080"].get("/api/v2/users"),
|
||||
json={"email": DUPLICATE_USER_EMAIL, "userRoles": [{"id": viewer_role_id}]},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
@@ -3,7 +3,12 @@ from http import HTTPStatus
|
||||
|
||||
import requests
|
||||
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD, create_active_user
|
||||
from fixtures.auth import (
|
||||
USER_ADMIN_EMAIL,
|
||||
USER_ADMIN_PASSWORD,
|
||||
assert_user_has_role,
|
||||
create_active_user,
|
||||
)
|
||||
from fixtures.types import SigNoz
|
||||
|
||||
|
||||
@@ -22,71 +27,45 @@ def test_reinvite_deleted_user(
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
reinvite_user_email = "reinvite@integration.test"
|
||||
reinvite_user_name = "reinvite user"
|
||||
reinvite_user_role = "EDITOR"
|
||||
reinvite_user_password = "password123Z$"
|
||||
|
||||
# invite the user
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
||||
json={
|
||||
"email": reinvite_user_email,
|
||||
"role": reinvite_user_role,
|
||||
"name": reinvite_user_name,
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
user_id = create_active_user(
|
||||
signoz,
|
||||
admin_token,
|
||||
email=reinvite_user_email,
|
||||
role="signoz-editor",
|
||||
password="password123Z$",
|
||||
name="reinvite user",
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
invited_user = response.json()["data"]
|
||||
reset_token = invited_user["token"]
|
||||
|
||||
# reset the password to make it active
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/resetPassword"),
|
||||
json={"password": reinvite_user_password, "token": reset_token},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
|
||||
# call the delete api which now soft deletes the user
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{invited_user['id']}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{user_id}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
|
||||
# Re-invite the same email — should succeed
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
||||
json={
|
||||
"email": reinvite_user_email,
|
||||
"role": "VIEWER",
|
||||
"name": "reinvite user v2",
|
||||
},
|
||||
# Re-invite the same email — should succeed and create a different user
|
||||
reinvited_user_id = create_active_user(
|
||||
signoz,
|
||||
admin_token,
|
||||
email=reinvite_user_email,
|
||||
role="signoz-viewer",
|
||||
password="newPassword123Z$",
|
||||
name="reinvite user v2",
|
||||
)
|
||||
assert reinvited_user_id != user_id
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{reinvited_user_id}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
reinvited_user = response.json()["data"]
|
||||
assert reinvited_user["role"] == "VIEWER"
|
||||
assert reinvited_user["id"] != invited_user["id"] # confirms a new user was created
|
||||
|
||||
reinvited_user_reset_password_token = reinvited_user["token"]
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/resetPassword"),
|
||||
json={
|
||||
"password": "newPassword123Z$",
|
||||
"token": reinvited_user_reset_password_token,
|
||||
},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert_user_has_role(response.json()["data"], "signoz-viewer")
|
||||
|
||||
# Verify user can log in with new password
|
||||
user_token = get_token("reinvite@integration.test", "newPassword123Z$")
|
||||
user_token = get_token(reinvite_user_email, "newPassword123Z$")
|
||||
assert user_token is not None
|
||||
|
||||
|
||||
@@ -105,7 +84,7 @@ def test_delete_user(
|
||||
signoz,
|
||||
admin_token,
|
||||
email="delete-verify-v2@integration.test",
|
||||
role="EDITOR",
|
||||
role="signoz-editor",
|
||||
password="password123Z$",
|
||||
name="delete verify v2",
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ from sqlalchemy import sql
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.role import find_role_by_name
|
||||
from fixtures.types import SigNoz
|
||||
|
||||
UNIQUE_INDEX_USER_EMAIL = "useruniqueindex@integration.test"
|
||||
@@ -41,11 +42,11 @@ def test_unique_index_allows_multiple_deleted_rows(
|
||||
|
||||
# Step 1: invite and delete the first user
|
||||
resp = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
||||
signoz.self.host_configs["8080"].get("/api/v2/users"),
|
||||
json={
|
||||
"email": UNIQUE_INDEX_USER_EMAIL,
|
||||
"role": "EDITOR",
|
||||
"name": "unique index user v1",
|
||||
"displayName": "unique index user v1",
|
||||
"userRoles": [{"id": find_role_by_name(signoz, admin_token, "signoz-editor")}],
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
@@ -62,11 +63,11 @@ def test_unique_index_allows_multiple_deleted_rows(
|
||||
|
||||
# Step 2: re-invite and delete the same email (second deleted row)
|
||||
resp = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
||||
signoz.self.host_configs["8080"].get("/api/v2/users"),
|
||||
json={
|
||||
"email": UNIQUE_INDEX_USER_EMAIL,
|
||||
"role": "EDITOR",
|
||||
"name": "unique index user v2",
|
||||
"displayName": "unique index user v2",
|
||||
"userRoles": [{"id": find_role_by_name(signoz, admin_token, "signoz-editor")}],
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
|
||||
@@ -41,7 +41,7 @@ def test_setup(
|
||||
transaction_group("read", "telemetryresource", "traces", ["builder_query/signoz.workspace.key.id/key-a"]),
|
||||
],
|
||||
)
|
||||
user_id = create_active_user(signoz, admin_token, email=scoped_email, role="VIEWER", password=user_password)
|
||||
user_id = create_active_user(signoz, admin_token, email=scoped_email, role="signoz-viewer", password=user_password)
|
||||
change_user_role(signoz, admin_token, user_id, "signoz-viewer", scoped_role)
|
||||
|
||||
|
||||
|
||||
@@ -24,11 +24,11 @@ def test_setup(
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
create_role(admin_token, any_key_role, [transaction_group("read", "telemetryresource", "logs", ["builder_query/signoz.workspace.key.id/*"])])
|
||||
any_user = create_active_user(signoz, admin_token, email=any_key_email, role="VIEWER", password=user_password)
|
||||
any_user = create_active_user(signoz, admin_token, email=any_key_email, role="signoz-viewer", password=user_password)
|
||||
change_user_role(signoz, admin_token, any_user, "signoz-viewer", any_key_role)
|
||||
|
||||
create_role(admin_token, builder_all_role, [transaction_group("read", "telemetryresource", "logs", ["builder_query/*"])])
|
||||
all_user = create_active_user(signoz, admin_token, email=builder_all_email, role="VIEWER", password=user_password)
|
||||
all_user = create_active_user(signoz, admin_token, email=builder_all_email, role="signoz-viewer", password=user_password)
|
||||
change_user_role(signoz, admin_token, all_user, "signoz-viewer", builder_all_role)
|
||||
|
||||
|
||||
|
||||
@@ -38,15 +38,15 @@ def test_setup(
|
||||
transaction_group("read", "telemetryresource", "meter-metrics", ["clickhouse_sql/*"]),
|
||||
],
|
||||
)
|
||||
chsql_user = create_active_user(signoz, admin_token, email=chsql_email, role="VIEWER", password=user_password)
|
||||
chsql_user = create_active_user(signoz, admin_token, email=chsql_email, role="signoz-viewer", password=user_password)
|
||||
change_user_role(signoz, admin_token, chsql_user, "signoz-viewer", chsql_role)
|
||||
|
||||
create_role(admin_token, key_a_role, [transaction_group("read", "telemetryresource", "traces", ["builder_query/signoz.workspace.key.id/key-a"])])
|
||||
key_a_user = create_active_user(signoz, admin_token, email=key_a_email, role="VIEWER", password=user_password)
|
||||
key_a_user = create_active_user(signoz, admin_token, email=key_a_email, role="signoz-viewer", password=user_password)
|
||||
change_user_role(signoz, admin_token, key_a_user, "signoz-viewer", key_a_role)
|
||||
|
||||
# A plain managed viewer (signoz-viewer) — for the meter-metrics/audit-logs policy checks.
|
||||
create_active_user(signoz, admin_token, email=viewer_email, role="VIEWER", password=user_password)
|
||||
create_active_user(signoz, admin_token, email=viewer_email, role="signoz-viewer", password=user_password)
|
||||
|
||||
|
||||
def test_clickhouse_sql_requires_chsql_grant(
|
||||
|
||||
@@ -24,7 +24,7 @@ def test_setup(
|
||||
) -> None:
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
create_role(admin_token, spacey_role, [transaction_group("read", "telemetryresource", "logs", ["builder_query/signoz.workspace.key.id/key with space"])])
|
||||
user_id = create_active_user(signoz, admin_token, email=spacey_email, role="VIEWER", password=user_password)
|
||||
user_id = create_active_user(signoz, admin_token, email=spacey_email, role="signoz-viewer", password=user_password)
|
||||
change_user_role(signoz, admin_token, user_id, "signoz-viewer", spacey_role)
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ def test_setup(
|
||||
) -> None:
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
create_role(admin_token, keywild_role, [transaction_group("read", "telemetryresource", "logs", ["builder_query/signoz.workspace.key.id/*"])])
|
||||
user_id = create_active_user(signoz, admin_token, email=keywild_email, role="VIEWER", password=user_password)
|
||||
user_id = create_active_user(signoz, admin_token, email=keywild_email, role="signoz-viewer", password=user_password)
|
||||
change_user_role(signoz, admin_token, user_id, "signoz-viewer", keywild_role)
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD, add_license
|
||||
from fixtures.role import (
|
||||
expected_managed_transaction_keys,
|
||||
find_role_by_name,
|
||||
flatten_transaction_groups,
|
||||
managed_role_names,
|
||||
)
|
||||
@@ -67,11 +68,10 @@ def test_managed_role_transactions_match_expected(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
find_role_id: Callable[[str, str], str],
|
||||
role_name: str,
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
role_id = find_role_id(admin_token, role_name)
|
||||
role_id = find_role_by_name(signoz, admin_token, role_name)
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/roles/{role_id}"),
|
||||
|
||||
@@ -12,7 +12,7 @@ from fixtures.auth import (
|
||||
create_active_user,
|
||||
find_user_by_email,
|
||||
)
|
||||
from fixtures.role import flatten_transaction_groups, transaction_group
|
||||
from fixtures.role import find_role_by_name, flatten_transaction_groups, transaction_group
|
||||
|
||||
CRUD_ROLE_NAME = "crud-test-role"
|
||||
CRUD_ASSIGNEE_ROLE_NAME = "crud-assignee-role"
|
||||
@@ -61,10 +61,9 @@ def test_declarative_update_adds_and_removes_transactions(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
find_role_id: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
role_id = find_role_id(admin_token, CRUD_ROLE_NAME)
|
||||
role_id = find_role_by_name(signoz, admin_token, CRUD_ROLE_NAME)
|
||||
|
||||
def put_transactions(groups: list[dict]) -> None:
|
||||
resp = requests.put(
|
||||
@@ -208,10 +207,9 @@ def test_managed_role_is_immutable(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
find_role_id: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
admin_role_id = find_role_id(admin_token, "signoz-admin")
|
||||
admin_role_id = find_role_by_name(signoz, admin_token, "signoz-admin")
|
||||
|
||||
resp = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/roles/{admin_role_id}"),
|
||||
@@ -239,27 +237,27 @@ def test_delete_role_with_assignee_guarded(
|
||||
signoz,
|
||||
admin_token,
|
||||
email=CRUD_ASSIGNEE_USER_EMAIL,
|
||||
role="VIEWER",
|
||||
role="signoz-viewer",
|
||||
password=CRUD_ASSIGNEE_USER_PASSWORD,
|
||||
name="crud-assignee-user",
|
||||
)
|
||||
|
||||
resp = requests.post(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{user_id}/roles"),
|
||||
json={"name": CRUD_ASSIGNEE_ROLE_NAME},
|
||||
signoz.self.host_configs["8080"].get("/api/v2/user_roles"),
|
||||
json={"userId": user_id, "roleId": role_id},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.OK, resp.text
|
||||
assert resp.status_code == HTTPStatus.CREATED, resp.text
|
||||
|
||||
resp = requests.delete(signoz.self.host_configs["8080"].get(f"/api/v1/roles/{role_id}"), headers={"Authorization": f"Bearer {admin_token}"}, timeout=5)
|
||||
assert resp.status_code == HTTPStatus.BAD_REQUEST, f"delete role with assignee: expected 400, got {resp.status_code}: {resp.text}"
|
||||
|
||||
resp = requests.get(signoz.self.host_configs["8080"].get(f"/api/v2/users/{user_id}/roles"), headers={"Authorization": f"Bearer {admin_token}"}, timeout=5)
|
||||
resp = requests.get(signoz.self.host_configs["8080"].get(f"/api/v2/users/{user_id}"), headers={"Authorization": f"Bearer {admin_token}"}, timeout=5)
|
||||
assert resp.status_code == HTTPStatus.OK, resp.text
|
||||
entry = next(r for r in resp.json()["data"] if r["name"] == CRUD_ASSIGNEE_ROLE_NAME)
|
||||
entry = next(ur for ur in resp.json()["data"]["userRoles"] if ur["role"]["name"] == CRUD_ASSIGNEE_ROLE_NAME)
|
||||
resp = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/users/{user_id}/roles/{entry['id']}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/user_roles/{entry['id']}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ from fixtures.auth import (
|
||||
create_active_user,
|
||||
find_user_by_email,
|
||||
)
|
||||
from fixtures.role import transaction_group
|
||||
from fixtures.role import find_role_by_name, transaction_group
|
||||
|
||||
_ACTOR_ROLE_NAME = "role-fga-actor"
|
||||
_ACTOR_USER_EMAIL = "customrole+rolefga@integration.test"
|
||||
@@ -57,7 +57,7 @@ def test_setup_actor_and_targets(
|
||||
signoz,
|
||||
admin_token,
|
||||
email=_ACTOR_USER_EMAIL,
|
||||
role="VIEWER",
|
||||
role="signoz-viewer",
|
||||
password=_ACTOR_USER_PASSWORD,
|
||||
name="role-fga-test-user",
|
||||
)
|
||||
@@ -68,12 +68,11 @@ def test_read_scoped_to_granted_role(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
find_role_id: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
token = get_token(_ACTOR_USER_EMAIL, _ACTOR_USER_PASSWORD)
|
||||
a_id = find_role_id(admin_token, _TARGET_A)
|
||||
b_id = find_role_id(admin_token, _TARGET_B)
|
||||
a_id = find_role_by_name(signoz, admin_token, _TARGET_A)
|
||||
b_id = find_role_by_name(signoz, admin_token, _TARGET_B)
|
||||
|
||||
resp = requests.get(signoz.self.host_configs["8080"].get(f"/api/v1/roles/{a_id}"), headers={"Authorization": f"Bearer {token}"}, timeout=5)
|
||||
assert resp.status_code == HTTPStatus.OK, f"read granted role: {resp.text}"
|
||||
@@ -101,11 +100,10 @@ def test_create_is_collection_scoped(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
find_role_id: Callable[[str, str], str],
|
||||
create_role: Callable[..., str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
actor_id = find_role_id(admin_token, _ACTOR_ROLE_NAME)
|
||||
actor_id = find_role_by_name(signoz, admin_token, _ACTOR_ROLE_NAME)
|
||||
token = get_token(_ACTOR_USER_EMAIL, _ACTOR_USER_PASSWORD)
|
||||
|
||||
resp = requests.post(
|
||||
@@ -138,12 +136,11 @@ def test_update_scoped_to_granted_role(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
find_role_id: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
actor_id = find_role_id(admin_token, _ACTOR_ROLE_NAME)
|
||||
a_id = find_role_id(admin_token, _TARGET_A)
|
||||
b_id = find_role_id(admin_token, _TARGET_B)
|
||||
actor_id = find_role_by_name(signoz, admin_token, _ACTOR_ROLE_NAME)
|
||||
a_id = find_role_by_name(signoz, admin_token, _TARGET_A)
|
||||
b_id = find_role_by_name(signoz, admin_token, _TARGET_B)
|
||||
|
||||
resp = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/roles/{actor_id}"),
|
||||
@@ -184,12 +181,11 @@ def test_delete_scoped_to_granted_role(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
find_role_id: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
actor_id = find_role_id(admin_token, _ACTOR_ROLE_NAME)
|
||||
a_id = find_role_id(admin_token, _TARGET_A)
|
||||
b_id = find_role_id(admin_token, _TARGET_B)
|
||||
actor_id = find_role_by_name(signoz, admin_token, _ACTOR_ROLE_NAME)
|
||||
a_id = find_role_by_name(signoz, admin_token, _TARGET_A)
|
||||
b_id = find_role_by_name(signoz, admin_token, _TARGET_B)
|
||||
|
||||
resp = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/roles/{actor_id}"),
|
||||
@@ -221,11 +217,10 @@ def test_revoke_read(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
find_role_id: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
actor_id = find_role_id(admin_token, _ACTOR_ROLE_NAME)
|
||||
b_id = find_role_id(admin_token, _TARGET_B)
|
||||
actor_id = find_role_by_name(signoz, admin_token, _ACTOR_ROLE_NAME)
|
||||
b_id = find_role_by_name(signoz, admin_token, _TARGET_B)
|
||||
token = get_token(_ACTOR_USER_EMAIL, _ACTOR_USER_PASSWORD)
|
||||
|
||||
resp = requests.put(
|
||||
@@ -261,7 +256,6 @@ def test_role_fga_cleanup(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
find_role_id: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
user = find_user_by_email(signoz, admin_token, _ACTOR_USER_EMAIL)
|
||||
@@ -279,7 +273,7 @@ def test_role_fga_cleanup(
|
||||
|
||||
for name in (_ACTOR_ROLE_NAME, _TARGET_B, _CREATED_ROLE):
|
||||
resp = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/roles/{find_role_id(admin_token, name)}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/roles/{find_role_by_name(signoz, admin_token, name)}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ from fixtures.auth import (
|
||||
create_active_user,
|
||||
find_user_by_email,
|
||||
)
|
||||
from fixtures.role import transaction_group
|
||||
from fixtures.role import find_role_by_name, transaction_group
|
||||
from fixtures.savedview import SAVED_VIEW_BASE, create_saved_view, find_saved_view_by_name
|
||||
|
||||
_SAVED_VIEW_FGA_CUSTOM_ROLE_NAME = "saved-view-fga-readonly"
|
||||
@@ -57,7 +57,7 @@ def test_create_custom_role_readonly_view(
|
||||
signoz,
|
||||
admin_token,
|
||||
email=_SAVED_VIEW_FGA_CUSTOM_USER_EMAIL,
|
||||
role="VIEWER",
|
||||
role="signoz-viewer",
|
||||
password=_SAVED_VIEW_FGA_CUSTOM_USER_PASSWORD,
|
||||
name="saved-view-fga-test-user",
|
||||
)
|
||||
@@ -152,10 +152,9 @@ def test_create_is_collection_scoped(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
find_role_id: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
role_id = find_role_id(admin_token, _SAVED_VIEW_FGA_CUSTOM_ROLE_NAME)
|
||||
role_id = find_role_by_name(signoz, admin_token, _SAVED_VIEW_FGA_CUSTOM_ROLE_NAME)
|
||||
target_id = find_saved_view_by_name(signoz, admin_token, _SAVED_VIEW_FGA_TARGET_NAME)["id"]
|
||||
|
||||
resp = requests.put(
|
||||
@@ -184,10 +183,9 @@ def test_update_scoped_to_granted_view(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
find_role_id: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
role_id = find_role_id(admin_token, _SAVED_VIEW_FGA_CUSTOM_ROLE_NAME)
|
||||
role_id = find_role_by_name(signoz, admin_token, _SAVED_VIEW_FGA_CUSTOM_ROLE_NAME)
|
||||
target_id = find_saved_view_by_name(signoz, admin_token, _SAVED_VIEW_FGA_TARGET_NAME)["id"]
|
||||
other_id = find_saved_view_by_name(signoz, admin_token, _SAVED_VIEW_FGA_OTHER_NAME)["id"]
|
||||
|
||||
@@ -242,10 +240,9 @@ def test_delete_scoped_to_granted_view(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
find_role_id: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
role_id = find_role_id(admin_token, _SAVED_VIEW_FGA_CUSTOM_ROLE_NAME)
|
||||
role_id = find_role_by_name(signoz, admin_token, _SAVED_VIEW_FGA_CUSTOM_ROLE_NAME)
|
||||
target_id = find_saved_view_by_name(signoz, admin_token, _SAVED_VIEW_FGA_TARGET_NAME)["id"]
|
||||
other_id = find_saved_view_by_name(signoz, admin_token, _SAVED_VIEW_FGA_OTHER_NAME)["id"]
|
||||
|
||||
@@ -279,10 +276,9 @@ def test_revoke_read_scoped(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
find_role_id: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
role_id = find_role_id(admin_token, _SAVED_VIEW_FGA_CUSTOM_ROLE_NAME)
|
||||
role_id = find_role_by_name(signoz, admin_token, _SAVED_VIEW_FGA_CUSTOM_ROLE_NAME)
|
||||
other_id = find_saved_view_by_name(signoz, admin_token, _SAVED_VIEW_FGA_OTHER_NAME)["id"]
|
||||
|
||||
resp = requests.put(
|
||||
@@ -319,7 +315,6 @@ def test_saved_view_fga_cleanup(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
find_role_id: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
user = find_user_by_email(signoz, admin_token, _SAVED_VIEW_FGA_CUSTOM_USER_EMAIL)
|
||||
@@ -336,7 +331,7 @@ def test_saved_view_fga_cleanup(
|
||||
assert resp.status_code == HTTPStatus.NO_CONTENT, f"remove role from user: {resp.text}"
|
||||
|
||||
resp = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/roles/{find_role_id(admin_token, _SAVED_VIEW_FGA_CUSTOM_ROLE_NAME)}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/roles/{find_role_by_name(signoz, admin_token, _SAVED_VIEW_FGA_CUSTOM_ROLE_NAME)}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ from fixtures.auth import (
|
||||
create_active_user,
|
||||
find_user_by_email,
|
||||
)
|
||||
from fixtures.role import transaction_group
|
||||
from fixtures.role import find_role_by_name, transaction_group
|
||||
from fixtures.serviceaccount import (
|
||||
SERVICE_ACCOUNT_BASE,
|
||||
create_service_account,
|
||||
@@ -70,7 +70,7 @@ def test_create_custom_role_readonly_sa(
|
||||
signoz,
|
||||
admin_token,
|
||||
email=_SA_FGA_CUSTOM_USER_EMAIL,
|
||||
role="VIEWER",
|
||||
role="signoz-viewer",
|
||||
password=_SA_FGA_CUSTOM_USER_PASSWORD,
|
||||
name="sa-fga-test-user",
|
||||
)
|
||||
@@ -116,12 +116,11 @@ def test_write_forbidden_without_grant(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
find_role_id: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
token = get_token(_SA_FGA_CUSTOM_USER_EMAIL, _SA_FGA_CUSTOM_USER_PASSWORD)
|
||||
target_id = find_service_account_by_name(signoz, admin_token, _SA_FGA_TARGET_SA_NAME)["id"]
|
||||
viewer_role_id = find_role_id(admin_token, "signoz-viewer")
|
||||
viewer_role_id = find_role_by_name(signoz, admin_token, "signoz-viewer")
|
||||
|
||||
resp = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"{SERVICE_ACCOUNT_BASE}/{target_id}"),
|
||||
@@ -144,10 +143,9 @@ def test_update_scoped_to_granted_sa(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
find_role_id: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
role_id = find_role_id(admin_token, _SA_FGA_CUSTOM_ROLE_NAME)
|
||||
role_id = find_role_by_name(signoz, admin_token, _SA_FGA_CUSTOM_ROLE_NAME)
|
||||
target_id = find_service_account_by_name(signoz, admin_token, _SA_FGA_TARGET_SA_NAME)["id"]
|
||||
other_id = find_service_account_by_name(signoz, admin_token, _SA_FGA_OTHER_SA_NAME)["id"]
|
||||
|
||||
@@ -191,14 +189,13 @@ def test_attach_detach_dual_scoped(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
find_role_id: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
role_id = find_role_id(admin_token, _SA_FGA_CUSTOM_ROLE_NAME)
|
||||
role_id = find_role_by_name(signoz, admin_token, _SA_FGA_CUSTOM_ROLE_NAME)
|
||||
target_id = find_service_account_by_name(signoz, admin_token, _SA_FGA_TARGET_SA_NAME)["id"]
|
||||
other_id = find_service_account_by_name(signoz, admin_token, _SA_FGA_OTHER_SA_NAME)["id"]
|
||||
editor_role_id = find_role_id(admin_token, "signoz-editor")
|
||||
viewer_role_id = find_role_id(admin_token, "signoz-viewer")
|
||||
editor_role_id = find_role_by_name(signoz, admin_token, "signoz-editor")
|
||||
viewer_role_id = find_role_by_name(signoz, admin_token, "signoz-viewer")
|
||||
|
||||
# attach/detach granted on the target SA id AND the signoz-editor role name only.
|
||||
resp = requests.put(
|
||||
@@ -261,10 +258,9 @@ def test_revoke_read_scoped(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
find_role_id: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
role_id = find_role_id(admin_token, _SA_FGA_CUSTOM_ROLE_NAME)
|
||||
role_id = find_role_by_name(signoz, admin_token, _SA_FGA_CUSTOM_ROLE_NAME)
|
||||
target_id = find_service_account_by_name(signoz, admin_token, _SA_FGA_TARGET_SA_NAME)["id"]
|
||||
|
||||
resp = requests.put(
|
||||
@@ -284,10 +280,9 @@ def test_delete_custom_role_cleanup(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
find_role_id: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
role_id = find_role_id(admin_token, _SA_FGA_CUSTOM_ROLE_NAME)
|
||||
role_id = find_role_by_name(signoz, admin_token, _SA_FGA_CUSTOM_ROLE_NAME)
|
||||
user = find_user_by_email(signoz, admin_token, _SA_FGA_CUSTOM_USER_EMAIL)
|
||||
|
||||
resp = requests.get(signoz.self.host_configs["8080"].get(f"/api/v2/users/{user['id']}/roles"), headers={"Authorization": f"Bearer {admin_token}"}, timeout=5)
|
||||
|
||||
Reference in New Issue
Block a user