Compare commits

...

7 Commits

Author SHA1 Message Date
Nikhil Soni
6114e7ae82 chore: add further filtering for required attribute columns 2026-05-18 11:28:40 +05:30
Nikhil Soni
91121fd9a1 refactor: move column selection to types 2026-05-18 10:57:48 +05:30
Nikhil Soni
877567d139 fix: only select the required attribute map 2026-05-18 10:50:44 +05:30
SagarRajput-7
44470cb35b feat(sa-fga): service account fga (#11258)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
* feat(sa-fga): changed the id from kind to kind+type

* feat(sa-fga): service account fga changes with common components for errors

* feat(sa-fga): added fga at more places in service account

* feat(sa-fga): refactor based on feedbacks

* feat(sa-fga): refactor and role page fga

* fix(authz): add attach detach permissions on metaresource

* feat(sa-fga): refactor and role page fga

* feat(sa-fga): test case fixes

* feat(sa-fga): enabled role detail page and remove the config flag

* feat(sa-fga): test case fixes

* feat(sa-fga): udpated the role details metaresource condition to list/create

* feat(sa-fga): test case fixes

* feat(sa-fga): feedback fixes from the copliot comments

* feat(sa-fga): feedback fixes from the reveiw comments and authztootip upgrade

* feat(sa-fga): feedback fixes from the testing and refactors

* feat(sa-fga): test cases fixes

* feat(sa-fga): added beta for the roles page

* feat(sa-fga): added roles doc and roles read check with name in the url param

* Revert "fix(authz): add attach detach permissions on metaresource"

This reverts commit 34938bb4ce.

---------

Co-authored-by: vikrantgupta25 <vikrant@signoz.io>
2026-05-17 14:35:27 +00:00
Vikrant Gupta
cf3fbb06c6 fix(authz): reserve signoz prefix for managed roles (#11327)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* feat(authz): reserve signoz-* for managed roles

* feat(authz): add integration tests

* feat(authz): reserve signoz-* for managed roles

* feat(authz): add integration tests
2026-05-16 16:03:21 +00:00
Vikrant Gupta
fd2e526f7c fix(deps): resolve all high/critical Dependabot security alerts (#11323)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* fix(deps): upgrade dependencies to resolve high/critical security alerts

Upgrade pgx/v5 (v5.8.0→v5.9.2), prometheus (v0.310.0→v0.311.3),
gosaml2 (v0.9.0→v0.11.0), goxmldsig (v1.2.0→v1.6.0), and
urllib3 (2.6.3→2.7.0) to fix all open high/critical Dependabot alerts.

Adapt parser.ParseExpr calls to use the new Parser interface introduced
in prometheus v0.311.x.

* refactor: reuse a single PromQL parser instance instead of creating per call

Add Parser() to the prometheus.Prometheus interface so a single
parser.Parser is created at startup and shared across all consumers.
For the legacy v2 querier and PromQLFilterExtractor (which don't have
access to the Prometheus interface), store a parser instance on the
struct, created once during construction.

* refactor: centralize PromQL parser creation via prometheus.NewParser()

Add pkg/prometheus/parser.go with a Parser type alias and NewParser()
factory function, mirroring the existing Engine/NewEngine pattern.
All consumers now create parsers through this single entry point
instead of calling parser.NewParser(parser.Options{}) directly.
2026-05-15 20:02:39 +00:00
Vinicius Lourenço
afe8942368 fix(infra-monitoring): error due to invalid operators on query builder (#11300)
* fix(infra-monitoring): error due to invalid operators on query builder

* fix(query-builder): keep not_in and transform to nin, same for other operators

* chore(code-cleanup): clean the duplicated code and bugs
2026-05-15 18:42:00 +00:00
82 changed files with 2583 additions and 935 deletions

View File

@@ -0,0 +1,14 @@
.wrapper {
cursor: not-allowed;
}
.errorContent {
background: var(--callout-error-background) !important;
border-color: var(--callout-error-border) !important;
backdrop-filter: blur(15px);
border-radius: 4px !important;
color: var(--foreground) !important;
font-style: normal;
font-weight: 400;
white-space: nowrap;
}

View File

@@ -0,0 +1,145 @@
import { ReactElement } from 'react';
import { render, screen } from 'tests/test-utils';
import { buildPermission } from 'hooks/useAuthZ/utils';
import type { AuthZObject, BrandedPermission } from 'hooks/useAuthZ/types';
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
import AuthZTooltip from './AuthZTooltip';
jest.mock('hooks/useAuthZ/useAuthZ');
const mockUseAuthZ = useAuthZ as jest.MockedFunction<typeof useAuthZ>;
const noPermissions = {
isLoading: false,
isFetching: false,
error: null,
permissions: null,
refetchPermissions: jest.fn(),
};
const TestButton = (
props: React.ButtonHTMLAttributes<HTMLButtonElement>,
): ReactElement => (
<button type="button" {...props}>
Action
</button>
);
const createPerm = buildPermission(
'create',
'serviceaccount:*' as AuthZObject<'create'>,
);
const attachSAPerm = (id: string): BrandedPermission =>
buildPermission('attach', `serviceaccount:${id}` as AuthZObject<'attach'>);
const attachRolePerm = buildPermission(
'attach',
'role:*' as AuthZObject<'attach'>,
);
describe('AuthZTooltip — single check', () => {
it('renders child unchanged when permission is granted', () => {
mockUseAuthZ.mockReturnValue({
...noPermissions,
permissions: { [createPerm]: { isGranted: true } },
});
render(
<AuthZTooltip checks={[createPerm]}>
<TestButton />
</AuthZTooltip>,
);
expect(screen.getByRole('button', { name: 'Action' })).not.toBeDisabled();
});
it('disables child when permission is denied', () => {
mockUseAuthZ.mockReturnValue({
...noPermissions,
permissions: { [createPerm]: { isGranted: false } },
});
render(
<AuthZTooltip checks={[createPerm]}>
<TestButton />
</AuthZTooltip>,
);
expect(screen.getByRole('button', { name: 'Action' })).toBeDisabled();
});
it('disables child while loading', () => {
mockUseAuthZ.mockReturnValue({ ...noPermissions, isLoading: true });
render(
<AuthZTooltip checks={[createPerm]}>
<TestButton />
</AuthZTooltip>,
);
expect(screen.getByRole('button', { name: 'Action' })).toBeDisabled();
});
});
describe('AuthZTooltip — multi-check (checks array)', () => {
it('renders child enabled when all checks are granted', () => {
const sa = attachSAPerm('sa-1');
mockUseAuthZ.mockReturnValue({
...noPermissions,
permissions: {
[sa]: { isGranted: true },
[attachRolePerm]: { isGranted: true },
},
});
render(
<AuthZTooltip checks={[sa, attachRolePerm]}>
<TestButton />
</AuthZTooltip>,
);
expect(screen.getByRole('button', { name: 'Action' })).not.toBeDisabled();
});
it('disables child when first check is denied, second granted', () => {
const sa = attachSAPerm('sa-1');
mockUseAuthZ.mockReturnValue({
...noPermissions,
permissions: {
[sa]: { isGranted: false },
[attachRolePerm]: { isGranted: true },
},
});
render(
<AuthZTooltip checks={[sa, attachRolePerm]}>
<TestButton />
</AuthZTooltip>,
);
expect(screen.getByRole('button', { name: 'Action' })).toBeDisabled();
});
it('disables child when both checks are denied and lists denied permissions in data attr', () => {
const sa = attachSAPerm('sa-1');
mockUseAuthZ.mockReturnValue({
...noPermissions,
permissions: {
[sa]: { isGranted: false },
[attachRolePerm]: { isGranted: false },
},
});
render(
<AuthZTooltip checks={[sa, attachRolePerm]}>
<TestButton />
</AuthZTooltip>,
);
expect(screen.getByRole('button', { name: 'Action' })).toBeDisabled();
const wrapper = screen.getByRole('button', { name: 'Action' }).parentElement;
expect(wrapper?.getAttribute('data-denied-permissions')).toContain(sa);
expect(wrapper?.getAttribute('data-denied-permissions')).toContain(
attachRolePerm,
);
});
});

View File

@@ -0,0 +1,85 @@
import { ReactElement, cloneElement, useMemo } from 'react';
import {
TooltipRoot,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@signozhq/ui/tooltip';
import type { BrandedPermission } from 'hooks/useAuthZ/types';
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
import { parsePermission } from 'hooks/useAuthZ/utils';
import styles from './AuthZTooltip.module.scss';
interface AuthZTooltipProps {
checks: BrandedPermission[];
children: ReactElement;
enabled?: boolean;
tooltipMessage?: string;
}
function formatDeniedMessage(
denied: BrandedPermission[],
override?: string,
): string {
if (override) {
return override;
}
const labels = denied.map((p) => {
const { relation, object } = parsePermission(p);
const resource = object.split(':')[0];
return `${relation} ${resource}`;
});
return labels.length === 1
? `You don't have ${labels[0]} permission`
: `You don't have ${labels.join(', ')} permissions`;
}
function AuthZTooltip({
checks,
children,
enabled = true,
tooltipMessage,
}: AuthZTooltipProps): JSX.Element {
const shouldCheck = enabled && checks.length > 0;
const { permissions, isLoading } = useAuthZ(checks, { enabled: shouldCheck });
const deniedPermissions = useMemo(() => {
if (!permissions) {
return [];
}
return checks.filter((p) => permissions[p]?.isGranted === false);
}, [checks, permissions]);
if (shouldCheck && isLoading) {
return (
<span className={styles.wrapper}>
{cloneElement(children, { disabled: true })}
</span>
);
}
if (!shouldCheck || deniedPermissions.length === 0) {
return children;
}
return (
<TooltipProvider>
<TooltipRoot>
<TooltipTrigger asChild>
<span
className={styles.wrapper}
data-denied-permissions={deniedPermissions.join(',')}
>
{cloneElement(children, { disabled: true })}
</span>
</TooltipTrigger>
<TooltipContent className={styles.errorContent}>
{formatDeniedMessage(deniedPermissions, tooltipMessage)}
</TooltipContent>
</TooltipRoot>
</TooltipProvider>
);
}
export default AuthZTooltip;

View File

@@ -2,6 +2,8 @@ import { Controller, useForm } from 'react-hook-form';
import { useQueryClient } from 'react-query';
import { X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
import { SACreatePermission } from 'hooks/useAuthZ/permissions/service-account.permissions';
import { DialogFooter, DialogWrapper } from '@signozhq/ui/dialog';
import { Input } from '@signozhq/ui/input';
import { toast } from '@signozhq/ui/sonner';
@@ -132,17 +134,19 @@ function CreateServiceAccountModal(): JSX.Element {
Cancel
</Button>
<Button
type="submit"
// @ts-expect-error -- form prop not in @signozhq/ui Button type - TODO: Fix this - @SagarRajput
form="create-sa-form"
variant="solid"
color="primary"
loading={isSubmitting}
disabled={!isValid}
>
Create Service Account
</Button>
<AuthZTooltip checks={[SACreatePermission]}>
<Button
type="submit"
// @ts-expect-error -- form prop not in @signozhq/ui Button type - TODO: Fix this - @SagarRajput
form="create-sa-form"
variant="solid"
color="primary"
loading={isSubmitting}
disabled={!isValid}
>
Create Service Account
</Button>
</AuthZTooltip>
</DialogFooter>
</DialogWrapper>
);

View File

@@ -11,6 +11,15 @@ import {
import CreateServiceAccountModal from '../CreateServiceAccountModal';
jest.mock('components/AuthZTooltip/AuthZTooltip', () => ({
__esModule: true,
default: ({
children,
}: {
children: React.ReactElement;
}): React.ReactElement => children,
}));
jest.mock('@signozhq/ui/sonner', () => ({
...jest.requireActual('@signozhq/ui/sonner'),
toast: { success: jest.fn(), error: jest.fn() },
@@ -113,7 +122,9 @@ describe('CreateServiceAccountModal', () => {
getErrorMessage: expect.any(Function),
}),
);
const passedError = showErrorModal.mock.calls[0][0] as any;
const passedError = showErrorModal.mock.calls[0][0] as {
getErrorMessage: () => string;
};
expect(passedError.getErrorMessage()).toBe('Internal Server Error');
});
@@ -132,6 +143,9 @@ describe('CreateServiceAccountModal', () => {
await user.click(screen.getByRole('button', { name: /Cancel/i }));
await waitForElementToBeRemoved(dialog);
expect(
screen.queryByRole('dialog', { name: /New Service Account/i }),
).not.toBeInTheDocument();
});
it('shows "Name is required" after clearing the name field', async () => {
@@ -142,6 +156,8 @@ describe('CreateServiceAccountModal', () => {
await user.type(nameInput, 'Bot');
await user.clear(nameInput);
await screen.findByText('Name is required');
await expect(
screen.findByText('Name is required'),
).resolves.toBeInTheDocument();
});
});

View File

@@ -1,34 +1,13 @@
import { ReactElement } from 'react';
import {
AuthtypesGettableTransactionDTO,
AuthtypesTransactionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { ENVIRONMENT } from 'constants/env';
import { BrandedPermission } from 'hooks/useAuthZ/types';
import { buildPermission } from 'hooks/useAuthZ/utils';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { render, screen, waitFor } from 'tests/test-utils';
import { AUTHZ_CHECK_URL, authzMockResponse } from 'tests/authz-test-utils';
import { GuardAuthZ } from './GuardAuthZ';
const BASE_URL = ENVIRONMENT.baseURL || '';
const AUTHZ_CHECK_URL = `${BASE_URL}/api/v1/authz/check`;
function authzMockResponse(
payload: AuthtypesTransactionDTO[],
authorizedByIndex: boolean[],
): { data: AuthtypesGettableTransactionDTO[]; status: string } {
return {
data: payload.map((txn, i) => ({
relation: txn.relation,
object: txn.object,
authorized: authorizedByIndex[i] ?? false,
})),
status: 'success',
};
}
describe('GuardAuthZ', () => {
const TestChild = (): ReactElement => <div>Protected Content</div>;
const LoadingFallback = (): ReactElement => <div>Loading...</div>;

View File

@@ -0,0 +1,4 @@
.callout {
box-sizing: border-box;
width: 100%;
}

View File

@@ -0,0 +1,22 @@
import { render, screen } from 'tests/test-utils';
import PermissionDeniedCallout from './PermissionDeniedCallout';
describe('PermissionDeniedCallout', () => {
it('renders the permission name in the callout message', () => {
render(<PermissionDeniedCallout permissionName="serviceaccount:attach" />);
expect(screen.getByText(/You don't have/)).toBeInTheDocument();
expect(screen.getByText(/serviceaccount:attach/)).toBeInTheDocument();
expect(screen.getByText(/permission/)).toBeInTheDocument();
});
it('accepts an optional className', () => {
const { container } = render(
<PermissionDeniedCallout
permissionName="serviceaccount:read"
className="custom-class"
/>,
);
expect(container.firstChild).toHaveClass('custom-class');
});
});

View File

@@ -0,0 +1,26 @@
import { Callout } from '@signozhq/ui/callout';
import cx from 'classnames';
import styles from './PermissionDeniedCallout.module.scss';
interface PermissionDeniedCalloutProps {
permissionName: string;
className?: string;
}
function PermissionDeniedCallout({
permissionName,
className,
}: PermissionDeniedCalloutProps): JSX.Element {
return (
<Callout
type="error"
showIcon
size="small"
className={cx(styles.callout, className)}
>
{`You don't have ${permissionName} permission`}
</Callout>
);
}
export default PermissionDeniedCallout;

View File

@@ -0,0 +1,44 @@
.container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
min-height: 50vh;
padding: var(--spacing-10);
}
.content {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-2);
max-width: 512px;
}
.icon {
margin-bottom: var(--spacing-1);
}
.title {
margin: 0;
font-size: var(--label-base-500-font-size);
font-weight: var(--label-base-500-font-weight);
line-height: var(--line-height-18);
letter-spacing: -0.07px;
color: var(--l1-foreground);
}
.subtitle {
margin: 0;
font-size: var(--label-base-400-font-size);
font-weight: var(--label-base-400-font-weight);
line-height: var(--line-height-18);
letter-spacing: -0.07px;
color: var(--l2-foreground);
}
.permission {
font-family: monospace;
color: var(--l2-foreground);
}

View File

@@ -0,0 +1,21 @@
import { render, screen } from 'tests/test-utils';
import PermissionDeniedFullPage from './PermissionDeniedFullPage';
describe('PermissionDeniedFullPage', () => {
it('renders the title and subtitle with the permissionName interpolated', () => {
render(<PermissionDeniedFullPage permissionName="serviceaccount:list" />);
expect(
screen.getByText("Uh-oh! You don't have permission to view this page."),
).toBeInTheDocument();
expect(screen.getByText(/serviceaccount:list/)).toBeInTheDocument();
expect(
screen.getByText(/Please ask your SigNoz administrator to grant access/),
).toBeInTheDocument();
});
it('renders with a different permissionName', () => {
render(<PermissionDeniedFullPage permissionName="role:read" />);
expect(screen.getByText(/role:read/)).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,31 @@
import { CircleSlash2 } from '@signozhq/icons';
import styles from './PermissionDeniedFullPage.module.scss';
import { Style } from '@signozhq/design-tokens';
interface PermissionDeniedFullPageProps {
permissionName: string;
}
function PermissionDeniedFullPage({
permissionName,
}: PermissionDeniedFullPageProps): JSX.Element {
return (
<div className={styles.container}>
<div className={styles.content}>
<span className={styles.icon}>
<CircleSlash2 color={Style.CALLOUT_WARNING_TITLE} size={14} />
</span>
<p className={styles.title}>
Uh-oh! You don&apos;t have permission to view this page.
</p>
<p className={styles.subtitle}>
You need <code className={styles.permission}>{permissionName}</code> to
view this page. Please ask your SigNoz administrator to grant access.
</p>
</div>
</div>
);
}
export default PermissionDeniedFullPage;

View File

@@ -32,10 +32,24 @@ import { isKeyMatch } from './utils';
import './Checkbox.styles.scss';
const SELECTED_OPERATORS = [OPERATORS['='], 'in'];
const NON_SELECTED_OPERATORS = [OPERATORS['!='], 'not in'];
const NON_SELECTED_OPERATORS = [OPERATORS['!='], 'not in', 'nin'];
const SOURCES_WITH_EMPTY_STATE_ENABLED = [QuickFiltersSource.LOGS_EXPLORER];
// Sources that use backend APIs expecting short operator format (e.g., 'nin' instead of 'not in')
const SOURCES_WITH_SHORT_OPERATORS = [QuickFiltersSource.INFRA_MONITORING];
/**
* Returns the correct NOT_IN operator value based on source.
* InfraMonitoring backend expects 'nin', others expect 'not in'.
*/
function getNotInOperator(source: QuickFiltersSource): string {
if (SOURCES_WITH_SHORT_OPERATORS.includes(source)) {
return 'nin';
}
return getOperatorValue('NOT_IN');
}
function setDefaultValues(
values: string[],
trueOrFalse: boolean,
@@ -401,6 +415,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
}
}
break;
case 'nin':
case 'not in':
// if the current running operator is NIN then when unchecking the value it gets
// added to the clause like key NIN [value1 , currentUnselectedValue]
@@ -495,7 +510,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
if (!checked) {
const newFilter = {
...currentFilter,
op: getOperatorValue('NOT_IN'),
op: getNotInOperator(source),
value: [currentFilter.value as string, value],
};
query.filters.items = query.filters.items.map((item) => {
@@ -518,7 +533,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
// case - when there is no filter for the current key that means all are selected right now.
const newFilterItem: TagFilterItem = {
id: uuid(),
op: getOperatorValue('NOT_IN'),
op: getNotInOperator(source),
key: filter.attributeKey,
value,
};

View File

@@ -80,6 +80,7 @@ interface BaseProps {
isError?: boolean;
error?: APIError;
onRefetch?: () => void;
disabled?: boolean;
}
interface SingleProps extends BaseProps {
@@ -123,6 +124,7 @@ function RolesSelect(props: RolesSelectProps): JSX.Element {
isError = internalError,
error = convertToApiError(internalErrorObj),
onRefetch = externalRoles === undefined ? internalRefetch : undefined,
disabled,
} = props;
const notFoundContent = isError ? (
@@ -151,6 +153,7 @@ function RolesSelect(props: RolesSelectProps): JSX.Element {
</Checkbox>
)}
getPopupContainer={getPopupContainer}
disabled={disabled}
/>
);
}
@@ -168,6 +171,7 @@ function RolesSelect(props: RolesSelectProps): JSX.Element {
notFoundContent={notFoundContent}
options={options}
getPopupContainer={getPopupContainer}
disabled={disabled}
/>
);
}

View File

@@ -4,6 +4,11 @@ import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
import { DatePicker } from 'antd';
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
import {
APIKeyCreatePermission,
buildSAAttachPermission,
} from 'hooks/useAuthZ/permissions/service-account.permissions';
import { popupContainer } from 'utils/selectPopupContainer';
import { disabledDate } from '../utils';
@@ -18,6 +23,7 @@ export interface KeyFormPhaseProps {
isValid: boolean;
onSubmit: () => void;
onClose: () => void;
accountId?: string;
}
function KeyFormPhase({
@@ -28,6 +34,7 @@ function KeyFormPhase({
isValid,
onSubmit,
onClose,
accountId,
}: KeyFormPhaseProps): JSX.Element {
return (
<>
@@ -111,17 +118,25 @@ function KeyFormPhase({
<Button variant="solid" color="secondary" onClick={onClose}>
Cancel
</Button>
<Button
type="submit"
// @ts-expect-error -- form prop not in @signozhq/ui Button type - TODO: Fix this - @SagarRajput
form={FORM_ID}
variant="solid"
color="primary"
loading={isSubmitting}
disabled={!isValid}
<AuthZTooltip
checks={[
APIKeyCreatePermission,
buildSAAttachPermission(accountId ?? ''),
]}
enabled={!!accountId}
>
Create Key
</Button>
<Button
type="submit"
// @ts-expect-error -- form prop not in @signozhq/ui Button type - TODO: Fix this - @SagarRajput
form={FORM_ID}
variant="solid"
color="primary"
loading={isSubmitting}
disabled={!isValid}
>
Create Key
</Button>
</AuthZTooltip>
</div>
</div>
</>

View File

@@ -161,6 +161,7 @@ function AddKeyModal(): JSX.Element {
isValid={isValid}
onSubmit={handleSubmit(handleCreate)}
onClose={handleClose}
accountId={accountId ?? undefined}
/>
)}

View File

@@ -1,6 +1,8 @@
import { useQueryClient } from 'react-query';
import { Trash2, X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
import { buildSADeletePermission } from 'hooks/useAuthZ/permissions/service-account.permissions';
import { DialogWrapper } from '@signozhq/ui/dialog';
import { toast } from '@signozhq/ui/sonner';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
@@ -65,7 +67,7 @@ function DeleteAccountModal(): JSX.Element {
}
function handleCancel(): void {
setIsDeleteOpen(null);
void setIsDeleteOpen(null);
}
const content = (
@@ -82,15 +84,20 @@ function DeleteAccountModal(): JSX.Element {
<X size={12} />
Cancel
</Button>
<Button
variant="solid"
color="destructive"
loading={isDeleting}
onClick={handleConfirm}
<AuthZTooltip
checks={[buildSADeletePermission(accountId ?? '')]}
enabled={!!accountId}
>
<Trash2 size={12} />
Delete
</Button>
<Button
variant="solid"
color="destructive"
loading={isDeleting}
onClick={handleConfirm}
>
<Trash2 size={12} />
Delete
</Button>
</AuthZTooltip>
</div>
);

View File

@@ -7,6 +7,12 @@ import { Input } from '@signozhq/ui/input';
import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
import { DatePicker } from 'antd';
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
import {
buildAPIKeyDeletePermission,
buildAPIKeyUpdatePermission,
buildSADetachPermission,
} from 'hooks/useAuthZ/permissions/service-account.permissions';
import { popupContainer } from 'utils/selectPopupContainer';
import { disabledDate, formatLastObservedAt } from '../utils';
@@ -24,6 +30,8 @@ export interface EditKeyFormProps {
onClose: () => void;
onRevokeClick: () => void;
formatTimezoneAdjustedTimestamp: (ts: string, format: string) => string;
canUpdate?: boolean;
accountId?: string;
}
function EditKeyForm({
@@ -37,6 +45,8 @@ function EditKeyForm({
onClose,
onRevokeClick,
formatTimezoneAdjustedTimestamp,
canUpdate = true,
accountId = '',
}: EditKeyFormProps): JSX.Element {
return (
<>
@@ -45,12 +55,34 @@ function EditKeyForm({
<label className="edit-key-modal__label" htmlFor="edit-key-name">
Name
</label>
<Input
id="edit-key-name"
className="edit-key-modal__input"
placeholder="Enter key name"
{...register('name')}
/>
{!canUpdate ? (
<AuthZTooltip
checks={[buildAPIKeyUpdatePermission(keyItem?.id ?? '')]}
enabled={!!keyItem?.id}
>
<div className="edit-key-modal__key-display">
<span className="edit-key-modal__id-text">{keyItem?.name || '—'}</span>
<LockKeyhole size={12} className="edit-key-modal__lock-icon" />
</div>
</AuthZTooltip>
) : (
<Input
id="edit-key-name"
className="edit-key-modal__input"
placeholder="Enter key name"
{...register('name')}
/>
)}
</div>
<div className="edit-key-modal__field">
<label className="edit-key-modal__label" htmlFor="edit-key-id">
ID
</label>
<div id="edit-key-id" className="edit-key-modal__key-display">
<span className="edit-key-modal__id-text">{keyItem?.id || '—'}</span>
<LockKeyhole size={12} className="edit-key-modal__lock-icon" />
</div>
</div>
<div className="edit-key-modal__field">
@@ -73,21 +105,22 @@ function EditKeyForm({
type="single"
value={field.value}
onChange={(val): void => {
if (val) {
if (val && canUpdate) {
field.onChange(val);
}
}}
size="sm"
className="edit-key-modal__expiry-toggle"
>
<ToggleGroupItem
value={ExpiryMode.NONE}
disabled={!canUpdate}
className="edit-key-modal__expiry-toggle-btn"
>
No Expiration
</ToggleGroupItem>
<ToggleGroupItem
value={ExpiryMode.DATE}
disabled={!canUpdate}
className="edit-key-modal__expiry-toggle-btn"
>
Set Expiration Date
@@ -114,6 +147,7 @@ function EditKeyForm({
popupClassName="edit-key-modal-datepicker-popup"
getPopupContainer={popupContainer}
disabledDate={disabledDate}
disabled={!canUpdate}
/>
)}
/>
@@ -133,26 +167,39 @@ function EditKeyForm({
</form>
<div className="edit-key-modal__footer">
<Button variant="link" color="destructive" onClick={onRevokeClick}>
<Trash2 size={12} />
Revoke Key
</Button>
<AuthZTooltip
checks={[
buildAPIKeyDeletePermission(keyItem?.id ?? ''),
buildSADetachPermission(accountId ?? ''),
]}
enabled={!!accountId && !!keyItem?.id}
>
<Button variant="link" color="destructive" onClick={onRevokeClick}>
<Trash2 size={12} />
Revoke Key
</Button>
</AuthZTooltip>
<div className="edit-key-modal__footer-right">
<Button variant="solid" color="secondary" onClick={onClose}>
<X size={12} />
Cancel
</Button>
<Button
type="submit"
// @ts-expect-error -- form prop not in @signozhq/ui Button type - TODO: Fix this - @SagarRajput
form={FORM_ID}
variant="solid"
color="primary"
loading={isSaving}
disabled={!isDirty}
<AuthZTooltip
checks={[buildAPIKeyUpdatePermission(keyItem?.id ?? '')]}
enabled={!!accountId && !!keyItem?.id}
>
Save Changes
</Button>
<Button
type="submit"
// @ts-expect-error -- form prop not in @signozhq/ui Button type - TODO: Fix this - @SagarRajput
form={FORM_ID}
variant="solid"
color="primary"
loading={isSaving}
disabled={!isDirty}
>
Save Changes
</Button>
</AuthZTooltip>
</div>
</div>
</>

View File

@@ -60,6 +60,16 @@
letter-spacing: 2px;
}
&__id-text {
font-size: 13px;
font-family: monospace;
color: var(--foreground);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
&__lock-icon {
color: var(--foreground);
flex-shrink: 0;

View File

@@ -16,6 +16,8 @@ import type {
import { AxiosError } from 'axios';
import { SA_QUERY_PARAMS } from 'container/ServiceAccountsSettings/constants';
import dayjs from 'dayjs';
import { buildAPIKeyUpdatePermission } from 'hooks/useAuthZ/permissions/service-account.permissions';
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
import { parseAsString, useQueryState } from 'nuqs';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { useTimezone } from 'providers/Timezone';
@@ -69,6 +71,16 @@ function EditKeyModal({ keyItem }: EditKeyModalProps): JSX.Element {
const expiryMode = watch('expiryMode');
const { permissions: editPermissions, isLoading: isAuthZLoading } = useAuthZ(
editKeyId ? [buildAPIKeyUpdatePermission(editKeyId)] : [],
{ enabled: !!editKeyId },
);
const canUpdate = isAuthZLoading
? false
: (editPermissions?.[buildAPIKeyUpdatePermission(editKeyId ?? '')]
?.isGranted ?? true);
const { mutate: updateKey, isLoading: isSaving } = useUpdateServiceAccountKey({
mutation: {
onSuccess: async () => {
@@ -115,7 +127,7 @@ function EditKeyModal({ keyItem }: EditKeyModalProps): JSX.Element {
});
function handleClose(): void {
setEditKeyId(null);
void setEditKeyId(null);
setIsRevokeConfirmOpen(false);
}
@@ -169,6 +181,8 @@ function EditKeyModal({ keyItem }: EditKeyModalProps): JSX.Element {
isRevoking={isRevoking}
onCancel={(): void => setIsRevokeConfirmOpen(false)}
onConfirm={handleRevoke}
accountId={selectedAccountId ?? undefined}
keyId={keyItem?.id ?? undefined}
/>
) : undefined
}
@@ -190,6 +204,8 @@ function EditKeyModal({ keyItem }: EditKeyModalProps): JSX.Element {
onClose={handleClose}
onRevokeClick={(): void => setIsRevokeConfirmOpen(true)}
formatTimezoneAdjustedTimestamp={formatTimezoneAdjustedTimestamp}
canUpdate={canUpdate}
accountId={selectedAccountId ?? ''}
/>
)}
</DialogWrapper>

View File

@@ -1,9 +1,16 @@
import { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';
import { KeyRound, X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Skeleton, Table, Tooltip } from 'antd';
import { Skeleton, Table } from 'antd';
import type { ColumnsType } from 'antd/es/table/interface';
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
import {
APIKeyCreatePermission,
buildAPIKeyDeletePermission,
buildSAAttachPermission,
buildSADetachPermission,
} from 'hooks/useAuthZ/permissions/service-account.permissions';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import dayjs from 'dayjs';
import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs';
@@ -17,12 +24,15 @@ interface KeysTabProps {
keys: ServiceaccounttypesGettableFactorAPIKeyDTO[];
isLoading: boolean;
isDisabled?: boolean;
canUpdate?: boolean;
accountId?: string;
currentPage: number;
pageSize: number;
}
interface BuildColumnsParams {
isDisabled: boolean;
accountId: string;
onRevokeClick: (keyId: string) => void;
handleformatLastObservedAt: (
lastObservedAt: Date | null | undefined,
@@ -42,6 +52,7 @@ function formatExpiry(expiresAt: number): JSX.Element {
function buildColumns({
isDisabled,
accountId,
onRevokeClick,
handleformatLastObservedAt,
}: BuildColumnsParams): ColumnsType<ServiceaccounttypesGettableFactorAPIKeyDTO> {
@@ -92,22 +103,34 @@ function buildColumns({
key: 'action',
width: 48,
align: 'right' as const,
onCell: (): {
onClick: (e: React.MouseEvent) => void;
style: React.CSSProperties;
} => ({
onClick: (e): void => e.stopPropagation(),
style: { cursor: 'default' },
}),
render: (_, record): JSX.Element => (
<Tooltip title={isDisabled ? 'Service account disabled' : 'Revoke Key'}>
<AuthZTooltip
checks={[
buildAPIKeyDeletePermission(record.id),
buildSADetachPermission(accountId),
]}
enabled={!isDisabled && !!accountId}
>
<Button
variant="ghost"
size="sm"
color="destructive"
disabled={isDisabled}
onClick={(e): void => {
e.stopPropagation();
onClick={(): void => {
onRevokeClick(record.id);
}}
className="keys-tab__revoke-btn"
>
<X size={12} />
</Button>
</Tooltip>
</AuthZTooltip>
),
},
];
@@ -117,6 +140,7 @@ function KeysTab({
keys,
isLoading,
isDisabled = false,
accountId = '',
currentPage,
pageSize,
}: KeysTabProps): JSX.Element {
@@ -143,14 +167,20 @@ function KeysTab({
const onRevokeClick = useCallback(
(keyId: string): void => {
setRevokeKeyId(keyId);
void setRevokeKeyId(keyId);
},
[setRevokeKeyId],
);
const columns = useMemo(
() => buildColumns({ isDisabled, onRevokeClick, handleformatLastObservedAt }),
[isDisabled, onRevokeClick, handleformatLastObservedAt],
() =>
buildColumns({
isDisabled,
accountId,
onRevokeClick,
handleformatLastObservedAt,
}),
[isDisabled, accountId, onRevokeClick, handleformatLastObservedAt],
);
if (isLoading) {
@@ -176,16 +206,21 @@ function KeysTab({
Learn more
</a>
</p>
<Button
variant="link"
color="primary"
onClick={async (): Promise<void> => {
await setIsAddKeyOpen(true);
}}
disabled={isDisabled}
<AuthZTooltip
checks={[APIKeyCreatePermission, buildSAAttachPermission(accountId)]}
enabled={!isDisabled && !!accountId}
>
+ Add your first key
</Button>
<Button
variant="link"
color="primary"
onClick={async (): Promise<void> => {
await setIsAddKeyOpen(true);
}}
disabled={isDisabled}
>
+ Add your first key
</Button>
</AuthZTooltip>
</div>
);
}

View File

@@ -3,9 +3,11 @@ import { LockKeyhole } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { Input } from '@signozhq/ui/input';
import type { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
import RolesSelect from 'components/RolesSelect';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { ServiceAccountRow } from 'container/ServiceAccountsSettings/utils';
import { buildSAUpdatePermission } from 'hooks/useAuthZ/permissions/service-account.permissions';
import { useTimezone } from 'providers/Timezone';
import APIError from 'types/api/error';
@@ -19,6 +21,7 @@ interface OverviewTabProps {
localRoles: string[];
onRolesChange: (v: string[]) => void;
isDisabled: boolean;
canUpdate?: boolean;
availableRoles: AuthtypesRoleDTO[];
rolesLoading?: boolean;
rolesError?: boolean;
@@ -34,6 +37,7 @@ function OverviewTab({
localRoles,
onRolesChange,
isDisabled,
canUpdate = true,
availableRoles,
rolesLoading,
rolesError,
@@ -63,11 +67,16 @@ function OverviewTab({
<label className="sa-drawer__label" htmlFor="sa-name">
Name
</label>
{isDisabled ? (
<div className="sa-drawer__input-wrapper sa-drawer__input-wrapper--disabled">
<span className="sa-drawer__input-text">{localName || '—'}</span>
<LockKeyhole size={14} className="sa-drawer__lock-icon" />
</div>
{isDisabled || !canUpdate ? (
<AuthZTooltip
checks={[buildSAUpdatePermission(account.id)]}
enabled={!isDisabled && !canUpdate}
>
<div className="sa-drawer__input-wrapper sa-drawer__input-wrapper--disabled">
<span className="sa-drawer__input-text">{localName || '—'}</span>
<LockKeyhole size={14} className="sa-drawer__lock-icon" />
</div>
</AuthZTooltip>
) : (
<Input
id="sa-name"
@@ -78,6 +87,16 @@ function OverviewTab({
)}
</div>
<div className="sa-drawer__field">
<label className="sa-drawer__label" htmlFor="sa-id">
ID
</label>
<div className="sa-drawer__input-wrapper sa-drawer__input-wrapper--disabled">
<span className="sa-drawer__input-text">{account.id || '—'}</span>
<LockKeyhole size={14} className="sa-drawer__lock-icon" />
</div>
</div>
<div className="sa-drawer__field">
<label className="sa-drawer__label" htmlFor="sa-email">
Email Address

View File

@@ -1,6 +1,11 @@
import { useQueryClient } from 'react-query';
import { Trash2, X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
import {
buildAPIKeyDeletePermission,
buildSADetachPermission,
} from 'hooks/useAuthZ/permissions/service-account.permissions';
import { DialogWrapper } from '@signozhq/ui/dialog';
import { toast } from '@signozhq/ui/sonner';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
@@ -23,12 +28,16 @@ export interface RevokeKeyFooterProps {
isRevoking: boolean;
onCancel: () => void;
onConfirm: () => void;
accountId?: string;
keyId?: string;
}
export function RevokeKeyFooter({
isRevoking,
onCancel,
onConfirm,
accountId,
keyId,
}: RevokeKeyFooterProps): JSX.Element {
return (
<>
@@ -36,15 +45,23 @@ export function RevokeKeyFooter({
<X size={12} />
Cancel
</Button>
<Button
variant="solid"
color="destructive"
loading={isRevoking}
onClick={onConfirm}
<AuthZTooltip
checks={[
buildAPIKeyDeletePermission(keyId ?? ''),
buildSADetachPermission(accountId ?? ''),
]}
enabled={!!accountId && !!keyId}
>
<Trash2 size={12} />
Revoke Key
</Button>
<Button
variant="solid"
color="destructive"
loading={isRevoking}
onClick={onConfirm}
>
<Trash2 size={12} />
Revoke Key
</Button>
</AuthZTooltip>
</>
);
}
@@ -115,6 +132,8 @@ function RevokeKeyModal(): JSX.Element {
isRevoking={isRevoking}
onCancel={handleCancel}
onConfirm={handleConfirm}
accountId={accountId ?? undefined}
keyId={revokeKeyId || undefined}
/>
}
>

View File

@@ -16,6 +16,8 @@ import {
import type { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import { GuardAuthZ } from 'components/GuardAuthZ/GuardAuthZ';
import PermissionDeniedCallout from 'components/PermissionDeniedCallout/PermissionDeniedCallout';
import { useRoles } from 'components/RolesSelect';
import { SA_QUERY_PARAMS } from 'container/ServiceAccountsSettings/constants';
import {
@@ -27,6 +29,15 @@ import {
RoleUpdateFailure,
useServiceAccountRoleManager,
} from 'hooks/serviceAccount/useServiceAccountRoleManager';
import {
APIKeyCreatePermission,
APIKeyListPermission,
buildSAAttachPermission,
buildSADeletePermission,
buildSAReadPermission,
buildSAUpdatePermission,
} from 'hooks/useAuthZ/permissions/service-account.permissions';
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
import {
parseAsBoolean,
parseAsInteger,
@@ -37,6 +48,7 @@ import {
import APIError from 'types/api/error';
import { toAPIError } from 'utils/errorUtils';
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
import AddKeyModal from './AddKeyModal';
import DeleteAccountModal from './DeleteAccountModal';
import KeysTab from './KeysTab';
@@ -96,6 +108,22 @@ function ServiceAccountDrawer({
const queryClient = useQueryClient();
const { permissions: drawerPermissions, isLoading: isAuthZLoading } = useAuthZ(
selectedAccountId
? [
buildSAReadPermission(selectedAccountId),
buildSAUpdatePermission(selectedAccountId),
buildSADeletePermission(selectedAccountId),
APIKeyListPermission,
]
: [],
{ enabled: !!selectedAccountId },
);
const canRead =
drawerPermissions?.[buildSAReadPermission(selectedAccountId ?? '')]
?.isGranted ?? false;
const {
data: accountData,
isLoading: isAccountLoading,
@@ -104,7 +132,7 @@ function ServiceAccountDrawer({
refetch: refetchAccount,
} = useGetServiceAccount(
{ id: selectedAccountId ?? '' },
{ query: { enabled: !!selectedAccountId } },
{ query: { enabled: canRead && !!selectedAccountId } },
);
const account = useMemo(
@@ -117,7 +145,9 @@ function ServiceAccountDrawer({
currentRoles,
isLoading: isRolesLoading,
applyDiff,
} = useServiceAccountRoleManager(selectedAccountId ?? '');
} = useServiceAccountRoleManager(selectedAccountId ?? '', {
enabled: canRead && !!selectedAccountId,
});
const roleSessionRef = useRef<string | null>(null);
@@ -165,9 +195,16 @@ function ServiceAccountDrawer({
refetch: refetchRoles,
} = useRoles();
const canListKeys =
drawerPermissions?.[APIKeyListPermission]?.isGranted ?? false;
const canUpdate =
drawerPermissions?.[buildSAUpdatePermission(selectedAccountId ?? '')]
?.isGranted ?? true;
const { data: keysData, isLoading: keysLoading } = useListServiceAccountKeys(
{ id: selectedAccountId ?? '' },
{ query: { enabled: !!selectedAccountId } },
{ query: { enabled: !!selectedAccountId && canListKeys } },
);
const keys = keysData?.data ?? [];
@@ -392,18 +429,26 @@ function ServiceAccountDrawer({
</ToggleGroupItem>
</ToggleGroup>
{activeTab === ServiceAccountDrawerTab.Keys && (
<Button
variant="outlined"
size="sm"
color="secondary"
disabled={isDeleted}
onClick={(): void => {
void setIsAddKeyOpen(true);
}}
<AuthZTooltip
checks={[
APIKeyCreatePermission,
buildSAAttachPermission(selectedAccountId ?? ''),
]}
enabled={!isDeleted && !!selectedAccountId}
>
<Plus size={12} />
Add Key
</Button>
<Button
variant="outlined"
size="sm"
color="secondary"
disabled={isDeleted}
onClick={(): void => {
void setIsAddKeyOpen(true);
}}
>
<Plus size={12} />
Add Key
</Button>
</AuthZTooltip>
)}
</div>
@@ -412,7 +457,9 @@ function ServiceAccountDrawer({
activeTab === ServiceAccountDrawerTab.Keys ? ' sa-drawer__body--keys' : ''
}`}
>
{isAccountLoading && <Skeleton active paragraph={{ rows: 6 }} />}
{(isAuthZLoading || isAccountLoading) && (
<Skeleton active paragraph={{ rows: 6 }} />
)}
{isAccountError && (
<ErrorInPlace
error={toAPIError(
@@ -421,38 +468,55 @@ function ServiceAccountDrawer({
)}
/>
)}
{!isAccountLoading && !isAccountError && (
<>
{activeTab === ServiceAccountDrawerTab.Overview && account && (
<OverviewTab
account={account}
localName={localName}
onNameChange={handleNameChange}
localRoles={localRoles}
onRolesChange={(roles): void => {
setLocalRoles(roles);
clearRoleErrors();
}}
isDisabled={isDeleted}
availableRoles={availableRoles}
rolesLoading={rolesLoading}
rolesError={rolesError}
rolesErrorObj={rolesErrorObj}
onRefetchRoles={refetchRoles}
saveErrors={saveErrors}
/>
)}
{activeTab === ServiceAccountDrawerTab.Keys && (
<KeysTab
keys={keys}
isLoading={keysLoading}
isDisabled={isDeleted}
currentPage={keysPage}
pageSize={PAGE_SIZE}
/>
)}
</>
)}
{!isAuthZLoading &&
!isAccountLoading &&
!isAccountError &&
selectedAccountId && (
<GuardAuthZ
relation="read"
object={`serviceaccount:${selectedAccountId}`}
fallbackOnNoPermissions={(): JSX.Element => (
<PermissionDeniedCallout permissionName="serviceaccount:read" />
)}
>
<>
{activeTab === ServiceAccountDrawerTab.Overview && account && (
<OverviewTab
account={account}
localName={localName}
onNameChange={handleNameChange}
localRoles={localRoles}
onRolesChange={(roles): void => {
setLocalRoles(roles);
clearRoleErrors();
}}
isDisabled={isDeleted}
canUpdate={canUpdate}
availableRoles={availableRoles}
rolesLoading={rolesLoading}
rolesError={rolesError}
rolesErrorObj={rolesErrorObj}
onRefetchRoles={refetchRoles}
saveErrors={saveErrors}
/>
)}
{activeTab === ServiceAccountDrawerTab.Keys &&
(canListKeys ? (
<KeysTab
keys={keys}
isLoading={keysLoading}
isDisabled={isDeleted}
canUpdate={canUpdate}
accountId={selectedAccountId}
currentPage={keysPage}
pageSize={PAGE_SIZE}
/>
) : (
<PermissionDeniedCallout permissionName="factor-api-key:list" />
))}
</>
</GuardAuthZ>
)}
</div>
</div>
);
@@ -482,16 +546,21 @@ function ServiceAccountDrawer({
) : (
<>
{!isDeleted && (
<Button
variant="link"
color="destructive"
onClick={(): void => {
void setIsDeleteOpen(true);
}}
<AuthZTooltip
checks={[buildSADeletePermission(selectedAccountId ?? '')]}
enabled={!!selectedAccountId}
>
<Trash2 size={12} />
Delete Service Account
</Button>
<Button
variant="link"
color="destructive"
onClick={(): void => {
void setIsDeleteOpen(true);
}}
>
<Trash2 size={12} />
Delete Service Account
</Button>
</AuthZTooltip>
)}
{!isDeleted && (
<div className="sa-drawer__footer-right">

View File

@@ -6,6 +6,15 @@ import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import EditKeyModal from '../EditKeyModal';
jest.mock('components/AuthZTooltip/AuthZTooltip', () => ({
__esModule: true,
default: ({
children,
}: {
children: React.ReactElement;
}): React.ReactElement => children,
}));
jest.mock('@signozhq/ui/sonner', () => ({
...jest.requireActual('@signozhq/ui/sonner'),
toast: { success: jest.fn(), error: jest.fn() },
@@ -19,7 +28,7 @@ const mockKey: ServiceaccounttypesGettableFactorAPIKeyDTO = {
id: 'key-1',
name: 'Original Key Name',
expiresAt: 0,
lastObservedAt: null as any,
lastObservedAt: null as unknown as Date,
serviceAccountId: 'sa-1',
};

View File

@@ -6,6 +6,15 @@ import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import KeysTab from '../KeysTab';
jest.mock('components/AuthZTooltip/AuthZTooltip', () => ({
__esModule: true,
default: ({
children,
}: {
children: React.ReactElement;
}): React.ReactElement => children,
}));
jest.mock('@signozhq/ui/sonner', () => ({
...jest.requireActual('@signozhq/ui/sonner'),
toast: { success: jest.fn(), error: jest.fn() },
@@ -20,7 +29,7 @@ const keys: ServiceaccounttypesGettableFactorAPIKeyDTO[] = [
id: 'key-1',
name: 'Production Key',
expiresAt: 0,
lastObservedAt: null as any,
lastObservedAt: null as unknown as Date,
serviceAccountId: 'sa-1',
},
{

View File

@@ -0,0 +1,158 @@
import type { ReactNode } from 'react';
import { listRolesSuccessResponse } from 'mocks-server/__mockdata__/roles';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
import {
setupAuthzAdmin,
setupAuthzDeny,
setupAuthzDenyAll,
} from 'tests/authz-test-utils';
import {
APIKeyListPermission,
buildSADeletePermission,
} from 'hooks/useAuthZ/permissions/service-account.permissions';
import ServiceAccountDrawer from '../ServiceAccountDrawer';
const ROLES_ENDPOINT = '*/api/v1/roles';
const SA_KEYS_ENDPOINT = '*/api/v1/service_accounts/:id/keys';
const SA_ENDPOINT = '*/api/v1/service_accounts/sa-1';
const SA_DELETE_ENDPOINT = '*/api/v1/service_accounts/sa-1';
const SA_ROLES_ENDPOINT = '*/api/v1/service_accounts/:id/roles';
const SA_ROLE_DELETE_ENDPOINT = '*/api/v1/service_accounts/:id/roles/:rid';
const activeAccountResponse = {
id: 'sa-1',
name: 'CI Bot',
email: 'ci-bot@signoz.io',
roles: ['signoz-admin'],
status: 'ACTIVE',
createdAt: '2026-01-01T00:00:00Z',
updatedAt: '2026-01-02T00:00:00Z',
};
jest.mock('@signozhq/ui/drawer', () => ({
...jest.requireActual('@signozhq/ui/drawer'),
DrawerWrapper: ({
children,
footer,
open,
}: {
children?: ReactNode;
footer?: ReactNode;
open: boolean;
}): JSX.Element | null =>
open ? (
<div>
{children}
{footer}
</div>
) : null,
}));
jest.mock('@signozhq/ui/sonner', () => ({
...jest.requireActual('@signozhq/ui/sonner'),
toast: { success: jest.fn(), error: jest.fn() },
}));
function renderDrawer(
searchParams: Record<string, string> = { account: 'sa-1' },
): ReturnType<typeof render> {
return render(
<NuqsTestingAdapter searchParams={searchParams} hasMemory>
<ServiceAccountDrawer onSuccess={jest.fn()} />
</NuqsTestingAdapter>,
);
}
function setupBaseHandlers(): void {
server.use(
rest.get(ROLES_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(listRolesSuccessResponse)),
),
rest.get(SA_KEYS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ data: [] })),
),
rest.get(SA_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ data: activeAccountResponse })),
),
rest.put(SA_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
),
rest.delete(SA_DELETE_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
),
rest.get(SA_ROLES_ENDPOINT, (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
data: listRolesSuccessResponse.data.filter(
(r) => r.name === 'signoz-admin',
),
}),
),
),
rest.post(SA_ROLES_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
),
rest.delete(SA_ROLE_DELETE_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
),
);
}
describe('ServiceAccountDrawer — permissions', () => {
beforeEach(() => {
jest.clearAllMocks();
setupBaseHandlers();
});
afterEach(() => {
server.resetHandlers();
});
it('shows PermissionDeniedCallout inside drawer when read permission is denied', async () => {
server.use(setupAuthzDenyAll());
renderDrawer();
await waitFor(() => {
expect(screen.getByText(/serviceaccount:read/)).toBeInTheDocument();
});
});
it('shows drawer content when read permission is granted', async () => {
server.use(setupAuthzAdmin());
renderDrawer();
await screen.findByDisplayValue('CI Bot');
expect(screen.queryByText(/serviceaccount:read/)).not.toBeInTheDocument();
});
it('shows PermissionDeniedCallout in Keys tab when list-keys permission is denied', async () => {
server.use(setupAuthzDeny(APIKeyListPermission));
renderDrawer();
await screen.findByDisplayValue('CI Bot');
fireEvent.click(screen.getByRole('radio', { name: /keys/i }));
await waitFor(() => {
expect(screen.getByText(/factor-api-key:list/)).toBeInTheDocument();
});
});
it('disables Delete button when delete permission is denied', async () => {
server.use(setupAuthzDeny(buildSADeletePermission('sa-1')));
renderDrawer();
await screen.findByDisplayValue('CI Bot');
const deleteBtn = screen.getByRole('button', {
name: /Delete Service Account/i,
});
await waitFor(() => expect(deleteBtn).toBeDisabled());
});
});

View File

@@ -3,6 +3,7 @@ import { listRolesSuccessResponse } from 'mocks-server/__mockdata__/roles';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import { setupAuthzAdmin } from 'tests/authz-test-utils';
import ServiceAccountDrawer from '../ServiceAccountDrawer';
@@ -98,6 +99,7 @@ describe('ServiceAccountDrawer', () => {
rest.delete(SA_ROLE_DELETE_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
),
setupAuthzAdmin(),
);
});
@@ -300,13 +302,6 @@ describe('ServiceAccountDrawer', () => {
await screen.findByText(/No keys/i);
});
it('shows skeleton while loading account data', () => {
renderDrawer();
// Skeleton renders while the fetch is in-flight
expect(document.querySelector('.ant-skeleton')).toBeInTheDocument();
});
it('shows error state when account fetch fails', async () => {
server.use(
rest.get(SA_ENDPOINT, (_, res, ctx) =>
@@ -359,6 +354,7 @@ describe('ServiceAccountDrawer save-error UX', () => {
rest.delete(SA_ROLE_DELETE_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
),
setupAuthzAdmin(),
);
});

View File

@@ -1,33 +1,16 @@
import { ReactElement } from 'react';
import type { RouteComponentProps } from 'react-router-dom';
import {
import type {
AuthtypesGettableTransactionDTO,
AuthtypesTransactionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { ENVIRONMENT } from 'constants/env';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { render, screen, waitFor } from 'tests/test-utils';
import { AUTHZ_CHECK_URL, authzMockResponse } from 'tests/authz-test-utils';
import { createGuardedRoute } from './createGuardedRoute';
const BASE_URL = ENVIRONMENT.baseURL || '';
const AUTHZ_CHECK_URL = `${BASE_URL}/api/v1/authz/check`;
function authzMockResponse(
payload: AuthtypesTransactionDTO[],
authorizedByIndex: boolean[],
): { data: AuthtypesGettableTransactionDTO[]; status: string } {
return {
data: payload.map((txn, i) => ({
relation: txn.relation,
object: txn.object,
authorized: authorizedByIndex[i] ?? false,
})),
status: 'success',
};
}
describe('createGuardedRoute', () => {
const TestComponent = ({ testProp }: { testProp: string }): ReactElement => (
<div>Test Component: {testProp}</div>

View File

@@ -34,7 +34,7 @@ function OnNoPermissionsFallback(response: {
<br />
Object: <span>{object}</span>
<br />
Ask your SigNoz administrator to grant access.
Please ask your SigNoz administrator to grant access.
</p>
</div>
</div>

View File

@@ -431,6 +431,31 @@ export const OPERATORS = {
NOTILIKE: 'NOT_ILIKE',
};
/**
* Maps short-form InfraMonitoring operators to long-form display labels.
* InfraMonitoring backend uses short forms (NIN), UI displays long forms (NOT_IN).
*/
export const INFRA_SHORT_TO_LONG_OPERATOR_MAP: Record<string, string> = {
NIN: 'NOT_IN',
NLIKE: 'NOT_LIKE',
NOTILIKE: 'NOT_ILIKE',
NREGEX: 'NOT_REGEX',
NEXISTS: 'NOT_EXISTS',
NCONTAINS: 'NOT_CONTAINS',
};
/**
* Maps long-form operators to short-form for InfraMonitoring API.
*/
export const INFRA_LONG_TO_SHORT_OPERATOR_MAP: Record<string, string> = {
NOT_IN: 'NIN',
NOT_LIKE: 'NLIKE',
NOT_ILIKE: 'NOTILIKE',
NOT_REGEX: 'NREGEX',
NOT_EXISTS: 'NEXISTS',
NOT_CONTAINS: 'NCONTAINS',
};
export const QUERY_BUILDER_OPERATORS_BY_TYPES = {
string: [
OPERATORS['='],

View File

@@ -12,7 +12,10 @@ import { useLocation } from 'react-router-dom';
import { Button, Select, Spin, Tag, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import { OPERATORS } from 'constants/queryBuilder';
import {
INFRA_LONG_TO_SHORT_OPERATOR_MAP,
OPERATORS,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8s/constants';
@@ -68,6 +71,17 @@ import {
import './QueryBuilderSearch.styles.scss';
function getOperatorValueForContext(
op: string,
isInfraMonitoring?: boolean,
): string {
const mappedOp =
isInfraMonitoring && INFRA_LONG_TO_SHORT_OPERATOR_MAP[op]
? INFRA_LONG_TO_SHORT_OPERATOR_MAP[op]
: op;
return getOperatorValue(mappedOp);
}
function QueryBuilderSearch({
query,
onChange,
@@ -301,7 +315,7 @@ function QueryBuilderSearch({
dataType: fetchValueDataType(computedTagValue, tagOperator),
type: '',
},
op: getOperatorValue(tagOperator),
op: getOperatorValueForContext(tagOperator, isInfraMonitoring),
value: computedTagValue,
};
});

View File

@@ -7,10 +7,17 @@ import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { orderByValueDelimiter } from '../OrderByFilter/utils';
export const tagRegexp =
/^\s*(.*?)\s*(\bIN\b|\bNOT_IN\b|\bLIKE\b|\bNOT_LIKE\b|\bILIKE\b|\bNOT_ILIKE\b|\bREGEX\b|\bNOT_REGEX\b|=|!=|\bEXISTS\b|\bNOT_EXISTS\b|\bCONTAINS\b|\bNOT_CONTAINS\b|>=|>|<=|<|\bHAS\b|\bNHAS\b)\s*(.*)$/gi;
/^\s*(.*?)\s*(\bIN\b|\bNOT_IN\b|\bNIN\b|\bLIKE\b|\bNOT_LIKE\b|\bNLIKE\b|\bILIKE\b|\bNOT_ILIKE\b|\bNOTILIKE\b|\bREGEX\b|\bNOT_REGEX\b|\bNREGEX\b|=|!=|\bEXISTS\b|\bNOT_EXISTS\b|\bNEXISTS\b|\bCONTAINS\b|\bNOT_CONTAINS\b|\bNCONTAINS\b|>=|>|<=|<|\bHAS\b|\bNHAS\b)\s*(.*)$/gi;
export function isInNInOperator(value: string): boolean {
return value === OPERATORS.IN || value === OPERATORS.NIN;
return (
value === 'IN' ||
value === 'NOT_IN' ||
value === 'NIN' ||
value === 'in' ||
value === 'not in' ||
value === 'nin'
);
}
interface ITagToken {
@@ -45,7 +52,8 @@ export function isExistsNotExistsOperator(value: string): boolean {
const { tagOperator } = getTagToken(value);
return (
tagOperator?.trim() === OPERATORS.NOT_EXISTS ||
tagOperator?.trim() === OPERATORS.EXISTS
tagOperator?.trim() === OPERATORS.EXISTS ||
tagOperator?.trim() === 'NEXISTS'
);
}
@@ -83,6 +91,19 @@ export function getOperatorValue(op: string): string {
return 'contains';
case 'NOT_CONTAINS':
return 'not contains';
// Short form operators (InfraMonitoring)
case 'NIN':
return 'nin';
case 'NLIKE':
return 'nlike';
case 'NOTILIKE':
return 'notilike';
case 'NREGEX':
return 'nregex';
case 'NEXISTS':
return 'nexists';
case 'NCONTAINS':
return 'ncontains';
default:
return op;
}
@@ -114,6 +135,19 @@ export function getOperatorFromValue(op: string): string {
return OPERATORS.HAS;
case 'not has':
return OPERATORS.NHAS;
// Short-form operators (InfraMonitoring)
case 'nin':
return 'NIN';
case 'nlike':
return 'NLIKE';
case 'notilike':
return 'NOTILIKE';
case 'nregex':
return 'NREGEX';
case 'nexists':
return 'NEXISTS';
case 'ncontains':
return 'NCONTAINS';
default:
return op;
}

View File

@@ -29,18 +29,6 @@
border-bottom: 1px solid var(--l1-border);
}
&__close {
width: 16px;
height: 16px;
padding: 0;
color: var(--foreground);
flex-shrink: 0;
&:hover {
color: var(--l1-foreground);
}
}
&__header-divider {
display: block;
width: 1px;
@@ -167,7 +155,6 @@
line-height: 20px;
letter-spacing: -0.07px;
color: var(--l1-foreground);
text-transform: capitalize;
}
&__body {

View File

@@ -25,10 +25,13 @@ import { PermissionScope } from './PermissionSidePanel.types';
import './PermissionSidePanel.styles.scss';
const RELATIONS_ALL_ONLY = new Set(['list', 'create']);
interface ResourceRowProps {
resource: ResourceDefinition;
config: ResourceConfig;
isExpanded: boolean;
relation: string;
onToggleExpand: (id: string) => void;
onScopeChange: (id: string, scope: ScopeType) => void;
onSelectedIdsChange: (id: string, ids: string[]) => void;
@@ -38,10 +41,12 @@ function ResourceRow({
resource,
config,
isExpanded,
relation,
onToggleExpand,
onScopeChange,
onSelectedIdsChange,
}: ResourceRowProps): JSX.Element {
const showOnlySelected = !RELATIONS_ALL_ONLY.has(relation);
return (
<div className="psp-resource">
<div
@@ -78,36 +83,40 @@ function ResourceRow({
<RadioGroupLabel htmlFor={`${resource.id}-all`}>All</RadioGroupLabel>
</div>
{showOnlySelected && (
<div className="psp-resource__radio-item">
<RadioGroupItem
value={PermissionScope.ONLY_SELECTED}
id={`${resource.id}-only-selected`}
/>
<RadioGroupLabel htmlFor={`${resource.id}-only-selected`}>
Only selected
</RadioGroupLabel>
</div>
)}
<div className="psp-resource__radio-item">
<RadioGroupItem
value={PermissionScope.ONLY_SELECTED}
id={`${resource.id}-only-selected`}
value={PermissionScope.NONE}
id={`${resource.id}-none`}
/>
<RadioGroupLabel htmlFor={`${resource.id}-only-selected`}>
Only selected
</RadioGroupLabel>
<RadioGroupLabel htmlFor={`${resource.id}-none`}>None</RadioGroupLabel>
</div>
</RadioGroup>
{config.scope === PermissionScope.ONLY_SELECTED && (
{config.scope === PermissionScope.ONLY_SELECTED && showOnlySelected && (
<div className="psp-resource__select-wrapper">
{/* TODO: right now made to only accept user input, we need to give it proper resource based value fetching from APIs */}
<Select
mode="tags"
open={false}
allowClear
suffixIcon={null}
value={config.selectedIds}
onChange={(vals: string[]): void =>
onSelectedIdsChange(resource.id, vals)
}
options={resource.options ?? []}
placeholder="Select resources..."
placeholder="Type and press Enter to add..."
className="psp-resource__select"
popupClassName="psp-resource__select-popup"
showSearch
filterOption={(input, option): boolean =>
String(option?.label ?? '')
.toLowerCase()
.includes(input.toLowerCase())
}
/>
</div>
)}
@@ -121,10 +130,12 @@ function PermissionSidePanel({
open,
onClose,
permissionLabel,
relation,
resources,
initialConfig,
isLoading = false,
isSaving = false,
canEdit = true,
onSave,
}: PermissionSidePanelProps): JSX.Element | null {
const [config, setConfig] = useState<PermissionConfig>(() =>
@@ -213,13 +224,13 @@ function PermissionSidePanel({
<div className="permission-side-panel">
<div className="permission-side-panel__header">
<Button
variant="ghost"
variant="link"
color="secondary"
size="icon"
className="permission-side-panel__close"
onClick={onClose}
aria-label="Close panel"
>
<X size={16} />
<X size={14} />
</Button>
<span className="permission-side-panel__header-divider" />
<span className="permission-side-panel__title">
@@ -238,6 +249,7 @@ function PermissionSidePanel({
resource={resource}
config={config[resource.id] ?? DEFAULT_RESOURCE_CONFIG}
isExpanded={expandedIds.has(resource.id)}
relation={relation}
onToggleExpand={handleToggleExpand}
onScopeChange={handleScopeChange}
onSelectedIdsChange={handleSelectedIdsChange}
@@ -274,7 +286,7 @@ function PermissionSidePanel({
size="sm"
onClick={handleSave}
loading={isSaving}
disabled={isLoading || unsavedCount === 0}
disabled={isLoading || unsavedCount === 0 || !canEdit}
>
Save Changes
</Button>

View File

@@ -5,6 +5,8 @@ export interface ResourceOption {
export interface ResourceDefinition {
id: string;
kind: string;
type: string;
label: string;
options?: ResourceOption[];
}
@@ -12,6 +14,7 @@ export interface ResourceDefinition {
export enum PermissionScope {
ALL = 'all',
ONLY_SELECTED = 'only_selected',
NONE = 'none',
}
export type ScopeType = PermissionScope;
@@ -27,9 +30,11 @@ export interface PermissionSidePanelProps {
open: boolean;
onClose: () => void;
permissionLabel: string;
relation: string;
resources: ResourceDefinition[];
initialConfig?: PermissionConfig;
isLoading?: boolean;
isSaving?: boolean;
canEdit?: boolean;
onSave: (config: PermissionConfig) => void;
}

View File

@@ -9,8 +9,9 @@
.role-details-header {
display: flex;
flex-direction: column;
gap: 0;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.role-details-title {
@@ -28,44 +29,6 @@
opacity: 0.55;
}
.role-details-nav {
display: flex;
align-items: center;
justify-content: space-between;
}
.role-details-tab {
gap: 4px;
padding: 0 16px;
height: 32px;
border-radius: 0;
font-size: 12px;
overflow: hidden;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.06px;
&[data-state='on'] {
border-radius: 2px 0 0 2px;
}
}
.role-details-tab-count {
display: flex;
align-items: center;
justify-content: center;
min-width: 20px;
padding: 0 6px;
border-radius: 50px;
background: var(--secondary);
font-size: 12px;
font-weight: 400;
line-height: 20px;
color: var(--foreground);
letter-spacing: -0.06px;
text-transform: uppercase;
}
.role-details-actions {
display: flex;
align-items: center;
@@ -155,6 +118,17 @@
margin: 0;
}
.role-details-permissions-learn-more {
color: var(--primary);
font-size: var(--font-size-xs);
text-decoration: none;
white-space: nowrap;
&:hover {
text-decoration: underline;
}
}
.role-details-permission-list {
display: flex;
flex-direction: column;
@@ -282,30 +256,6 @@
}
}
.role-details-delete-action-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
min-width: 32px;
border: none;
border-radius: 2px;
background: transparent;
color: var(--destructive);
opacity: 0.6;
padding: 0;
transition:
background-color 0.2s,
opacity 0.2s;
box-shadow: none;
&:hover {
background: color-mix(in srgb, var(--danger-background) 10%, transparent);
opacity: 0.9;
}
}
.role-details-delete-modal {
width: calc(100% - 30px) !important;
max-width: 384px;

View File

@@ -1,10 +1,9 @@
import { useEffect, useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';
import { useHistory, useLocation } from 'react-router-dom';
import { Table2, Trash2, Users } from '@signozhq/icons';
import { Trash2 } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { toast } from '@signozhq/ui/sonner';
import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
import { Skeleton } from 'antd';
import {
getGetObjectsQueryKey,
@@ -13,7 +12,15 @@ import {
useGetRole,
usePatchObjects,
} from 'api/generated/services/role';
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
import PermissionDeniedFullPage from 'components/PermissionDeniedFullPage/PermissionDeniedFullPage';
import permissionsType from 'hooks/useAuthZ/permissions.type';
import {
buildRoleDeletePermission,
buildRoleReadPermission,
buildRoleUpdatePermission,
} from 'hooks/useAuthZ/permissions/role.permissions';
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
import type { AuthzResources } from '../utils';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
@@ -23,7 +30,6 @@ import { useErrorModal } from 'providers/ErrorModalProvider';
import { RoleType } from 'types/roles';
import { handleApiError, toAPIError } from 'utils/errorUtils';
import { IS_ROLE_DETAILS_AND_CRUD_ENABLED } from '../config';
import type { PermissionConfig } from '../PermissionSidePanel';
import PermissionSidePanel from '../PermissionSidePanel';
import CreateRoleModal from '../RolesComponents/CreateRoleModal';
@@ -34,35 +40,33 @@ import {
deriveResourcesForRelation,
objectsToPermissionConfig,
} from '../utils';
import MembersTab from './components/MembersTab';
import OverviewTab from './components/OverviewTab';
import { ROLE_ID_REGEX } from './constants';
import './RoleDetailsPage.styles.scss';
type TabKey = 'overview' | 'members';
// eslint-disable-next-line sonarjs/cognitive-complexity
function RoleDetailsPage(): JSX.Element {
const { pathname } = useLocation();
const { pathname, search } = useLocation();
const history = useHistory();
useEffect(() => {
if (!IS_ROLE_DETAILS_AND_CRUD_ENABLED) {
history.push(ROUTES.ROLES_SETTINGS);
}
}, [history]);
const queryClient = useQueryClient();
const { showErrorModal } = useErrorModal();
const authzResources = permissionsType.data as unknown as AuthzResources;
// Extract channelId from URL pathname since useParams doesn't work in nested routing
// Extract roleId from URL pathname since useParams doesn't work in nested routing
const roleIdMatch = pathname.match(ROLE_ID_REGEX);
const roleId = roleIdMatch ? roleIdMatch[1] : '';
const [activeTab, setActiveTab] = useState<TabKey>('overview');
// Role name passed as query param by the listing page — used to check read permission
// before the role details API resolves. Absent when navigating directly (e.g. deep link),
// in which case we skip the FGA check and fall back to the BE guard.
const nameFromQuery = useMemo(
() => new URLSearchParams(search).get('name') ?? '',
[search],
);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [activePermission, setActivePermission] = useState<string | null>(null);
@@ -75,6 +79,27 @@ function RoleDetailsPage(): JSX.Element {
const isTransitioning = isFetching && role?.id !== roleId;
const isManaged = role?.type === RoleType.MANAGED;
const roleName = role?.name ?? '';
// Read check — fires immediately using the name query param so we can gate the page
// before the role details API resolves. Skipped when name is absent.
const { permissions: readPerms, isLoading: isReadAuthZLoading } = useAuthZ(
nameFromQuery ? [buildRoleReadPermission(nameFromQuery)] : [],
{ enabled: !!nameFromQuery },
);
const hasReadPermission = nameFromQuery
? (readPerms?.[buildRoleReadPermission(nameFromQuery)]?.isGranted ?? true)
: true;
// Update check uses role name once loaded
const { permissions: updatePerms, isLoading: isAuthZLoading } = useAuthZ(
roleName && !isManaged ? [buildRoleUpdatePermission(roleName)] : [],
{ enabled: !!roleName && !isManaged },
);
const hasUpdatePermission = isAuthZLoading
? false
: (updatePerms?.[buildRoleUpdatePermission(roleName)]?.isGranted ?? false);
const permissionTypes = useMemo(
() => derivePermissionTypes(authzResources?.relations ?? null),
[authzResources],
@@ -90,7 +115,11 @@ function RoleDetailsPage(): JSX.Element {
const { data: objectsData, isLoading: isLoadingObjects } = useGetObjects(
{ id: roleId, relation: activePermission ?? '' },
{ query: { enabled: !!activePermission && !!roleId && !isManaged } },
{
query: {
enabled: !!activePermission && !!roleId && !isManaged,
},
},
);
const initialConfig = useMemo(() => {
@@ -110,7 +139,6 @@ function RoleDetailsPage(): JSX.Element {
getGetObjectsQueryKey({ id: roleId, relation: activePermission }),
);
}
setActivePermission(null);
};
const { mutate: patchObjects, isLoading: isSaving } = usePatchObjects({
@@ -130,7 +158,11 @@ function RoleDetailsPage(): JSX.Element {
},
});
if (!IS_ROLE_DETAILS_AND_CRUD_ENABLED || isLoading || isTransitioning) {
if (!hasReadPermission && readPerms !== null) {
return <PermissionDeniedFullPage permissionName="role:read" />;
}
if (isLoading || isTransitioning || (!!nameFromQuery && isReadAuthZLoading)) {
return (
<div className="role-details-page">
<Skeleton
@@ -186,73 +218,49 @@ function RoleDetailsPage(): JSX.Element {
<div className="role-details-page">
<div className="role-details-header">
<h2 className="role-details-title">Role {role.name}</h2>
</div>
<div className="role-details-nav">
<ToggleGroup
type="single"
value={activeTab}
onChange={(val): void => {
if (val) {
setActiveTab(val as TabKey);
}
}}
className="role-details-tabs"
>
<ToggleGroupItem value="overview" className="role-details-tab">
<Table2 size={14} />
Overview
</ToggleGroupItem>
<ToggleGroupItem value="members" className="role-details-tab">
<Users size={14} />
Members
<span className="role-details-tab-count">0</span>
</ToggleGroupItem>
</ToggleGroup>
{!isManaged && (
<div className="role-details-actions">
<Button
variant="ghost"
color="destructive"
className="role-details-delete-action-btn"
onClick={(): void => setIsDeleteModalOpen(true)}
aria-label="Delete role"
>
<Trash2 size={14} />
</Button>
<Button
variant="solid"
color="secondary"
size="sm"
onClick={(): void => setIsEditModalOpen(true)}
>
Edit Role Details
</Button>
<AuthZTooltip checks={[buildRoleDeletePermission(role.name)]}>
<Button
variant="link"
color="destructive"
onClick={(): void => setIsDeleteModalOpen(true)}
aria-label="Delete role"
>
<Trash2 size={12} />
</Button>
</AuthZTooltip>
<AuthZTooltip checks={[buildRoleUpdatePermission(role.name)]}>
<Button
variant="solid"
color="secondary"
onClick={(): void => setIsEditModalOpen(true)}
>
Edit Role Details
</Button>
</AuthZTooltip>
</div>
)}
</div>
{activeTab === 'overview' && (
<OverviewTab
role={role || null}
isManaged={isManaged}
permissionTypes={permissionTypes}
onPermissionClick={(key): void => setActivePermission(key)}
/>
)}
{activeTab === 'members' && <MembersTab />}
<OverviewTab
role={role || null}
isManaged={isManaged}
permissionTypes={permissionTypes}
onPermissionClick={(key): void => setActivePermission(key)}
/>
{!isManaged && (
<>
<PermissionSidePanel
open={activePermission !== null}
onClose={(): void => setActivePermission(null)}
permissionLabel={activePermission ? capitalize(activePermission) : ''}
relation={activePermission ?? ''}
resources={resourcesForActivePermission}
initialConfig={initialConfig}
isLoading={isLoadingObjects}
isSaving={isSaving}
canEdit={hasUpdatePermission}
onSave={handleSave}
/>

View File

@@ -1,5 +1,3 @@
jest.mock('../../config', () => ({ IS_ROLE_DETAILS_AND_CRUD_ENABLED: true }));
import * as roleApi from 'api/generated/services/role';
import {
customRoleResponse,
@@ -15,9 +13,16 @@ import {
waitFor,
within,
} from 'tests/test-utils';
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
import {
mockUseAuthZDenyAll,
mockUseAuthZGrantAll,
} from 'tests/authz-test-utils';
import RoleDetailsPage from '../RoleDetailsPage';
jest.mock('hooks/useAuthZ/useAuthZ');
const mockUseAuthZ = useAuthZ as jest.MockedFunction<typeof useAuthZ>;
const CUSTOM_ROLE_ID = '019c24aa-3333-0001-aaaa-111111111111';
const MANAGED_ROLE_ID = '019c24aa-2248-756f-9833-984f1ab63819';
@@ -29,7 +34,7 @@ const allScopeObjectsResponse = {
status: 'success',
data: [
{
resource: { kind: 'role', type: 'metaresources' },
resource: { kind: 'role', type: 'role' },
selectors: ['*'],
},
],
@@ -46,6 +51,10 @@ function setupDefaultHandlers(roleId = CUSTOM_ROLE_ID): void {
);
}
beforeEach(() => {
mockUseAuthZ.mockImplementation(mockUseAuthZGrantAll);
});
afterEach(() => {
jest.clearAllMocks();
server.resetHandlers();
@@ -63,9 +72,6 @@ describe('RoleDetailsPage', () => {
screen.findByText('Role — billing-manager'),
).resolves.toBeInTheDocument();
expect(screen.getByText('Overview')).toBeInTheDocument();
expect(screen.getByText('Members')).toBeInTheDocument();
expect(
screen.getByText('Custom role for managing billing and invoices.'),
).toBeInTheDocument();
@@ -212,6 +218,18 @@ describe('RoleDetailsPage', () => {
);
});
it('shows PermissionDeniedFullPage when read permission is denied via query param', async () => {
mockUseAuthZ.mockImplementation(mockUseAuthZDenyAll);
render(<RoleDetailsPage />, undefined, {
initialRoute: `/settings/roles/${CUSTOM_ROLE_ID}?name=billing-manager`,
});
await expect(
screen.findByText(/you don't have permission to view this page/i),
).resolves.toBeInTheDocument();
});
describe('permission side panel', () => {
beforeEach(() => {
// Both hooks mocked so data renders synchronously — no React Query scheduler or MSW round-trip.
@@ -238,7 +256,18 @@ describe('RoleDetailsPage', () => {
const panel = document.querySelector(
'.permission-side-panel',
) as HTMLElement;
await within(panel).findByRole('button', { name: 'Role' });
await within(panel).findByRole('button', { name: 'role' });
return panel;
}
async function openReadPanel(): Promise<HTMLElement> {
await screen.findByText('Role — billing-manager');
fireEvent.click(screen.getByText('Read'));
await screen.findByText('Edit Read Permissions');
const panel = document.querySelector(
'.permission-side-panel',
) as HTMLElement;
await within(panel).findByRole('button', { name: 'role' });
return panel;
}
@@ -253,7 +282,7 @@ describe('RoleDetailsPage', () => {
within(panel).getByRole('button', { name: /save changes/i }),
).toBeDisabled();
fireEvent.click(within(panel).getByRole('button', { name: 'Role' }));
fireEvent.click(within(panel).getByRole('button', { name: 'role' }));
fireEvent.click(screen.getByText('All'));
expect(
@@ -281,7 +310,7 @@ describe('RoleDetailsPage', () => {
const panel = await openCreatePanel();
fireEvent.click(within(panel).getByRole('button', { name: 'Role' }));
fireEvent.click(within(panel).getByRole('button', { name: 'role' }));
fireEvent.click(screen.getByText('All'));
fireEvent.click(
within(panel).getByRole('button', { name: /save changes/i }),
@@ -317,9 +346,11 @@ describe('RoleDetailsPage', () => {
initialRoute: `/settings/roles/${CUSTOM_ROLE_ID}`,
});
const panel = await openCreatePanel();
const panel = await openReadPanel();
fireEvent.click(within(panel).getByRole('button', { name: 'Role' }));
fireEvent.click(within(panel).getByRole('button', { name: 'role' }));
// Default is NONE, so switch to Only selected first to reveal the combobox
fireEvent.click(screen.getByText('Only selected'));
const combobox = within(panel).getByRole('combobox');
fireEvent.change(combobox, { target: { value: 'role-001' } });
@@ -342,6 +373,48 @@ describe('RoleDetailsPage', () => {
);
});
it('set scope to None on create panel (existing All) → patchObjects deletions: ["*"], additions: null', async () => {
const patchSpy = jest.fn();
jest.spyOn(roleApi, 'useGetObjects').mockReturnValue({
data: allScopeObjectsResponse,
isLoading: false,
} as any);
server.use(
rest.patch(
`${rolesApiBase}/:id/relations/:relation/objects`,
async (req, res, ctx) => {
patchSpy(await req.json());
return res(ctx.status(200), ctx.json({ status: 'success', data: null }));
},
),
);
render(<RoleDetailsPage />, undefined, {
initialRoute: `/settings/roles/${CUSTOM_ROLE_ID}`,
});
const panel = await openCreatePanel();
fireEvent.click(within(panel).getByRole('button', { name: 'role' }));
fireEvent.click(screen.getByText('None'));
fireEvent.click(
within(panel).getByRole('button', { name: /save changes/i }),
);
await waitFor(() =>
expect(patchSpy).toHaveBeenCalledWith({
additions: null,
deletions: [
{
resource: { kind: 'role', type: 'role' },
selectors: ['*'],
},
],
}),
);
});
it('existing All scope changed to Only selected (empty) → patchObjects deletions: ["*"], additions: null', async () => {
const patchSpy = jest.fn();
@@ -363,9 +436,9 @@ describe('RoleDetailsPage', () => {
initialRoute: `/settings/roles/${CUSTOM_ROLE_ID}`,
});
const panel = await openCreatePanel();
const panel = await openReadPanel();
fireEvent.click(within(panel).getByRole('button', { name: 'Role' }));
fireEvent.click(within(panel).getByRole('button', { name: 'role' }));
fireEvent.click(screen.getByText('Only selected'));
fireEvent.click(
within(panel).getByRole('button', { name: /save changes/i }),
@@ -393,7 +466,7 @@ describe('RoleDetailsPage', () => {
expect(screen.queryByText(/unsaved change/)).not.toBeInTheDocument();
fireEvent.click(within(panel).getByRole('button', { name: 'Role' }));
fireEvent.click(within(panel).getByRole('button', { name: 'role' }));
fireEvent.click(screen.getByText('All'));
expect(screen.getByText('1 unsaved change')).toBeInTheDocument();

View File

@@ -2,6 +2,7 @@ import { Callout } from '@signozhq/ui/callout';
import { PermissionType, TimestampBadge } from '../../utils';
import PermissionItem from './PermissionItem';
import { AuthtypesRelationDTO } from 'api/generated/services/sigNoz.schemas';
interface OverviewTabProps {
role: {
@@ -55,18 +56,28 @@ function OverviewTab({
<div className="role-details-permissions">
<div className="role-details-permissions-header">
<span className="role-details-section-label">Permissions</span>
<a
href="https://signoz.io/docs/manage/administrator-guide/iam/permissions/"
target="_blank"
rel="noopener noreferrer"
className="role-details-permissions-learn-more"
>
Learn more
</a>
<hr className="role-details-permissions-divider" />
</div>
<div className="role-details-permission-list">
{permissionTypes.map((permissionType) => (
<PermissionItem
key={permissionType.key}
permissionType={permissionType}
isManaged={isManaged}
onPermissionClick={onPermissionClick}
/>
))}
{permissionTypes
.filter((p) => p.key !== AuthtypesRelationDTO.assignee)
.map((permissionType) => (
<PermissionItem
key={permissionType.key}
permissionType={permissionType}
isManaged={isManaged}
onPermissionClick={onPermissionClick}
/>
))}
</div>
</div>
</div>

View File

@@ -27,9 +27,8 @@ function DeleteRoleModal({
<Button
key="cancel"
className="cancel-btn"
prefix={<X size={16} />}
prefix={<X size={14} />}
onClick={onCancel}
size="sm"
variant="solid"
color="secondary"
>
@@ -38,10 +37,9 @@ function DeleteRoleModal({
<Button
key="delete"
className="delete-btn"
prefix={<Trash2 size={16} />}
prefix={<Trash2 size={14} />}
onClick={onConfirm}
loading={isDeleting}
size="sm"
variant="solid"
color="destructive"
>

View File

@@ -4,16 +4,17 @@ import { Pagination, Skeleton } from 'antd';
import { useListRoles } from 'api/generated/services/role';
import { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import PermissionDeniedFullPage from 'components/PermissionDeniedFullPage/PermissionDeniedFullPage';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import ROUTES from 'constants/routes';
import { RoleListPermission } from 'hooks/useAuthZ/permissions/role.permissions';
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
import useUrlQuery from 'hooks/useUrlQuery';
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
import { useTimezone } from 'providers/Timezone';
import { RoleType } from 'types/roles';
import { toAPIError } from 'utils/errorUtils';
import { IS_ROLE_DETAILS_AND_CRUD_ENABLED } from '../config';
import '../RolesSettings.styles.scss';
const PAGE_SIZE = 20;
@@ -29,7 +30,14 @@ interface RolesListingTableProps {
function RolesListingTable({
searchQuery,
}: RolesListingTableProps): JSX.Element {
const { data, isLoading, isError, error } = useListRoles();
const { permissions: listPerms, isLoading: isAuthZLoading } = useAuthZ([
RoleListPermission,
]);
const hasListPermission = listPerms?.[RoleListPermission]?.isGranted ?? false;
const { data, isLoading, isError, error } = useListRoles({
query: { enabled: hasListPermission },
});
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const history = useHistory();
const urlQuery = useUrlQuery();
@@ -151,7 +159,11 @@ function RolesListingTable({
</>
);
if (isLoading) {
if (!hasListPermission && listPerms !== null) {
return <PermissionDeniedFullPage permissionName="role:list" />;
}
if (isAuthZLoading || isLoading) {
return (
<div className="roles-listing-table">
<Skeleton active paragraph={{ rows: 5 }} />
@@ -182,31 +194,26 @@ function RolesListingTable({
);
}
const navigateToRole = (roleId: string): void => {
history.push(ROUTES.ROLE_DETAILS.replace(':roleId', roleId));
const navigateToRole = (roleId: string, roleName?: string): void => {
const search = roleName ? `?name=${encodeURIComponent(roleName)}` : '';
history.push(`${ROUTES.ROLE_DETAILS.replace(':roleId', roleId)}${search}`);
};
// todo: use table from periscope when its available for consumption
const renderRow = (role: AuthtypesRoleDTO): JSX.Element => (
<div
key={role.id}
className={`roles-table-row ${
IS_ROLE_DETAILS_AND_CRUD_ENABLED ? 'roles-table-row--clickable' : ''
}`}
className="roles-table-row roles-table-row--clickable"
role="button"
tabIndex={IS_ROLE_DETAILS_AND_CRUD_ENABLED ? 0 : -1}
tabIndex={0}
onClick={(): void => {
if (IS_ROLE_DETAILS_AND_CRUD_ENABLED && role.id) {
navigateToRole(role.id);
if (role.id) {
navigateToRole(role.id, role.name);
}
}}
onKeyDown={(e): void => {
if (
IS_ROLE_DETAILS_AND_CRUD_ENABLED &&
(e.key === 'Enter' || e.key === ' ') &&
role.id
) {
navigateToRole(role.id);
if ((e.key === 'Enter' || e.key === ' ') && role.id) {
navigateToRole(role.id, role.name);
}
}}
>

View File

@@ -22,12 +22,21 @@
color: var(--foreground);
font-family: Inter;
font-style: normal;
font-size: 14px;
font-weight: 400;
font-size: var(--paragraph-base-400-font-size);
font-weight: var(--paragraph-base-400-font-weight);
line-height: 20px;
letter-spacing: -0.07px;
margin: 0;
}
.roles-settings-header-learn-more {
color: var(--primary);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
.roles-settings-content {
@@ -285,16 +294,23 @@
}
}
// todo: https://github.com/SigNoz/components/issues/116
input,
input {
&::placeholder {
opacity: 0.4;
}
}
textarea {
width: 100%;
background: var(--l3-background);
border: 1px solid var(--l1-border);
box-sizing: border-box;
min-height: 100px;
resize: vertical;
background: var(--input-background, transparent);
border: 1px solid var(--border);
border-radius: 2px;
padding: 6px 8px;
font-family: Inter;
font-size: 14px;
font-size: var(--font-size-xs);
font-weight: 400;
line-height: 18px;
letter-spacing: -0.07px;
@@ -303,7 +319,7 @@
box-shadow: none;
&::placeholder {
color: var(--l3-foreground);
color: var(--muted-foreground);
opacity: 0.4;
}
@@ -313,25 +329,6 @@
box-shadow: none;
}
}
input {
height: 32px;
}
input:disabled {
opacity: 0.5;
cursor: not-allowed;
&:hover {
border-color: var(--l1-border);
box-shadow: none;
}
}
textarea {
min-height: 100px;
resize: vertical;
}
}
.ant-modal-footer {

View File

@@ -2,8 +2,9 @@ import { useState } from 'react';
import { Plus } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
import { RoleCreatePermission } from 'hooks/useAuthZ/permissions/role.permissions';
import { IS_ROLE_DETAILS_AND_CRUD_ENABLED } from './config';
import CreateRoleModal from './RolesComponents/CreateRoleModal';
import RolesListingTable from './RolesComponents/RolesListingTable';
@@ -18,7 +19,15 @@ function RolesSettings(): JSX.Element {
<div className="roles-settings-header">
<h3 className="roles-settings-header-title">Roles</h3>
<p className="roles-settings-header-description">
Create and manage custom roles for your team.
Create and manage custom roles for your team.{' '}
<a
href="https://signoz.io/docs/manage/administrator-guide/iam/roles/"
target="_blank"
rel="noopener noreferrer"
className="roles-settings-header-learn-more"
>
Learn more
</a>
</p>
</div>
<div className="roles-settings-content">
@@ -29,7 +38,7 @@ function RolesSettings(): JSX.Element {
value={searchQuery}
onChange={(e): void => setSearchQuery(e.target.value)}
/>
{IS_ROLE_DETAILS_AND_CRUD_ENABLED && (
<AuthZTooltip checks={[RoleCreatePermission]}>
<Button
variant="solid"
color="primary"
@@ -39,7 +48,7 @@ function RolesSettings(): JSX.Element {
<Plus size={14} />
Custom role
</Button>
)}
</AuthZTooltip>
</div>
<RolesListingTable searchQuery={searchQuery} />
</div>

View File

@@ -5,13 +5,19 @@ import {
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { fireEvent, render, screen } from 'tests/test-utils';
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
import { mockUseAuthZGrantAll } from 'tests/authz-test-utils';
import RolesSettings from '../RolesSettings';
jest.mock('hooks/useAuthZ/useAuthZ');
const mockUseAuthZ = useAuthZ as jest.MockedFunction<typeof useAuthZ>;
const rolesApiURL = 'http://localhost/api/v1/roles';
describe('RolesSettings', () => {
beforeEach(() => {
mockUseAuthZ.mockImplementation(mockUseAuthZGrantAll);
server.use(
rest.get(rolesApiURL, (_req, res, ctx) =>
res(ctx.status(200), ctx.json(listRolesSuccessResponse)),

View File

@@ -58,8 +58,18 @@ const baseAuthzResources: AuthzResources = {
};
const resourceDefs: ResourceDefinition[] = [
{ id: 'dashboard', label: 'Dashboard' },
{ id: 'alert', label: 'Alert' },
{
id: 'metaresource:dashboard',
kind: 'dashboard',
type: 'metaresource',
label: 'Dashboard',
},
{
id: 'metaresource:alert',
kind: 'alert',
type: 'metaresource',
label: 'Alert',
},
];
const ID_A = 'aaaaaaaa-0000-0000-0000-000000000001';
@@ -69,15 +79,24 @@ const ID_C = 'cccccccc-0000-0000-0000-000000000003';
describe('buildPatchPayload', () => {
it('sends only the added selector as an addition', () => {
const initial: PermissionConfig = {
dashboard: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [ID_A] },
alert: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [] },
'metaresource:dashboard': {
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [ID_A],
},
'metaresource:alert': {
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [],
},
};
const newConfig: PermissionConfig = {
dashboard: {
'metaresource:dashboard': {
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [ID_A, ID_B],
},
alert: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [] },
'metaresource:alert': {
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [],
},
};
const result = buildPatchPayload({
@@ -95,18 +114,24 @@ describe('buildPatchPayload', () => {
it('sends only the removed selector as a deletion', () => {
const initial: PermissionConfig = {
dashboard: {
'metaresource:dashboard': {
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [ID_A, ID_B, ID_C],
},
alert: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [] },
'metaresource:alert': {
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [],
},
};
const newConfig: PermissionConfig = {
dashboard: {
'metaresource:dashboard': {
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [ID_A, ID_C],
},
alert: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [] },
'metaresource:alert': {
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [],
},
};
const result = buildPatchPayload({
@@ -124,18 +149,24 @@ describe('buildPatchPayload', () => {
it('treats selector order as irrelevant — produces no payload when IDs are identical', () => {
const initial: PermissionConfig = {
dashboard: {
'metaresource:dashboard': {
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [ID_A, ID_B],
},
alert: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [] },
'metaresource:alert': {
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [],
},
};
const newConfig: PermissionConfig = {
dashboard: {
'metaresource:dashboard': {
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [ID_B, ID_A],
},
alert: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [] },
'metaresource:alert': {
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [],
},
};
const result = buildPatchPayload({
@@ -151,15 +182,21 @@ describe('buildPatchPayload', () => {
it('replaces wildcard with specific IDs when switching all → only_selected', () => {
const initial: PermissionConfig = {
dashboard: { scope: PermissionScope.ALL, selectedIds: [] },
alert: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [] },
'metaresource:dashboard': { scope: PermissionScope.ALL, selectedIds: [] },
'metaresource:alert': {
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [],
},
};
const newConfig: PermissionConfig = {
dashboard: {
'metaresource:dashboard': {
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [ID_A, ID_B],
},
alert: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [] },
'metaresource:alert': {
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [],
},
};
const result = buildPatchPayload({
@@ -179,12 +216,21 @@ describe('buildPatchPayload', () => {
it('only deletes wildcard when switching all → only_selected with empty selector list', () => {
const initial: PermissionConfig = {
dashboard: { scope: PermissionScope.ALL, selectedIds: [] },
alert: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [] },
'metaresource:dashboard': { scope: PermissionScope.ALL, selectedIds: [] },
'metaresource:alert': {
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [],
},
};
const newConfig: PermissionConfig = {
dashboard: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [] },
alert: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [] },
'metaresource:dashboard': {
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [],
},
'metaresource:alert': {
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [],
},
};
const result = buildPatchPayload({
@@ -200,14 +246,135 @@ describe('buildPatchPayload', () => {
expect(result.additions).toBeNull();
});
it('only includes resources that actually changed', () => {
it('ALL → NONE: deletes wildcard, no additions', () => {
const initial: PermissionConfig = {
dashboard: { scope: PermissionScope.ALL, selectedIds: [] },
alert: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [ID_A] },
'metaresource:dashboard': { scope: PermissionScope.ALL, selectedIds: [] },
'metaresource:alert': { scope: PermissionScope.NONE, selectedIds: [] },
};
const newConfig: PermissionConfig = {
dashboard: { scope: PermissionScope.ALL, selectedIds: [] }, // unchanged
alert: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [ID_A, ID_B] }, // added ID_B
'metaresource:dashboard': { scope: PermissionScope.NONE, selectedIds: [] },
'metaresource:alert': { scope: PermissionScope.NONE, selectedIds: [] },
};
const result = buildPatchPayload({
newConfig,
initialConfig: initial,
resources: resourceDefs,
authzRes: baseAuthzResources,
});
expect(result.deletions).toStrictEqual([
{ resource: dashboardResource, selectors: ['*'] },
]);
expect(result.additions).toBeNull();
});
it('NONE → ALL: adds wildcard, no deletions', () => {
const initial: PermissionConfig = {
'metaresource:dashboard': { scope: PermissionScope.NONE, selectedIds: [] },
'metaresource:alert': { scope: PermissionScope.NONE, selectedIds: [] },
};
const newConfig: PermissionConfig = {
'metaresource:dashboard': { scope: PermissionScope.ALL, selectedIds: [] },
'metaresource:alert': { scope: PermissionScope.NONE, selectedIds: [] },
};
const result = buildPatchPayload({
newConfig,
initialConfig: initial,
resources: resourceDefs,
authzRes: baseAuthzResources,
});
expect(result.additions).toStrictEqual([
{ resource: dashboardResource, selectors: ['*'] },
]);
expect(result.deletions).toBeNull();
});
it('ONLY_SELECTED → NONE: deletes selected IDs, no additions', () => {
const initial: PermissionConfig = {
'metaresource:dashboard': {
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [ID_A, ID_B],
},
'metaresource:alert': { scope: PermissionScope.NONE, selectedIds: [] },
};
const newConfig: PermissionConfig = {
'metaresource:dashboard': { scope: PermissionScope.NONE, selectedIds: [] },
'metaresource:alert': { scope: PermissionScope.NONE, selectedIds: [] },
};
const result = buildPatchPayload({
newConfig,
initialConfig: initial,
resources: resourceDefs,
authzRes: baseAuthzResources,
});
expect(result.deletions).toStrictEqual([
{ resource: dashboardResource, selectors: [ID_A, ID_B] },
]);
expect(result.additions).toBeNull();
});
it('NONE → ONLY_SELECTED with IDs: adds those IDs, no deletions', () => {
const initial: PermissionConfig = {
'metaresource:dashboard': { scope: PermissionScope.NONE, selectedIds: [] },
'metaresource:alert': { scope: PermissionScope.NONE, selectedIds: [] },
};
const newConfig: PermissionConfig = {
'metaresource:dashboard': {
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [ID_A],
},
'metaresource:alert': { scope: PermissionScope.NONE, selectedIds: [] },
};
const result = buildPatchPayload({
newConfig,
initialConfig: initial,
resources: resourceDefs,
authzRes: baseAuthzResources,
});
expect(result.additions).toStrictEqual([
{ resource: dashboardResource, selectors: [ID_A] },
]);
expect(result.deletions).toBeNull();
});
it('NONE → NONE: no change, produces empty payload', () => {
const initial: PermissionConfig = {
'metaresource:dashboard': { scope: PermissionScope.NONE, selectedIds: [] },
'metaresource:alert': { scope: PermissionScope.NONE, selectedIds: [] },
};
const result = buildPatchPayload({
newConfig: { ...initial },
initialConfig: initial,
resources: resourceDefs,
authzRes: baseAuthzResources,
});
expect(result.additions).toBeNull();
expect(result.deletions).toBeNull();
});
it('only includes resources that actually changed', () => {
const initial: PermissionConfig = {
'metaresource:dashboard': { scope: PermissionScope.ALL, selectedIds: [] },
'metaresource:alert': {
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [ID_A],
},
};
const newConfig: PermissionConfig = {
'metaresource:dashboard': { scope: PermissionScope.ALL, selectedIds: [] }, // unchanged
'metaresource:alert': {
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [ID_A, ID_B],
}, // added ID_B
};
const result = buildPatchPayload({
@@ -232,7 +399,7 @@ describe('objectsToPermissionConfig', () => {
const result = objectsToPermissionConfig(objects, resourceDefs);
expect(result.dashboard).toStrictEqual({
expect(result['metaresource:dashboard']).toStrictEqual({
scope: PermissionScope.ALL,
selectedIds: [],
});
@@ -245,21 +412,21 @@ describe('objectsToPermissionConfig', () => {
const result = objectsToPermissionConfig(objects, resourceDefs);
expect(result.dashboard).toStrictEqual({
expect(result['metaresource:dashboard']).toStrictEqual({
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [ID_A, ID_B],
});
});
it('defaults to ONLY_SELECTED with empty selectedIds when resource is absent from API response', () => {
it('defaults to NONE scope when resource is absent from API response', () => {
const result = objectsToPermissionConfig([], resourceDefs);
expect(result.dashboard).toStrictEqual({
scope: PermissionScope.ONLY_SELECTED,
expect(result['metaresource:dashboard']).toStrictEqual({
scope: PermissionScope.NONE,
selectedIds: [],
});
expect(result.alert).toStrictEqual({
scope: PermissionScope.ONLY_SELECTED,
expect(result['metaresource:alert']).toStrictEqual({
scope: PermissionScope.NONE,
selectedIds: [],
});
});
@@ -268,8 +435,11 @@ describe('objectsToPermissionConfig', () => {
describe('configsEqual', () => {
it('returns true for identical configs', () => {
const config: PermissionConfig = {
dashboard: { scope: PermissionScope.ALL, selectedIds: [] },
alert: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [ID_A] },
'metaresource:dashboard': { scope: PermissionScope.ALL, selectedIds: [] },
'metaresource:alert': {
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [ID_A],
},
};
expect(configsEqual(config, { ...config })).toBe(true);
@@ -277,22 +447,25 @@ describe('configsEqual', () => {
it('returns false when configs differ', () => {
const a: PermissionConfig = {
dashboard: { scope: PermissionScope.ALL, selectedIds: [] },
'metaresource:dashboard': { scope: PermissionScope.ALL, selectedIds: [] },
};
const b: PermissionConfig = {
dashboard: { scope: PermissionScope.ONLY_SELECTED, selectedIds: [] },
'metaresource:dashboard': {
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [],
},
};
expect(configsEqual(a, b)).toBe(false);
const c: PermissionConfig = {
dashboard: {
'metaresource:dashboard': {
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [ID_C, ID_B],
},
};
const d: PermissionConfig = {
dashboard: {
'metaresource:dashboard': {
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [ID_A, ID_B],
},
@@ -303,13 +476,13 @@ describe('configsEqual', () => {
it('returns true when selectedIds are the same but in different order', () => {
const a: PermissionConfig = {
dashboard: {
'metaresource:dashboard': {
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [ID_A, ID_B],
},
};
const b: PermissionConfig = {
dashboard: {
'metaresource:dashboard': {
scope: PermissionScope.ONLY_SELECTED,
selectedIds: [ID_B, ID_A],
},
@@ -322,23 +495,26 @@ describe('configsEqual', () => {
describe('buildConfig', () => {
it('uses initial values when provided and defaults for resources not in initial', () => {
const initial: PermissionConfig = {
dashboard: { scope: PermissionScope.ALL, selectedIds: [] },
'metaresource:dashboard': { scope: PermissionScope.ALL, selectedIds: [] },
};
const result = buildConfig(resourceDefs, initial);
expect(result.dashboard).toStrictEqual({
expect(result['metaresource:dashboard']).toStrictEqual({
scope: PermissionScope.ALL,
selectedIds: [],
});
expect(result.alert).toStrictEqual(DEFAULT_RESOURCE_CONFIG);
expect(result['metaresource:alert']).toStrictEqual(DEFAULT_RESOURCE_CONFIG);
});
it('applies DEFAULT_RESOURCE_CONFIG to all resources when no initial is provided', () => {
it('applies DEFAULT_RESOURCE_CONFIG (NONE scope) to all resources when no initial is provided', () => {
const result = buildConfig(resourceDefs);
expect(result.dashboard).toStrictEqual(DEFAULT_RESOURCE_CONFIG);
expect(result.alert).toStrictEqual(DEFAULT_RESOURCE_CONFIG);
expect(result['metaresource:dashboard']).toStrictEqual(
DEFAULT_RESOURCE_CONFIG,
);
expect(result['metaresource:alert']).toStrictEqual(DEFAULT_RESOURCE_CONFIG);
expect(DEFAULT_RESOURCE_CONFIG.scope).toBe(PermissionScope.NONE);
});
});
@@ -375,7 +551,10 @@ describe('deriveResourcesForRelation', () => {
const result = deriveResourcesForRelation(baseAuthzResources, 'create');
expect(result).toHaveLength(2);
expect(result.map((r) => r.id)).toStrictEqual(['dashboard', 'alert']);
expect(result.map((r) => r.id)).toStrictEqual([
'metaresource:dashboard',
'metaresource:alert',
]);
});
it('returns an empty array when authzResources is null', () => {

View File

@@ -1 +0,0 @@
export const IS_ROLE_DETAILS_AND_CRUD_ENABLED = false;

View File

@@ -12,6 +12,7 @@ import type {
PermissionConfig,
ResourceConfig,
ResourceDefinition,
ScopeType,
} from './PermissionSidePanel/PermissionSidePanel.types';
import { PermissionScope } from './PermissionSidePanel/PermissionSidePanel.types';
import {
@@ -70,8 +71,10 @@ export function deriveResourcesForRelation(
return authzResources.resources
.filter((r) => supportedTypes.includes(r.type))
.map((r) => ({
id: r.kind,
label: capitalize(r.kind).replaceAll('_', ' '),
id: `${r.type}:${r.kind}`,
kind: r.kind,
type: r.type,
label: r.kind,
options: [],
}));
}
@@ -82,10 +85,12 @@ export function objectsToPermissionConfig(
): PermissionConfig {
const config: PermissionConfig = {};
for (const res of resources) {
const obj = objects.find((o) => o.resource.kind === res.id);
const obj = objects.find(
(o) => o.resource.kind === res.kind && o.resource.type === res.type,
);
if (!obj) {
config[res.id] = {
scope: PermissionScope.ONLY_SELECTED,
scope: PermissionScope.NONE,
selectedIds: [],
};
} else {
@@ -99,6 +104,16 @@ export function objectsToPermissionConfig(
return config;
}
function selectorsForScope(scope: ScopeType, selectedIds: string[]): string[] {
if (scope === PermissionScope.ALL) {
return ['*'];
}
if (scope === PermissionScope.ONLY_SELECTED) {
return selectedIds;
}
return []; // NONE
}
// eslint-disable-next-line sonarjs/cognitive-complexity
export function buildPatchPayload({
newConfig,
@@ -118,7 +133,9 @@ export function buildPatchPayload({
for (const res of resources) {
const initial = initialConfig[res.id];
const current = newConfig[res.id];
const found = authzRes.resources.find((r) => r.kind === res.id);
const found = authzRes.resources.find(
(r) => r.kind === res.kind && r.type === res.type,
);
if (!found) {
continue;
}
@@ -127,8 +144,8 @@ export function buildPatchPayload({
type: found.type,
};
const initialScope = initial?.scope ?? PermissionScope.ONLY_SELECTED;
const currentScope = current?.scope ?? PermissionScope.ONLY_SELECTED;
const initialScope = initial?.scope ?? PermissionScope.NONE;
const currentScope = current?.scope ?? PermissionScope.NONE;
if (initialScope === currentScope) {
// Same scope — only diff individual selectors when both are ONLY_SELECTED
@@ -144,16 +161,20 @@ export function buildPatchPayload({
additions.push({ resource: resourceDef, selectors: added });
}
}
// Both ALL → no change, skip
// Both ALL or both NONE → no change, skip
} else {
// Scope changed (ALL ↔ ONLY_SELECTED) — replace old with new
const initialSelectors =
initialScope === PermissionScope.ALL ? ['*'] : (initial?.selectedIds ?? []);
// Scope changed — replace old selectors with new ones
const initialSelectors = selectorsForScope(
initialScope,
initial?.selectedIds ?? [],
);
if (initialSelectors.length > 0) {
deletions.push({ resource: resourceDef, selectors: initialSelectors });
}
const currentSelectors =
currentScope === PermissionScope.ALL ? ['*'] : (current?.selectedIds ?? []);
const currentSelectors = selectorsForScope(
currentScope,
current?.selectedIds ?? [],
);
if (currentSelectors.length > 0) {
additions.push({ resource: resourceDef, selectors: currentSelectors });
}
@@ -191,7 +212,7 @@ export function TimestampBadge({ date }: TimestampBadgeProps): JSX.Element {
}
export const DEFAULT_RESOURCE_CONFIG: ResourceConfig = {
scope: PermissionScope.ONLY_SELECTED,
scope: PermissionScope.NONE,
selectedIds: [],
};

View File

@@ -0,0 +1,132 @@
import type { AuthtypesTransactionDTO } from 'api/generated/services/sigNoz.schemas';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { render, screen, waitFor } from 'tests/test-utils';
import { AUTHZ_CHECK_URL, authzMockResponse } from 'tests/authz-test-utils';
import ServiceAccountsSettings from './ServiceAccountsSettings';
const SA_LIST_URL = 'http://localhost/api/v1/service_accounts';
function renderPage(): ReturnType<typeof render> {
return render(
<NuqsTestingAdapter searchParams={{}} hasMemory>
<ServiceAccountsSettings />
</NuqsTestingAdapter>,
);
}
describe('ServiceAccountsSettings — FGA', () => {
beforeEach(() => {
server.use(
rest.get(SA_LIST_URL, (_req, res, ctx) =>
res(ctx.status(200), ctx.json({ data: [] })),
),
);
});
it('shows PermissionDeniedFullPage when list permission is denied', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(
ctx.status(200),
ctx.json(
authzMockResponse(
payload,
payload.map(() => false),
),
),
);
}),
);
renderPage();
await waitFor(() => {
expect(
screen.getByText(/You don't have permission to view this page/),
).toBeInTheDocument();
});
expect(screen.queryByRole('table')).not.toBeInTheDocument();
});
it('shows table when list permission is granted', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(
ctx.status(200),
ctx.json(
authzMockResponse(
payload,
payload.map(() => true),
),
),
);
}),
);
renderPage();
await waitFor(() => {
expect(screen.getByRole('table')).toBeInTheDocument();
});
expect(
screen.queryByText(/You don't have permission to view this page/),
).not.toBeInTheDocument();
});
it('disables New Service Account button when create permission is denied', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
// grant list, deny create — matched by relation name
return res(
ctx.status(200),
ctx.json(
authzMockResponse(
payload,
payload.map((txn: AuthtypesTransactionDTO) => txn.relation === 'list'),
),
),
);
}),
);
renderPage();
await waitFor(() => {
expect(
screen.getByRole('button', { name: /New Service Account/i }),
).toBeDisabled();
});
});
it('enables New Service Account button when create permission is granted', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(
ctx.status(200),
ctx.json(
authzMockResponse(
payload,
payload.map(() => true),
),
),
);
}),
);
renderPage();
await waitFor(() => {
expect(
screen.getByRole('button', { name: /New Service Account/i }),
).not.toBeDisabled();
});
});
});

View File

@@ -5,12 +5,20 @@ import { Input } from '@signozhq/ui/input';
import type { MenuProps } from 'antd';
import { Dropdown } from 'antd';
import { useListServiceAccounts } from 'api/generated/services/serviceaccount';
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
import CreateServiceAccountModal from 'components/CreateServiceAccountModal/CreateServiceAccountModal';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import PermissionDeniedFullPage from 'components/PermissionDeniedFullPage/PermissionDeniedFullPage';
import Spinner from 'components/Spinner';
import ServiceAccountDrawer from 'components/ServiceAccountDrawer/ServiceAccountDrawer';
import ServiceAccountsTable, {
PAGE_SIZE,
} from 'components/ServiceAccountsTable/ServiceAccountsTable';
import {
SACreatePermission,
SAListPermission,
} from 'hooks/useAuthZ/permissions/service-account.permissions';
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
import {
parseAsBoolean,
parseAsInteger,
@@ -51,13 +59,19 @@ function ServiceAccountsSettings(): JSX.Element {
parseAsBoolean.withDefault(false),
);
const { permissions: listPerms, isLoading: isAuthZLoading } = useAuthZ([
SAListPermission,
]);
const hasListPermission = listPerms?.[SAListPermission]?.isGranted ?? false;
const {
data: serviceAccountsData,
isLoading,
isError,
error,
refetch: handleCreateSuccess,
} = useListServiceAccounts();
} = useListServiceAccounts({ query: { enabled: hasListPermission } });
const allAccounts = useMemo(
(): ServiceAccountRow[] =>
@@ -112,9 +126,9 @@ function ServiceAccountsSettings(): JSX.Element {
const maxPage = Math.max(1, Math.ceil(filteredAccounts.length / PAGE_SIZE));
if (currentPage > maxPage) {
setPage(maxPage);
void setPage(maxPage);
} else if (currentPage < 1) {
setPage(1);
void setPage(1);
}
}, [filteredAccounts.length, currentPage, setPage]);
@@ -130,8 +144,8 @@ function ServiceAccountsSettings(): JSX.Element {
</div>
),
onClick: (): void => {
setFilterMode(FilterMode.All);
setPage(1);
void setFilterMode(FilterMode.All);
void setPage(1);
},
},
{
@@ -143,8 +157,8 @@ function ServiceAccountsSettings(): JSX.Element {
</div>
),
onClick: (): void => {
setFilterMode(FilterMode.Active);
setPage(1);
void setFilterMode(FilterMode.Active);
void setPage(1);
},
},
{
@@ -156,8 +170,8 @@ function ServiceAccountsSettings(): JSX.Element {
</div>
),
onClick: (): void => {
setFilterMode(FilterMode.Deleted);
setPage(1);
void setFilterMode(FilterMode.Deleted);
void setPage(1);
},
},
];
@@ -176,7 +190,7 @@ function ServiceAccountsSettings(): JSX.Element {
const handleRowClick = useCallback(
(row: ServiceAccountRow): void => {
setSelectedAccountId(row.id);
void setSelectedAccountId(row.id);
},
[setSelectedAccountId],
);
@@ -184,9 +198,9 @@ function ServiceAccountsSettings(): JSX.Element {
const handleDrawerSuccess = useCallback(
(options?: { closeDrawer?: boolean }): void => {
if (options?.closeDrawer) {
setSelectedAccountId(null);
void setSelectedAccountId(null);
}
handleCreateSuccess();
void handleCreateSuccess();
},
[handleCreateSuccess, setSelectedAccountId],
);
@@ -208,63 +222,76 @@ function ServiceAccountsSettings(): JSX.Element {
</a>
</p>
</div>
<div className="sa-settings__controls">
<Dropdown
menu={{ items: filterMenuItems }}
trigger={['click']}
overlayClassName="sa-settings-filter-dropdown"
>
<Button
variant="solid"
color="secondary"
className="sa-settings-filter-trigger"
>
<span>{filterLabel}</span>
<ChevronDown size={12} className="sa-settings-filter-trigger__chevron" />
</Button>
</Dropdown>
<div className="sa-settings__search">
<Input
type="search"
name="service-accounts-search"
placeholder="Search by name or email..."
value={searchQuery}
onChange={(e): void => {
setSearchQuery(e.target.value);
setPage(1);
}}
className="sa-settings-search-input"
/>
</div>
<Button
variant="solid"
color="primary"
onClick={async (): Promise<void> => {
await setIsCreateModalOpen(true);
}}
>
<Plus size={12} />
New Service Account
</Button>
</div>
</div>
{isError ? (
<ErrorInPlace
error={toAPIError(
error,
'An unexpected error occurred while fetching service accounts.',
)}
/>
{isAuthZLoading || isLoading ? (
<Spinner height="50vh" />
) : !hasListPermission ? (
<PermissionDeniedFullPage permissionName="serviceaccount:list" />
) : (
<ServiceAccountsTable
data={filteredAccounts}
loading={isLoading}
onRowClick={handleRowClick}
/>
<div className="sa-settings__list-section">
<div className="sa-settings__controls">
<Dropdown
menu={{ items: filterMenuItems }}
trigger={['click']}
overlayClassName="sa-settings-filter-dropdown"
>
<Button
variant="solid"
color="secondary"
className="sa-settings-filter-trigger"
>
<span>{filterLabel}</span>
<ChevronDown
size={12}
className="sa-settings-filter-trigger__chevron"
/>
</Button>
</Dropdown>
<div className="sa-settings__search">
<Input
type="search"
name="service-accounts-search"
placeholder="Search by name or email..."
value={searchQuery}
onChange={(e): void => {
void setSearchQuery(e.target.value);
void setPage(1);
}}
className="sa-settings-search-input"
/>
</div>
<AuthZTooltip checks={[SACreatePermission]}>
<Button
variant="solid"
color="primary"
onClick={async (): Promise<void> => {
await setIsCreateModalOpen(true);
}}
>
<Plus size={12} />
New Service Account
</Button>
</AuthZTooltip>
</div>
{isError ? (
<ErrorInPlace
error={toAPIError(
error,
'An unexpected error occurred while fetching service accounts.',
)}
/>
) : (
<ServiceAccountsTable
data={filteredAccounts}
loading={isLoading}
onRowClick={handleRowClick}
/>
)}
</div>
)}
<CreateServiceAccountModal />

View File

@@ -3,12 +3,14 @@ import { listRolesSuccessResponse } from 'mocks-server/__mockdata__/roles';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
import { setupAuthzAdmin } from 'tests/authz-test-utils';
import ServiceAccountsSettings from '../ServiceAccountsSettings';
const SA_LIST_ENDPOINT = '*/api/v1/service_accounts';
const SA_ENDPOINT = '*/api/v1/service_accounts/:id';
const SA_KEYS_ENDPOINT = '*/api/v1/service_accounts/:id/keys';
const SA_ROLES_ENDPOINT = '*/api/v1/service_accounts/:id/roles';
const ROLES_ENDPOINT = '*/api/v1/roles';
jest.mock('@signozhq/ui/drawer', () => ({
@@ -85,6 +87,7 @@ describe('ServiceAccountsSettings (integration)', () => {
beforeEach(() => {
jest.clearAllMocks();
server.use(
setupAuthzAdmin(),
rest.get(SA_LIST_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ data: mockServiceAccountsAPI })),
),
@@ -98,6 +101,9 @@ describe('ServiceAccountsSettings (integration)', () => {
rest.get(SA_KEYS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ data: [] })),
),
rest.get(SA_ROLES_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ data: [] })),
),
rest.get(ROLES_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(listRolesSuccessResponse)),
),
@@ -178,15 +184,17 @@ describe('ServiceAccountsSettings (integration)', () => {
it('saving changes in the drawer refetches the list', async () => {
const listRefetchSpy = jest.fn();
const putSpy = jest.fn();
server.use(
rest.get(SA_LIST_ENDPOINT, (_, res, ctx) => {
listRefetchSpy();
return res(ctx.status(200), ctx.json({ data: mockServiceAccountsAPI }));
}),
rest.put(SA_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
),
rest.put(SA_ENDPOINT, async (req, res, ctx) => {
putSpy(await req.json());
return res(ctx.status(200), ctx.json({ status: 'success', data: {} }));
}),
);
render(
@@ -205,9 +213,17 @@ describe('ServiceAccountsSettings (integration)', () => {
const nameInput = await screen.findByDisplayValue('CI Bot');
fireEvent.change(nameInput, { target: { value: 'CI Bot Updated' } });
await screen.findByDisplayValue('CI Bot Updated');
fireEvent.click(screen.getByRole('button', { name: /Save Changes/i }));
await screen.findByDisplayValue('CI Bot Updated');
// Wait for the PUT to complete with the right payload — confirms save fired
await waitFor(() =>
expect(putSpy).toHaveBeenCalledWith(
expect.objectContaining({ name: 'CI Bot Updated' }),
),
);
await waitFor(() => {
expect(listRefetchSpy).toHaveBeenCalled();
});
@@ -222,6 +238,13 @@ describe('ServiceAccountsSettings (integration)', () => {
await screen.findByText('CI Bot');
// Wait for authz check to resolve before clicking
await waitFor(() =>
expect(
screen.getByRole('button', { name: /New Service Account/i }),
).not.toBeDisabled(),
);
fireEvent.click(screen.getByRole('button', { name: /New Service Account/i }));
await screen.findByRole('dialog', { name: /New Service Account/i });

View File

@@ -374,6 +374,7 @@ export const settingsNavSections: SettingsNavSection[] = [
icon: <Shield size={16} />,
isEnabled: false,
itemKey: 'roles',
isBeta: true,
},
{
key: ROUTES.MEMBERS_SETTINGS,

View File

@@ -67,6 +67,7 @@ export const useAutoComplete = (
query,
setSearchKey,
whereClauseConfig,
isInfraMonitoring,
);
const handleSelect = useCallback(
@@ -142,6 +143,7 @@ export const useAutoComplete = (
result,
isFetching,
whereClauseConfig,
isInfraMonitoring,
);
return {

View File

@@ -27,6 +27,13 @@ export const operatorTypeMapper: Record<string, OperatorType> = {
[OPERATORS['!=']]: 'SINGLE_VALUE',
[OPERATORS.HAS]: 'SINGLE_VALUE',
[OPERATORS.NHAS]: 'SINGLE_VALUE',
// Short-form operators for InfraMonitoring
NIN: 'MULTIPLY_VALUE',
NLIKE: 'SINGLE_VALUE',
NOTILIKE: 'SINGLE_VALUE',
NREGEX: 'SINGLE_VALUE',
NEXISTS: 'NON_VALUE',
NCONTAINS: 'SINGLE_VALUE',
};
export const useOperatorType = (operator: string): OperatorType =>

View File

@@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { INFRA_SHORT_TO_LONG_OPERATOR_MAP } from 'constants/queryBuilder';
import {
checkCommaInValue,
getTagToken,
@@ -25,6 +26,7 @@ export const useOptions = (
result: string[],
isFetching: boolean,
whereClauseConfig?: WhereClauseConfig,
isInfraMonitoring?: boolean,
// eslint-disable-next-line sonarjs/cognitive-complexity
): Option[] => {
const [options, setOptions] = useState<Option[]>([]);
@@ -107,10 +109,16 @@ export const useOptions = (
operator.startsWith(partialOperator?.toUpperCase()),
)
: operators;
const operatorsOptions = filteredOperators?.map((operator) => ({
value: `${partialKey} ${operator} `,
label: `${partialKey} ${operator} `,
}));
const operatorsOptions = filteredOperators?.map((op) => {
const labelOp =
isInfraMonitoring && INFRA_SHORT_TO_LONG_OPERATOR_MAP[op]
? INFRA_SHORT_TO_LONG_OPERATOR_MAP[op]
: op;
return {
value: `${partialKey} ${op} `,
label: `${partialKey} ${labelOp} `,
};
});
if (whereClauseConfig) {
return [
{
@@ -122,7 +130,7 @@ export const useOptions = (
}
return operatorsOptions;
},
[operators, searchValue, whereClauseConfig],
[isInfraMonitoring, operators, searchValue, whereClauseConfig],
);
useEffect(() => {

View File

@@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { INFRA_SHORT_TO_LONG_OPERATOR_MAP } from 'constants/queryBuilder';
import {
getOperatorFromValue,
getTagToken,
@@ -16,19 +17,29 @@ import { WhereClauseConfig } from './useAutoComplete';
/**
* Helper for formatting a TagFilter object into filter item strings
* @param {TagFilter} filters - query filter object to be converted
* @param {boolean} isInfraMonitoring - whether to use long form operator display
* @returns {string[]} An array of formatted conditions. Eg: `["service = web", "severity_text = INFO"]`)
*/
export function queryFilterTags(filter: TagFilter): string[] {
export function queryFilterTags(
filter: TagFilter,
isInfraMonitoring?: boolean,
): string[] {
return (filter?.items || []).map((ele) => {
if (isInNInOperator(getOperatorFromValue(ele.op))) {
const rawOp = getOperatorFromValue(ele.op);
const displayOp =
isInfraMonitoring && INFRA_SHORT_TO_LONG_OPERATOR_MAP[rawOp]
? INFRA_SHORT_TO_LONG_OPERATOR_MAP[rawOp]
: rawOp;
if (isInNInOperator(rawOp)) {
try {
const csvString = unparse([ele.value]);
return `${ele.key?.key} ${getOperatorFromValue(ele.op)} ${csvString}`;
return `${ele.key?.key} ${displayOp} ${csvString}`;
} catch {
return `${ele.key?.key} ${getOperatorFromValue(ele.op)} ${ele.value}`;
return `${ele.key?.key} ${displayOp} ${ele.value}`;
}
}
return `${ele.key?.key} ${getOperatorFromValue(ele.op)} ${ele.value}`;
return `${ele.key?.key} ${displayOp} ${ele.value}`;
});
}
@@ -53,10 +64,15 @@ export const useTag = (
query: IBuilderQuery,
setSearchKey: (value: string) => void,
whereClauseConfig?: WhereClauseConfig,
isInfraMonitoring?: boolean,
): IUseTag => {
const initTagsData = useMemo(
() => queryFilterTags(query?.filters || { items: [], op: 'AND' }),
[query?.filters],
() =>
queryFilterTags(
query?.filters || { items: [], op: 'AND' },
isInfraMonitoring,
),
[query?.filters, isInfraMonitoring],
);
const [tags, setTags] = useState<string[]>(initTagsData);

View File

@@ -31,10 +31,14 @@ interface UseServiceAccountRoleManagerResult {
export function useServiceAccountRoleManager(
accountId: string,
options?: { enabled?: boolean },
): UseServiceAccountRoleManagerResult {
const queryClient = useQueryClient();
const { data, isLoading } = useGetServiceAccountRoles({ id: accountId });
const { data, isLoading } = useGetServiceAccountRoles(
{ id: accountId },
{ query: { enabled: options?.enabled ?? true } },
);
const currentRoles = useMemo<AuthtypesRoleDTO[]>(
() => data?.data ?? [],

View File

@@ -0,0 +1,14 @@
import { buildPermission } from '../utils';
import type { BrandedPermission } from '../types';
// Collection-level — no specific role id needed
export const RoleCreatePermission = buildPermission('create', 'role:*');
export const RoleListPermission = buildPermission('list', 'role:*');
// Resource-level — require a specific role id
export const buildRoleReadPermission = (id: string): BrandedPermission =>
buildPermission('read', `role:${id}`);
export const buildRoleUpdatePermission = (id: string): BrandedPermission =>
buildPermission('update', `role:${id}`);
export const buildRoleDeletePermission = (id: string): BrandedPermission =>
buildPermission('delete', `role:${id}`);

View File

@@ -0,0 +1,38 @@
import { buildPermission } from '../utils';
import type { BrandedPermission } from '../types';
// Collection-level — wildcard selector required for correct response key matching
export const SAListPermission = buildPermission('list', 'serviceaccount:*');
export const SACreatePermission = buildPermission('create', 'serviceaccount:*');
// Resource-level — require a specific SA id
export const buildSAReadPermission = (id: string): BrandedPermission =>
buildPermission('read', `serviceaccount:${id}`);
export const buildSAUpdatePermission = (id: string): BrandedPermission =>
buildPermission('update', `serviceaccount:${id}`);
export const buildSADeletePermission = (id: string): BrandedPermission =>
buildPermission('delete', `serviceaccount:${id}`);
export const buildSAAttachPermission = (id: string): BrandedPermission =>
buildPermission('attach', `serviceaccount:${id}`);
export const buildSADetachPermission = (id: string): BrandedPermission =>
buildPermission('detach', `serviceaccount:${id}`);
// Wildcard role permissions — used alongside SA-level checks for role assign/revoke guards.
// Backend requires both serviceaccount:attach AND role:attach to assign a role to a SA,
// and serviceaccount:detach AND role:detach to remove a role from a SA.
export const RoleAttachWildcardPermission = buildPermission('attach', 'role:*');
export const RoleDetachWildcardPermission = buildPermission('detach', 'role:*');
// API key (factor-api-key) permissions.
// Listing keys: factor-api-key:list.
// Creating a key: factor-api-key:create (wildcard) + serviceaccount:attach.
// Revoking a key: factor-api-key:delete (specific key) + serviceaccount:detach.
export const APIKeyListPermission = buildPermission('list', 'factor-api-key:*');
export const APIKeyCreatePermission = buildPermission(
'create',
'factor-api-key:*',
);
export const buildAPIKeyUpdatePermission = (keyId: string): BrandedPermission =>
buildPermission('update', `factor-api-key:${keyId}`);
export const buildAPIKeyDeletePermission = (keyId: string): BrandedPermission =>
buildPermission('delete', `factor-api-key:${keyId}`);

View File

@@ -1,35 +1,14 @@
import { ReactElement } from 'react';
import { renderHook, waitFor } from '@testing-library/react';
import {
AuthtypesGettableTransactionDTO,
AuthtypesTransactionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { ENVIRONMENT } from 'constants/env';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { AllTheProviders } from 'tests/test-utils';
import { AUTHZ_CHECK_URL, authzMockResponse } from 'tests/authz-test-utils';
import { BrandedPermission } from './types';
import { useAuthZ } from './useAuthZ';
import { buildPermission } from './utils';
const BASE_URL = ENVIRONMENT.baseURL || '';
const AUTHZ_CHECK_URL = `${BASE_URL}/api/v1/authz/check`;
function authzMockResponse(
payload: AuthtypesTransactionDTO[],
authorizedByIndex: boolean[],
): { data: AuthtypesGettableTransactionDTO[]; status: string } {
return {
data: payload.map((txn, i) => ({
relation: txn.relation,
object: txn.object,
authorized: authorizedByIndex[i] ?? false,
})),
status: 'success',
};
}
const wrapper = ({ children }: { children: ReactElement }): ReactElement => (
<AllTheProviders>{children}</AllTheProviders>
);

View File

@@ -72,18 +72,26 @@ function SettingsPage(): JSX.Element {
}
if (isCloudUser) {
// Visible to all authenticated users
updatedItems = updatedItems.map((item) => ({
...item,
isEnabled:
item.key === ROUTES.ROLES_SETTINGS ||
item.key === ROUTES.ROLE_DETAILS ||
item.key === ROUTES.SERVICE_ACCOUNTS_SETTINGS
? true
: item.isEnabled,
}));
if (isAdmin) {
updatedItems = updatedItems.map((item) => ({
...item,
isEnabled:
item.key === ROUTES.BILLING ||
item.key === ROUTES.ROLES_SETTINGS ||
item.key === ROUTES.ROLE_DETAILS ||
item.key === ROUTES.INTEGRATIONS ||
item.key === ROUTES.INGESTION_SETTINGS ||
item.key === ROUTES.ORG_SETTINGS ||
item.key === ROUTES.MEMBERS_SETTINGS ||
item.key === ROUTES.SERVICE_ACCOUNTS_SETTINGS ||
item.key === ROUTES.SHORTCUTS ||
item.key === ROUTES.MCP_SERVER
? true
@@ -113,17 +121,25 @@ function SettingsPage(): JSX.Element {
}
if (isEnterpriseSelfHostedUser) {
// Visible to all authenticated users
updatedItems = updatedItems.map((item) => ({
...item,
isEnabled:
item.key === ROUTES.ROLES_SETTINGS ||
item.key === ROUTES.ROLE_DETAILS ||
item.key === ROUTES.SERVICE_ACCOUNTS_SETTINGS
? true
: item.isEnabled,
}));
if (isAdmin) {
updatedItems = updatedItems.map((item) => ({
...item,
isEnabled:
item.key === ROUTES.BILLING ||
item.key === ROUTES.ROLES_SETTINGS ||
item.key === ROUTES.ROLE_DETAILS ||
item.key === ROUTES.INTEGRATIONS ||
item.key === ROUTES.ORG_SETTINGS ||
item.key === ROUTES.MEMBERS_SETTINGS ||
item.key === ROUTES.SERVICE_ACCOUNTS_SETTINGS ||
item.key === ROUTES.INGESTION_SETTINGS ||
item.key === ROUTES.MCP_SERVER
? true
@@ -152,15 +168,22 @@ function SettingsPage(): JSX.Element {
}
if (!isCloudUser && !isEnterpriseSelfHostedUser) {
// Visible to all authenticated users
updatedItems = updatedItems.map((item) => ({
...item,
isEnabled:
item.key === ROUTES.ROLES_SETTINGS ||
item.key === ROUTES.ROLE_DETAILS ||
item.key === ROUTES.SERVICE_ACCOUNTS_SETTINGS
? true
: item.isEnabled,
}));
if (isAdmin) {
updatedItems = updatedItems.map((item) => ({
...item,
isEnabled:
item.key === ROUTES.ORG_SETTINGS ||
item.key === ROUTES.MEMBERS_SETTINGS ||
item.key === ROUTES.SERVICE_ACCOUNTS_SETTINGS ||
item.key === ROUTES.ROLES_SETTINGS ||
item.key === ROUTES.ROLE_DETAILS
item.key === ROUTES.ORG_SETTINGS || item.key === ROUTES.MEMBERS_SETTINGS
? true
: item.isEnabled,
}));

View File

@@ -78,11 +78,14 @@ describe('SettingsPage nav sections', () => {
});
});
it.each(['workspace', 'account'])('renders "%s" element', (id) => {
expect(screen.getByTestId(id)).toBeInTheDocument();
});
it.each(['workspace', 'account', 'roles', 'service-accounts'])(
'renders "%s" element',
(id) => {
expect(screen.getByTestId(id)).toBeInTheDocument();
},
);
it.each(['billing', 'roles'])('does not render "%s" element', (id) => {
it.each(['billing'])('does not render "%s" element', (id) => {
expect(screen.queryByTestId(id)).not.toBeInTheDocument();
});

View File

@@ -62,13 +62,16 @@ export const getRoutes = (
settings.push(...alertChannels(t));
// Visible to all authenticated users
settings.push(
...serviceAccountsSettings(t),
...rolesSettings(t),
...roleDetails(t),
);
// Admin-only: members management
if (isAdmin) {
settings.push(
...membersSettings(t),
...serviceAccountsSettings(t),
...rolesSettings(t),
...roleDetails(t),
);
settings.push(...membersSettings(t));
}
if ((isCloudUser || isEnterpriseSelfHostedUser) && isAdmin) {

View File

@@ -2,19 +2,15 @@ import { ReactElement } from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { renderHook, waitFor } from '@testing-library/react';
import setLocalStorageApi from 'api/browser/localstorage/set';
import type {
AuthtypesGettableTransactionDTO,
AuthtypesTransactionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { LOCALSTORAGE } from 'constants/localStorage';
import { SINGLE_FLIGHT_WAIT_TIME_MS } from 'hooks/useAuthZ/constants';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { USER_ROLES } from 'types/roles';
import { AUTHZ_CHECK_URL, authzMockResponse } from 'tests/authz-test-utils';
import { AppProvider, useAppContext } from '../App';
const AUTHZ_CHECK_URL = 'http://localhost/api/v1/authz/check';
const MY_USER_URL = 'http://localhost/api/v2/users/me';
const MY_ORG_URL = 'http://localhost/api/v2/orgs/me';
@@ -22,26 +18,9 @@ jest.mock('constants/env', () => ({
ENVIRONMENT: { baseURL: 'http://localhost', wsURL: '' },
}));
/**
* Since we are mocking the check permissions, this is needed
*/
const waitForSinglePreflightToFinish = async (): Promise<void> =>
await new Promise((r) => setTimeout(r, SINGLE_FLIGHT_WAIT_TIME_MS));
function authzMockResponse(
payload: AuthtypesTransactionDTO[],
authorizedByIndex: boolean[],
): { data: AuthtypesGettableTransactionDTO[]; status: string } {
return {
data: payload.map((txn, i) => ({
relation: txn.relation,
object: txn.object,
authorized: authorizedByIndex[i] ?? false,
})),
status: 'success',
};
}
const queryClient = new QueryClient({
defaultOptions: {
queries: {

View File

@@ -0,0 +1,128 @@
import type {
AuthtypesGettableTransactionDTO,
AuthtypesTransactionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { ENVIRONMENT } from 'constants/env';
import { gettableTransactionToPermission } from 'hooks/useAuthZ/utils';
import type {
BrandedPermission,
UseAuthZOptions,
UseAuthZResult,
} from 'hooks/useAuthZ/types';
import { rest } from 'msw';
import type { RestHandler } from 'msw';
export const AUTHZ_CHECK_URL = `${ENVIRONMENT.baseURL || ''}/api/v1/authz/check`;
export function authzMockResponse(
payload: AuthtypesTransactionDTO[],
authorizedByIndex: boolean[],
): { data: AuthtypesGettableTransactionDTO[]; status: string } {
return {
data: payload.map((txn, i) => ({
relation: txn.relation,
object: txn.object,
authorized: authorizedByIndex[i] ?? false,
})),
status: 'success',
};
}
export function setupAuthzAdmin(): RestHandler {
return rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = (await req.json()) as AuthtypesTransactionDTO[];
return res(
ctx.status(200),
ctx.json(
authzMockResponse(
payload,
payload.map(() => true),
),
),
);
});
}
/** Denies all permission checks. */
export function setupAuthzDenyAll(): RestHandler {
return rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = (await req.json()) as AuthtypesTransactionDTO[];
return res(
ctx.status(200),
ctx.json(
authzMockResponse(
payload,
payload.map(() => false),
),
),
);
});
}
/** Grants all permissions except the ones listed — matched precisely by relation + object. */
export function setupAuthzDeny(
...permissions: BrandedPermission[]
): RestHandler {
const denied = new Set<BrandedPermission>(permissions);
return rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = (await req.json()) as AuthtypesTransactionDTO[];
return res(
ctx.status(200),
ctx.json(
authzMockResponse(
payload,
payload.map((txn) => !denied.has(gettableTransactionToPermission(txn))),
),
),
);
});
}
/** Denies all permissions except the ones listed — matched precisely by relation + object. */
export function setupAuthzAllow(
...permissions: BrandedPermission[]
): RestHandler {
const allowed = new Set<BrandedPermission>(permissions);
return rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = (await req.json()) as AuthtypesTransactionDTO[];
return res(
ctx.status(200),
ctx.json(
authzMockResponse(
payload,
payload.map((txn) => allowed.has(gettableTransactionToPermission(txn))),
),
),
);
});
}
export function mockUseAuthZGrantAll(
permissions: BrandedPermission[],
_options?: UseAuthZOptions,
): UseAuthZResult {
return {
isLoading: false,
isFetching: false,
error: null,
permissions: Object.fromEntries(
permissions.map((p) => [p, { isGranted: true }]),
) as UseAuthZResult['permissions'],
refetchPermissions: jest.fn(),
};
}
export function mockUseAuthZDenyAll(
permissions: BrandedPermission[],
_options?: UseAuthZOptions,
): UseAuthZResult {
return {
isLoading: false,
isFetching: false,
error: null,
permissions: Object.fromEntries(
permissions.map((p) => [p, { isGranted: false }]),
) as UseAuthZResult['permissions'],
refetchPermissions: jest.fn(),
};
}

View File

@@ -98,10 +98,10 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
GET_STARTED_AZURE_MONITORING: ['ADMIN', 'EDITOR', 'VIEWER'],
WORKSPACE_LOCKED: ['ADMIN', 'EDITOR', 'VIEWER'],
WORKSPACE_SUSPENDED: ['ADMIN', 'EDITOR', 'VIEWER'],
ROLES_SETTINGS: ['ADMIN'],
ROLE_DETAILS: ['ADMIN'],
ROLES_SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER'],
ROLE_DETAILS: ['ADMIN', 'EDITOR', 'VIEWER'],
MEMBERS_SETTINGS: ['ADMIN'],
SERVICE_ACCOUNTS_SETTINGS: ['ADMIN'],
SERVICE_ACCOUNTS_SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER'],
BILLING: ['ADMIN'],
SUPPORT: ['ADMIN', 'EDITOR', 'VIEWER', 'ANONYMOUS'],
SOMETHING_WENT_WRONG: ['ADMIN', 'EDITOR', 'VIEWER'],

152
go.mod
View File

@@ -19,7 +19,7 @@ require (
github.com/gin-gonic/gin v1.11.0
github.com/go-co-op/gocron v1.30.1
github.com/go-openapi/runtime v0.29.2
github.com/go-openapi/strfmt v0.25.0
github.com/go-openapi/strfmt v0.26.1
github.com/go-playground/validator/v10 v10.27.0
github.com/go-redis/redismock/v9 v9.2.0
github.com/go-viper/mapstructure/v2 v2.5.0
@@ -30,10 +30,10 @@ require (
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
github.com/huandu/go-sqlbuilder v1.39.1
github.com/jackc/pgx/v5 v5.8.0
github.com/jackc/pgx/v5 v5.9.2
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12
github.com/knadh/koanf v1.5.0
github.com/knadh/koanf/v2 v2.3.2
github.com/knadh/koanf/v2 v2.3.3
github.com/mailru/easyjson v0.9.0
github.com/open-telemetry/opamp-go v0.22.0
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza v0.144.0
@@ -42,15 +42,15 @@ require (
github.com/opentracing/opentracing-go v1.2.0
github.com/perses/perses v0.53.1
github.com/pkg/errors v0.9.1
github.com/prometheus/alertmanager v0.31.0
github.com/prometheus/alertmanager v0.31.1
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/common v0.67.5
github.com/prometheus/prometheus v0.310.0
github.com/prometheus/prometheus v0.311.3
github.com/redis/go-redis/extra/redisotel/v9 v9.15.1
github.com/redis/go-redis/v9 v9.17.2
github.com/rs/cors v1.11.1
github.com/russellhaering/gosaml2 v0.9.0
github.com/russellhaering/goxmldsig v1.2.0
github.com/russellhaering/gosaml2 v0.11.0
github.com/russellhaering/goxmldsig v1.6.0
github.com/samber/lo v1.47.0
github.com/segmentio/analytics-go/v3 v3.2.1
github.com/sethvargo/go-password v0.2.0
@@ -67,12 +67,12 @@ require (
github.com/uptrace/bun/dialect/sqlitedialect v1.2.9
github.com/uptrace/bun/extra/bunotel v1.2.9
github.com/yuin/goldmark v1.7.16
go.opentelemetry.io/collector/confmap v1.51.0
go.opentelemetry.io/collector/confmap v1.54.0
go.opentelemetry.io/collector/otelcol v0.144.0
go.opentelemetry.io/collector/pdata v1.51.0
go.opentelemetry.io/collector/pdata v1.54.0
go.opentelemetry.io/contrib/config v0.10.0
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.63.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0
go.opentelemetry.io/otel v1.43.0
go.opentelemetry.io/otel/metric v1.43.0
go.opentelemetry.io/otel/sdk v1.43.0
@@ -80,57 +80,59 @@ require (
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.27.1
golang.org/x/crypto v0.49.0
golang.org/x/exp v0.0.0-20260112195511-716be5621a96
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa
golang.org/x/net v0.52.0
golang.org/x/oauth2 v0.35.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sync v0.20.0
golang.org/x/text v0.35.0
gonum.org/v1/gonum v0.17.0
google.golang.org/api v0.265.0
google.golang.org/api v0.272.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/apimachinery v0.35.2
k8s.io/apimachinery v0.35.3
modernc.org/sqlite v1.40.1
)
require (
github.com/IBM/pgxpoolprometheus v1.1.2 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.7 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.7 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.12 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect
github.com/aws/aws-sdk-go-v2/service/sns v1.39.11 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-openapi/swag/cmdutils v0.25.4 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect
github.com/go-openapi/swag/fileutils v0.25.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
github.com/go-openapi/swag/loading v0.25.4 // indirect
github.com/go-openapi/swag/mangling v0.25.4 // indirect
github.com/go-openapi/swag/netutils v0.25.4 // indirect
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/go-openapi/swag/cmdutils v0.25.5 // indirect
github.com/go-openapi/swag/conv v0.25.5 // indirect
github.com/go-openapi/swag/fileutils v0.25.5 // indirect
github.com/go-openapi/swag/jsonname v0.25.5 // indirect
github.com/go-openapi/swag/jsonutils v0.25.5 // indirect
github.com/go-openapi/swag/loading v0.25.5 // indirect
github.com/go-openapi/swag/mangling v0.25.5 // indirect
github.com/go-openapi/swag/netutils v0.25.5 // indirect
github.com/go-openapi/swag/stringutils v0.25.5 // indirect
github.com/go-openapi/swag/typeutils v0.25.5 // indirect
github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/hashicorp/go-metrics v0.5.4 // indirect
github.com/huandu/go-clone v1.7.3 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
@@ -138,7 +140,8 @@ require (
github.com/muhlemmer/gu v0.3.1 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/perses/common v0.30.2 // indirect
github.com/prometheus/client_golang/exp v0.0.0-20260108101519-fb0838f53562 // indirect
github.com/prometheus/client_golang/exp v0.0.0-20260325093428-d8591d0db856 // indirect
github.com/puzpuzpuz/xsync/v4 v4.4.0 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.15.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/swaggest/refl v1.4.0 // indirect
@@ -146,24 +149,33 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/zitadel/oidc/v3 v3.45.4 // indirect
github.com/zitadel/schema v1.3.2 // indirect
go.opentelemetry.io/collector/client v1.50.0 // indirect
go.opentelemetry.io/collector/client v1.54.0 // indirect
go.opentelemetry.io/collector/config/configoptional v1.50.0 // indirect
go.opentelemetry.io/collector/config/configretry v1.50.0 // indirect
go.opentelemetry.io/collector/exporter/exporterhelper v0.144.0 // indirect
go.opentelemetry.io/collector/internal/componentalias v0.145.0 // indirect
go.opentelemetry.io/collector/pdata/xpdata v0.144.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.opentelemetry.io/collector/internal/componentalias v0.148.0 // indirect
go.opentelemetry.io/collector/pdata/xpdata v0.148.0 // indirect
go.uber.org/goleak v1.3.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/term v0.41.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
require (
cel.dev/expr v0.25.1 // indirect
cloud.google.com/go/auth v0.18.1 // indirect
cloud.google.com/go/auth v0.18.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
@@ -176,7 +188,7 @@ require (
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
github.com/beevik/etree v1.1.0 // indirect
github.com/beevik/etree v1.6.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
@@ -191,7 +203,7 @@ require (
github.com/edsrzf/mmap-go v1.2.0 // indirect
github.com/elastic/lunes v0.2.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
github.com/expr-lang/expr v1.17.7
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
@@ -203,12 +215,12 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-openapi/analysis v0.24.2 // indirect
github.com/go-openapi/errors v0.22.6 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/errors v0.22.7 // indirect
github.com/go-openapi/jsonpointer v0.22.5 // indirect
github.com/go-openapi/jsonreference v0.21.4 // indirect
github.com/go-openapi/loads v0.23.2 // indirect
github.com/go-openapi/spec v0.22.3 // indirect
github.com/go-openapi/swag v0.25.4 // indirect
github.com/go-openapi/swag v0.25.5 // indirect
github.com/go-openapi/validate v0.25.1 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.10.5 // indirect
@@ -219,8 +231,8 @@ require (
github.com/google/btree v1.1.3 // indirect
github.com/google/cel-go v0.27.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
github.com/googleapis/gax-go/v2 v2.18.0 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
@@ -246,7 +258,7 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/jtolds/gls v4.20.0+incompatible // indirect
github.com/klauspost/compress v1.18.3 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
@@ -270,13 +282,12 @@ require (
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
github.com/natefinch/wrap v0.2.0 // indirect
github.com/oklog/run v1.2.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/oklog/ulid/v2 v2.1.1
github.com/open-feature/go-sdk v1.17.0
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.144.0 // indirect
github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.145.0 // indirect
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.145.0 // indirect
github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.145.0 // indirect
github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.148.0 // indirect
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.148.0 // indirect
github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.148.0 // indirect
github.com/openfga/openfga v1.11.2
github.com/paulmach/orb v0.11.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
@@ -321,22 +332,21 @@ require (
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
go.mongodb.org/mongo-driver v1.17.6 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/collector/component v1.51.0 // indirect
go.opentelemetry.io/collector/component/componentstatus v0.145.0 // indirect
go.opentelemetry.io/collector/component/componenttest v0.145.0 // indirect
go.opentelemetry.io/collector/component v1.54.0 // indirect
go.opentelemetry.io/collector/component/componentstatus v0.148.0 // indirect
go.opentelemetry.io/collector/component/componenttest v0.148.0 // indirect
go.opentelemetry.io/collector/config/configtelemetry v0.144.0 // indirect
go.opentelemetry.io/collector/confmap/provider/envprovider v1.50.0 // indirect
go.opentelemetry.io/collector/confmap/provider/fileprovider v1.50.0 // indirect
go.opentelemetry.io/collector/confmap/xconfmap v0.145.0 // indirect
go.opentelemetry.io/collector/confmap/xconfmap v0.148.0 // indirect
go.opentelemetry.io/collector/connector v0.144.0 // indirect
go.opentelemetry.io/collector/connector/connectortest v0.144.0 // indirect
go.opentelemetry.io/collector/connector/xconnector v0.144.0 // indirect
go.opentelemetry.io/collector/consumer v1.51.0 // indirect
go.opentelemetry.io/collector/consumer v1.54.0 // indirect
go.opentelemetry.io/collector/consumer/consumererror v0.144.0 // indirect
go.opentelemetry.io/collector/consumer/consumertest v0.145.0 // indirect
go.opentelemetry.io/collector/consumer/xconsumer v0.145.0 // indirect
go.opentelemetry.io/collector/consumer/consumertest v0.148.0 // indirect
go.opentelemetry.io/collector/consumer/xconsumer v0.148.0 // indirect
go.opentelemetry.io/collector/exporter v1.50.0 // indirect
go.opentelemetry.io/collector/exporter/exportertest v0.144.0 // indirect
go.opentelemetry.io/collector/exporter/xexporter v0.144.0 // indirect
@@ -344,17 +354,17 @@ require (
go.opentelemetry.io/collector/extension/extensioncapabilities v0.144.0 // indirect
go.opentelemetry.io/collector/extension/extensiontest v0.144.0 // indirect
go.opentelemetry.io/collector/extension/xextension v0.144.0 // indirect
go.opentelemetry.io/collector/featuregate v1.51.0 // indirect
go.opentelemetry.io/collector/featuregate v1.54.0 // indirect
go.opentelemetry.io/collector/internal/fanoutconsumer v0.144.0 // indirect
go.opentelemetry.io/collector/internal/telemetry v0.144.0 // indirect
go.opentelemetry.io/collector/pdata/pprofile v0.145.0 // indirect
go.opentelemetry.io/collector/pdata/testdata v0.145.0 // indirect
go.opentelemetry.io/collector/pipeline v1.51.0 // indirect
go.opentelemetry.io/collector/pdata/pprofile v0.148.0 // indirect
go.opentelemetry.io/collector/pdata/testdata v0.148.0 // indirect
go.opentelemetry.io/collector/pipeline v1.54.0 // indirect
go.opentelemetry.io/collector/pipeline/xpipeline v0.144.0 // indirect
go.opentelemetry.io/collector/processor v1.51.0 // indirect
go.opentelemetry.io/collector/processor v1.54.0 // indirect
go.opentelemetry.io/collector/processor/processorhelper v0.144.0 // indirect
go.opentelemetry.io/collector/processor/processortest v0.145.0 // indirect
go.opentelemetry.io/collector/processor/xprocessor v0.145.0 // indirect
go.opentelemetry.io/collector/processor/processortest v0.148.0 // indirect
go.opentelemetry.io/collector/processor/xprocessor v0.148.0 // indirect
go.opentelemetry.io/collector/receiver v1.50.0 // indirect
go.opentelemetry.io/collector/receiver/receiverhelper v0.144.0 // indirect
go.opentelemetry.io/collector/receiver/receivertest v0.144.0 // indirect
@@ -363,7 +373,7 @@ require (
go.opentelemetry.io/collector/service v0.144.0 // indirect
go.opentelemetry.io/collector/service/hostcapabilities v0.144.0 // indirect
go.opentelemetry.io/contrib/bridges/otelzap v0.13.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.65.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.67.0 // indirect
go.opentelemetry.io/contrib/otelconf v0.18.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 // indirect
@@ -371,7 +381,7 @@ require (
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.60.0
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 // indirect
@@ -386,14 +396,14 @@ require (
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.42.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9
google.golang.org/grpc v1.80.0 // indirect
gopkg.in/telebot.v3 v3.3.8 // indirect
k8s.io/client-go v0.35.2 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/client-go v0.35.3 // indirect
k8s.io/klog/v2 v2.140.0 // indirect
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
)

380
go.sum
View File

@@ -31,8 +31,8 @@ cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=
cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
@@ -138,54 +138,58 @@ github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVj
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw=
github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY=
github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY=
github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0=
github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g=
github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ=
github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8=
github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw=
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8=
github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
github.com/aws/aws-sdk-go-v2/service/appconfig v1.4.2/go.mod h1:FZ3HkCe+b10uFZZkFdvf98LHW21k49W8o8J366lqVKY=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.285.0 h1:cRZQsqCy59DSJmvmUYzi9K+dutysXzfx6F+fkcIHtOk=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.285.0/go.mod h1:Uy+C+Sc58jozdoL1McQr8bDsEvNFx+/nBY+vpO1HVUY=
github.com/aws/aws-sdk-go-v2/service/ecs v1.71.0 h1:MzP/ElwTpINq+hS80ZQz4epKVnUTlz8Sz+P/AFORCKM=
github.com/aws/aws-sdk-go-v2/service/ecs v1.71.0/go.mod h1:pMlGFDpHoLTJOIZHGdJOAWmi+xeIlQXuFTuQxs1epYE=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.296.0 h1:98Miqj16un1WLNyM1RjVDhXYumhqZrQfAeG8i4jPG6o=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.296.0/go.mod h1:T6ndRfdhnXLIY5oKBHjYZDVj706los2zGdpThppquvA=
github.com/aws/aws-sdk-go-v2/service/ecs v1.74.0 h1:YS5TXaEvzDb+sV+wdQFUtuCAk0GeFR9Ai6HFdxpz6q8=
github.com/aws/aws-sdk-go-v2/service/ecs v1.74.0/go.mod h1:10kBgdaNJz0FO/+JWDUH+0rtSjkn5yafgavDDmmhFzs=
github.com/aws/aws-sdk-go-v2/service/elasticache v1.51.12 h1:S066ajzfPRCSW4lsSHOYglne6SNi2CHt1u5omzW1RBg=
github.com/aws/aws-sdk-go-v2/service/elasticache v1.51.12/go.mod h1:86SE4NcXxbxr8KTG3yOyDmd4HyiFmKl8TexXnhYJ+Bw=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
github.com/aws/aws-sdk-go-v2/service/kafka v1.46.7 h1:0jDb9b505gbCmtjH1RT7kx8hDbVDzOhnTeZm7dzskpQ=
github.com/aws/aws-sdk-go-v2/service/kafka v1.46.7/go.mod h1:tWnHS64fg5ydLHivFlCAtEh/1iMNzr56QsH3F+UTwD4=
github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.11 h1:VM5e5M39zRSs+aT0O9SoxHjUXqXxhbw3Yi0FdMQWPIc=
github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.11/go.mod h1:0jvzYPIQGCpnY/dmdaotTk2JH4QuBlnW0oeyrcGLWJ4=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=
github.com/aws/aws-sdk-go-v2/service/kafka v1.49.1 h1:BgBatWcQIFqF1l6KGHjv66V0d/ISnWrTwxDx/Jf6EJM=
github.com/aws/aws-sdk-go-v2/service/kafka v1.49.1/go.mod h1:pMpys+PlrN//vj8j5s0oOAMJjauj81VkHzIZxPVWOro=
github.com/aws/aws-sdk-go-v2/service/lightsail v1.51.0 h1:cg6PxzoIide2wiEyLfikOFN+XwHafwR8p5+L9U1E8dQ=
github.com/aws/aws-sdk-go-v2/service/lightsail v1.51.0/go.mod h1:YvX7hjUWecrKX8fBkbEncyddEW85xjNH+u5JHioITOw=
github.com/aws/aws-sdk-go-v2/service/rds v1.117.0 h1:T1Xe9sYxSUUQOvd1RsFeVk/IXFPdqSiN0atXu/Hy/8A=
github.com/aws/aws-sdk-go-v2/service/rds v1.117.0/go.mod h1:QbXW4coAMakHQhf1qhE0eVVCen9gwB/Kvn+HHHKhpGY=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE=
github.com/aws/aws-sdk-go-v2/service/sns v1.39.11 h1:Ke7RS0NuP9Xwk31prXYcFGA1Qfn8QmNWcxyjKPcXZdc=
github.com/aws/aws-sdk-go-v2/service/sns v1.39.11/go.mod h1:hdZDKzao0PBfJJygT7T92x2uVcWc/htqlhrjFIjnHDM=
github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA=
github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk=
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps=
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0=
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
github.com/beevik/etree v1.6.0 h1:u8Kwy8pp9D9XeITj2Z0XtA5qqZEmtJtuXZRQi+j03eE=
github.com/beevik/etree v1.6.0/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
@@ -263,8 +267,8 @@ github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa5
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/digitalocean/godo v1.173.0 h1:tgzevGhlz9VFjk2y3NmeItUT4vIVVCRFETlG/1GlEQI=
github.com/digitalocean/godo v1.173.0/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU=
github.com/digitalocean/godo v1.178.0 h1:+B4xGOaoFwwwpM7TKhoyGHdmFg5eF9zDB1YfOLvNJ2E=
github.com/digitalocean/godo v1.178.0/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
@@ -300,11 +304,11 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM=
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:bH6Xx7IW64qjjJq8M2u4dxNaBiDfKK+z/3eGDpXEQhc=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
@@ -361,10 +365,10 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-openapi/analysis v0.24.2 h1:6p7WXEuKy1llDgOH8FooVeO+Uq2za9qoAOq4ZN08B50=
github.com/go-openapi/analysis v0.24.2/go.mod h1:x27OOHKANE0lutg2ml4kzYLoHGMKgRm1Cj2ijVOjJuE=
github.com/go-openapi/errors v0.22.6 h1:eDxcf89O8odEnohIXwEjY1IB4ph5vmbUsBMsFNwXWPo=
github.com/go-openapi/errors v0.22.6/go.mod h1:z9S8ASTUqx7+CP1Q8dD8ewGH/1JWFFLX/2PmAYNQLgk=
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA=
github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w=
github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
github.com/go-openapi/loads v0.23.2 h1:rJXAcP7g1+lWyBHC7iTY+WAF0rprtM+pm8Jxv1uQJp4=
@@ -373,38 +377,38 @@ github.com/go-openapi/runtime v0.29.2 h1:UmwSGWNmWQqKm1c2MGgXVpC2FTGwPDQeUsBMufc
github.com/go-openapi/runtime v0.29.2/go.mod h1:biq5kJXRJKBJxTDJXAa00DOTa/anflQPhT0/wmjuy+0=
github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc=
github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=
github.com/go-openapi/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ=
github.com/go-openapi/strfmt v0.25.0/go.mod h1:nNXct7OzbwrMY9+5tLX4I21pzcmE6ccMGXl3jFdPfn8=
github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU=
github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ=
github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4=
github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y=
github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk=
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48=
github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg=
github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0=
github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg=
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-openapi/strfmt v0.26.1 h1:7zGCHji7zSYDC2tCXIusoxYQz/48jAf2q+sF6wXTG+c=
github.com/go-openapi/strfmt v0.26.1/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y=
github.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU=
github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA=
github.com/go-openapi/swag/cmdutils v0.25.5 h1:yh5hHrpgsw4NwM9KAEtaDTXILYzdXh/I8Whhx9hKj7c=
github.com/go-openapi/swag/cmdutils v0.25.5/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=
github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g=
github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k=
github.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk=
github.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc=
github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=
github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo=
github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo=
github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU=
github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g=
github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw=
github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY=
github.com/go-openapi/swag/netutils v0.25.5 h1:LZq2Xc2QI8+7838elRAaPCeqJnHODfSyOa7ZGfxDKlU=
github.com/go-openapi/swag/netutils v0.25.5/go.mod h1:lHbtmj4m57APG/8H7ZcMMSWzNqIQcu0RFiXrPUara14=
github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M=
github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII=
github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E=
github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc=
github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ=
github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.0 h1:7SgOMTvJkM8yWrQlU8Jm18VeDPuAvB/xWrdxFJkoFag=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.0/go.mod h1:14iV8jyyQlinc9StD7w1xVPW3CO3q1Gj04Jy//Kw4VM=
github.com/go-openapi/testify/v2 v2.4.1 h1:zB34HDKj4tHwyUQHrUkpV0Q0iXQ6dUCOQtIqn8hE6Iw=
github.com/go-openapi/testify/v2 v2.4.1/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-openapi/validate v0.25.1 h1:sSACUI6Jcnbo5IWqbYHgjibrhhmt3vR6lCzKZnmAgBw=
github.com/go-openapi/validate v0.25.1/go.mod h1:RMVyVFYte0gbSTaZ0N4KmTn6u/kClvAFp+mAVfS/DQc=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
@@ -421,8 +425,8 @@ github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHO
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-redis/redismock/v9 v9.2.0 h1:ZrMYQeKPECZPjOj5u9eyOjg8Nnb0BS9lkVIZ6IpsKLw=
github.com/go-redis/redismock/v9 v9.2.0/go.mod h1:18KHfGDK4Y6c2R0H38EUGWAdc7ZQS9gfYxc94k7rWT0=
github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4=
github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk=
github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@@ -533,16 +537,16 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef h1:xpF9fUHpoIrrjX24DURVKiwHcFpw19ndIs+FwTSMbno=
github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc h1:VBbFa1lDYWEeV5FZKUiYKYT0VxCp9twUmmaq9eb8sXw=
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
@@ -550,11 +554,11 @@ github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0
github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
github.com/googleapis/gax-go/v2 v2.18.0 h1:jxP5Uuo3bxm3M6gGtV94P4lliVetoCB4Wk2x8QA86LI=
github.com/googleapis/gax-go/v2 v2.18.0/go.mod h1:uSzZN4a356eRG985CzJ3WfbFSpqkLTjsnhWGJR6EwrE=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gophercloud/gophercloud/v2 v2.10.0 h1:NRadC0aHNvy4iMoFXj5AFiPmut/Sj3hAPAo9B59VMGc=
github.com/gophercloud/gophercloud/v2 v2.10.0/go.mod h1:Ki/ILhYZr/5EPebrPL9Ej+tUg4lqx71/YH2JWVeU+Qk=
github.com/gophercloud/gophercloud/v2 v2.11.1 h1:jCs4vLH8sJgRqrPzqVfWgl7uI6JnIIlsgeIRM0uHjxY=
github.com/gophercloud/gophercloud/v2 v2.11.1/go.mod h1:Rm0YvKQ4QYX2rY9XaDKnjRzSGwlG5ge4h6ABYnmkKQM=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
@@ -640,8 +644,8 @@ github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/
github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/memberlist v0.5.4 h1:40YY+3qq2tAUhZIMEK8kqusKZBBjdwJ3NUjvYkcxh74=
github.com/hashicorp/memberlist v0.5.4/go.mod h1:OgN6xiIo6RlHUWk+ALjP9e32xWCoQrsOCmHrWCm2MWA=
github.com/hashicorp/nomad/api v0.0.0-20260205205048-8315996478d1 h1:2T7Ay5FMAnZUBxSbrkjufY5YKiLPWij0dDPnbM/KYak=
github.com/hashicorp/nomad/api v0.0.0-20260205205048-8315996478d1/go.mod h1:JAmS1nGJ1KcTM+MHAkgyrL0GDbsnKiJsp75KyqO2wWc=
github.com/hashicorp/nomad/api v0.0.0-20260324203407-b27b0c2e019a h1:HGwfgBNl90YBiHdbzZ/+8aMxO1UL9B/yNTAXa8iB8z8=
github.com/hashicorp/nomad/api v0.0.0-20260324203407-b27b0c2e019a/go.mod h1:KkLNLU0Nyfh5jWsFoF/PsmMbKpRIAoIV4lmQoJWgKCk=
github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=
@@ -675,8 +679,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=
@@ -688,7 +692,6 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfC
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@@ -714,14 +717,14 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs=
github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs=
github.com/knadh/koanf/v2 v2.3.2 h1:Ee6tuzQYFwcZXQpc2MiVeC6qHMandf5SMUJJNoFp/c4=
github.com/knadh/koanf/v2 v2.3.2/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
github.com/knadh/koanf/v2 v2.3.3 h1:jLJC8XCRfLC7n4F+ZKKdBsbq1bfXTpuFhf4L7t94D94=
github.com/knadh/koanf/v2 v2.3.3/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -751,8 +754,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/leodido/ragel-machinery v0.0.0-20190525184631-5f46317e436b h1:11UHH39z1RhZ5dc4y4r/4koJo6IYFgTRMe/LlwRTEw0=
github.com/leodido/ragel-machinery v0.0.0-20190525184631-5f46317e436b/go.mod h1:WZxr2/6a/Ar9bMDc2rN/LJrE/hF6bXE4LPyDSIxwAfg=
github.com/linode/linodego v1.65.0 h1:SdsuGD8VSsPWeShXpE7ihl5vec+fD3MgwhnfYC/rj7k=
github.com/linode/linodego v1.65.0/go.mod h1:tOFiTErdjkbVnV+4S0+NmIE9dqqZUEM2HsJaGu8wMh8=
github.com/linode/linodego v1.66.0 h1:rK8QJFaV53LWOEJvb/evhTg/dP5ElvtuZmx4iv4RJds=
github.com/linode/linodego v1.66.0/go.mod h1:12ykGs9qsvxE+OU3SXuW2w+DTruWF35FPlXC7gGk2tU=
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
@@ -840,8 +843,6 @@ github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JX
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E=
github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
@@ -858,16 +859,16 @@ github.com/open-telemetry/opentelemetry-collector-contrib/internal/common v0.144
github.com/open-telemetry/opentelemetry-collector-contrib/internal/common v0.144.0/go.mod h1:R0go5FMmUe51VpKl8YCk/rUxibA+U3lfPYMoihQ/nhw=
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.144.0 h1:Qv3nLVGKJ9LQCGwxteJxjSNyQ5CP99QRvYPFn6d8Y60=
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.144.0/go.mod h1:O2rZKRXk1WeYhzfJBVXES/g7+PlIds/TzPZW/4NfTNA=
github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.145.0 h1:0dYiJ7krIwaHFX6YLNDo/yawTZIu8X16tT/nwW1UTG8=
github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.145.0/go.mod h1:mhoa9lipcEH0heeKf6+xHzGUrCuAgImQv4/Qpmu0+Fk=
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.145.0 h1:0ithmsGyVtjzODmAPp9pkxA4IlnYpyeXmDWrryTkHNo=
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.145.0/go.mod h1:r+K/aCWpUCDDM5Gisznf9ZQjpZcyFr84CuATA9486JQ=
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.145.0 h1:sB4yuYx45zig1ceQ+kmrEYy0xMZ+mGagwYIFtJkkU1w=
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.145.0/go.mod h1:uLhceuH7ZtiVxk+B0MHI0vhJG2Y4aOzT/hrV6c5KjVU=
github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.148.0 h1:CiTjQE/Hh5xK2t56ogrDK4nl0+tJPNmASCs4zEYZ/xU=
github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.148.0/go.mod h1:WUFkzTiOpt7EYyL67gv1GOf3RD8qKWGtin3lY9LYzW4=
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.148.0 h1:i12duJOl5VCb9mbb8FfZCaP2CjeXbNsbg82JjSe7sy8=
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.148.0/go.mod h1:jyw+QvkmCrF/oYy31O2ndb5KZZK4l+iR89msnV3LN/k=
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.148.0 h1:1TLg6YrS3Au6F7xw3ws2Njbwj13IMqPplvGFi+18fWs=
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.148.0/go.mod h1:P8hZEDIQk4REgUWyLhSVRHwTxK6KkifKfg36BmmQ/DI=
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza v0.144.0 h1:EIAygME70IOdEwaSr6bA3Wcdp7hXEqRsGsVfrI5v8OA=
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza v0.144.0/go.mod h1:3Y6ctEEwRg19B0jqsrQH6Hiquqte+zC0ZxpXLLSa5sA=
github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.145.0 h1:en86L47oOTsAkbDc5VEMF5cziXPBK2D4hqGRqLaJtCw=
github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.145.0/go.mod h1:osDRUOIfd7IiKkDvcE/VrPp9FFOPJmFp73RuvgOn5gE=
github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.148.0 h1:xgD/kNGp/wWY+bwY599Pc01OamYN17phRiTP934bM5Y=
github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.148.0/go.mod h1:ZK7wvaefla9lB3bAW0rNKt7IzRPcTRQoOFqr4sZy/XM=
github.com/open-telemetry/opentelemetry-collector-contrib/processor/logstransformprocessor v0.144.0 h1:HbmpzTixpQG/xGhQuQoiJTXQPrixe+yivAsF6tl2o4g=
github.com/open-telemetry/opentelemetry-collector-contrib/processor/logstransformprocessor v0.144.0/go.mod h1:32jR9iqxVozOJ/Lg5RkCNoW18uCNwpKSbs6h5c28Ep4=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -924,8 +925,8 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM=
github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY=
github.com/prometheus/alertmanager v0.31.0 h1:DQW02uIUNNiAa9AD9VA5xaFw5D+xrV+bocJc4gN9bEU=
github.com/prometheus/alertmanager v0.31.0/go.mod h1:zWPQwhbLt2ybee8rL921UONeQ59Oncash+m/hGP17tU=
github.com/prometheus/alertmanager v0.31.1 h1:eAmIC42lzbWslHkMt693T36qdxfyZULswiHr681YS3Q=
github.com/prometheus/alertmanager v0.31.1/go.mod h1:zWPQwhbLt2ybee8rL921UONeQ59Oncash+m/hGP17tU=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
@@ -933,8 +934,8 @@ github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_golang/exp v0.0.0-20260108101519-fb0838f53562 h1:vwqZvuobg82U0gcG2eVrFH27806bUbNr32SvfRbvdsg=
github.com/prometheus/client_golang/exp v0.0.0-20260108101519-fb0838f53562/go.mod h1:PmAYDB13uBFBG9qE1qxZZgZWhg7Rg6SfKM5DMK7hjyI=
github.com/prometheus/client_golang/exp v0.0.0-20260325093428-d8591d0db856 h1:1Y6bmpZb8peQCy1IpctnAhIFuyhrdtMaDnETChhSNns=
github.com/prometheus/client_golang/exp v0.0.0-20260325093428-d8591d0db856/go.mod h1:Vf0QcmVhGqpjLxZOaWrFSep86vchQtJmbztFaMM4f6Q=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -958,12 +959,14 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/prometheus/prometheus v0.310.0 h1:iS0Uul/dHjy8ifBnqo3YEOhRxlTOWantRoDWwmIowwA=
github.com/prometheus/prometheus v0.310.0/go.mod h1:rs6XoWKvgAStqxHxb2Twh1BR6rp7qw7fmUgW+gaXjbw=
github.com/prometheus/prometheus v0.311.3 h1:3IrVxQv6v5i/ZCGi6OrYeBhtCwaPTn6Z3DYruXoYm3M=
github.com/prometheus/prometheus v0.311.3/go.mod h1:gjsCxTKtHO1Q8T9333u1s+lUR1OjPyM7ruuGH8RvVyo=
github.com/prometheus/sigv4 v0.4.1 h1:EIc3j+8NBea9u1iV6O5ZAN8uvPq2xOIUPcqCTivHuXs=
github.com/prometheus/sigv4 v0.4.1/go.mod h1:eu+ZbRvsc5TPiHwqh77OWuCnWK73IdkETYY46P4dXOU=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/puzpuzpuz/xsync/v4 v4.4.0 h1:vlSN6/CkEY0pY8KaB0yqo/pCLZvp9nhdbBdjipT4gWo=
github.com/puzpuzpuz/xsync/v4 v4.4.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/redis/go-redis/extra/rediscmd/v9 v9.15.1 h1:G3pzZlMvMX9VX9TBB8zr03CAkeyMtbyW2D59PdyaGkM=
github.com/redis/go-redis/extra/rediscmd/v9 v9.15.1/go.mod h1:JiJ4f0bngycE8LQqzY/4TB23witBbFnlUS6hPvHn6Zc=
@@ -979,16 +982,15 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/russellhaering/gosaml2 v0.9.0 h1:CNMnH42z/GirrKjdmNrSS6bAAs47F9bPdl4PfRmVOIk=
github.com/russellhaering/gosaml2 v0.9.0/go.mod h1:byViER/1YPUa0Puj9ROZblpoq2jsE7h/CJmitzX0geU=
github.com/russellhaering/goxmldsig v1.2.0 h1:Y6GTTc9Un5hCxSzVz4UIWQ/zuVwDvzJk80guqzwx6Vg=
github.com/russellhaering/goxmldsig v1.2.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw=
github.com/russellhaering/gosaml2 v0.11.0 h1:wlWm7dWMrpJBzh0xEOZof70nVen4f/2BEF8ZXaidJ9o=
github.com/russellhaering/gosaml2 v0.11.0/go.mod h1:GmL5LeCP7PBYzSkkFxtmHuRzC2eUZ/6JSLYQd5fzKK4=
github.com/russellhaering/goxmldsig v1.6.0 h1:8fdWXEPh2k/NZNQBPFNoVfS3JmzS4ZprY/sAOpKQLks=
github.com/russellhaering/goxmldsig v1.6.0/go.mod h1:TrnaquDcYxWXfJrOjeMBTX4mLBeYAqaHEyUeWPxZlBM=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
@@ -1058,8 +1060,8 @@ github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/srikanthccv/ClickHouse-go-mock v0.13.0 h1:/b7DQphGkh29ocNtLh4DGmQxQYA0CfHz65Wy2zAH2GM=
github.com/srikanthccv/ClickHouse-go-mock v0.13.0/go.mod h1:LiiyBUdXNwB/1DE9rgK/8q9qjVYsTzg6WXQ/3mU3TeY=
github.com/stackitcloud/stackit-sdk-go/core v0.21.1 h1:Y/PcAgM7DPYMNqum0MLv4n1mF9ieuevzcCIZYQfm3Ts=
github.com/stackitcloud/stackit-sdk-go/core v0.21.1/go.mod h1:osMglDby4csGZ5sIfhNyYq1bS1TxIdPY88+skE/kkmI=
github.com/stackitcloud/stackit-sdk-go/core v0.23.0 h1:zPrOhf3Xe47rKRs1fg/AqKYUiJJRYjdcv+3qsS50mEs=
github.com/stackitcloud/stackit-sdk-go/core v0.23.0/go.mod h1:osMglDby4csGZ5sIfhNyYq1bS1TxIdPY88+skE/kkmI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
@@ -1134,8 +1136,8 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs=
github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI=
github.com/vultr/govultr/v3 v3.28.1 h1:KR3LhppYARlBujY7+dcrE7YKL0Yo9qXL+msxykKQrLI=
github.com/vultr/govultr/v3 v3.28.1/go.mod h1:2zyUw9yADQaGwKnwDesmIOlBNLrm7edsCfWHFJpWKf8=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
@@ -1170,8 +1172,6 @@ go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3
go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU=
go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY=
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -1182,14 +1182,14 @@ go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/collector v0.144.0 h1:nMebGQYlNnrROtAgUmXl7/G8l2eZl/vwGx0ejQmE9FI=
go.opentelemetry.io/collector/client v1.50.0 h1:T0WC2bU252x9a7kRZNyyADpkRN6j4HnlfHTnbxc0ElU=
go.opentelemetry.io/collector/client v1.50.0/go.mod h1:fFG6F0BeKMMlIj9POp71ynIH+XG8BvIxt+9dqfWNmZA=
go.opentelemetry.io/collector/component v1.51.0 h1:btNW76MCRmpsk0ARRT5wspDXF9tvdaLd3uBtYXIiQn0=
go.opentelemetry.io/collector/component v1.51.0/go.mod h1:Zlgwh4yTLDhJglOXqiyXZ7paepTvvoijfFjLqOr/Qww=
go.opentelemetry.io/collector/component/componentstatus v0.145.0 h1:EwUZfSaagdpRXnlrb0TqReJXXW2p9HWBU5YiIeXPCAE=
go.opentelemetry.io/collector/component/componentstatus v0.145.0/go.mod h1:OiYb8rT4FtSJPFSGCKYvOaajdueDUTJZncixGrmy5aM=
go.opentelemetry.io/collector/component/componenttest v0.145.0 h1:ryhRrXqQybGMhz7A7t32NC8BXAFcX2o1RetgPM7vw88=
go.opentelemetry.io/collector/component/componenttest v0.145.0/go.mod h1:5uStrhUdZ0Fw3se00CPmVaRtW8o9N8kKiY76OSCWFjQ=
go.opentelemetry.io/collector/client v1.54.0 h1:JDpDdc67n2LGVcDzMKN7fSsmmB7333g6d38LshTuXR0=
go.opentelemetry.io/collector/client v1.54.0/go.mod h1:4ODFLlgYmMEA+GNy96Qsn6Gi2PwFQFNUScvv5vVTyfE=
go.opentelemetry.io/collector/component v1.54.0 h1:LvtX0Tzz18n44OrUFVk77N1FNsejfWJqztB28hrmDM8=
go.opentelemetry.io/collector/component v1.54.0/go.mod h1:yUMBYsySY/sDcXm8kOzEoZxt+JLdala6hxzSW0npOxY=
go.opentelemetry.io/collector/component/componentstatus v0.148.0 h1:sCGRaXNQolHFhPjrNJEwQ1WZOf96iL99tzm9GxuZsvg=
go.opentelemetry.io/collector/component/componentstatus v0.148.0/go.mod h1:yqg3SpGQc22W3wGICdnb+2kZVW9daBr3+LrGUCHkKfc=
go.opentelemetry.io/collector/component/componenttest v0.148.0 h1:tBXJWmy2X6KD8S0QU2YZa2zYBqP+IycSM4iOtwDD2pA=
go.opentelemetry.io/collector/component/componenttest v0.148.0/go.mod h1:1c1+6mZOmI0raoya5vA/X0F+fawEjNS6tCEs5xLATtA=
go.opentelemetry.io/collector/config/configauth v1.50.0 h1:JhKAsRl392kxgtcl4juVdal2K9gm9MNWi4VNTq4kTTQ=
go.opentelemetry.io/collector/config/configauth v1.50.0/go.mod h1:Qrl+DDIryjjeScfUd0ZItz4bpQZstCrfGka3zdntTgM=
go.opentelemetry.io/collector/config/configcompression v1.50.0 h1:P/Y55nVvXO+tqKs9q/u5eX7gq3gWtZa9ab9YBpOIG34=
@@ -1210,28 +1210,28 @@ go.opentelemetry.io/collector/config/configtelemetry v0.144.0 h1:Jy7vM9fhaV38JjX
go.opentelemetry.io/collector/config/configtelemetry v0.144.0/go.mod h1:Xjw2+DpNLjYtx596EHSWBy0dNQRiJ2H+BlWU907lO40=
go.opentelemetry.io/collector/config/configtls v1.50.0 h1:2Uqc/RQ0Zf7cPu2pjkQrUbZ0/aop/dV8D1efRAPUTTQ=
go.opentelemetry.io/collector/config/configtls v1.50.0/go.mod h1:YA3AerzQnRg5FGJqqIWeWBV4PeCyjZ4XxU/sAdkgKxc=
go.opentelemetry.io/collector/confmap v1.51.0 h1:C9YlMNkIgzuauLpUz2F7DLlWwqAmkQKNcKj1XATVWuE=
go.opentelemetry.io/collector/confmap v1.51.0/go.mod h1:uWi4b9lHfvEC2poJ2I2vXwGUREVEQTcdUguOpfqdcHM=
go.opentelemetry.io/collector/confmap v1.54.0 h1:RUoxQ4uAYHTI57GfHh61D00tTQsXm9T88ozrAiicByc=
go.opentelemetry.io/collector/confmap v1.54.0/go.mod h1:mQxG8bk0IWIt9gbWMvzE+cRkOuCuzbzkNGBq2YJ4wNM=
go.opentelemetry.io/collector/confmap/provider/envprovider v1.50.0 h1:2byObKr1U37umeSgwSbkjJmXZ48UiUhKY9Gr9FAqfY0=
go.opentelemetry.io/collector/confmap/provider/envprovider v1.50.0/go.mod h1:+u4AvdhjSVcJEhZwj4D3WTLr4b4mBn1C0bklPqjf+Qo=
go.opentelemetry.io/collector/confmap/provider/fileprovider v1.50.0 h1:r1Dbs0p8M6z5/MMWXW1yMO0fHrZQVC9HlrTXK9CeIR4=
go.opentelemetry.io/collector/confmap/provider/fileprovider v1.50.0/go.mod h1:RerRpGv/rTHEU/1BQnAQCTM7jEDQDP8Fr3gWcYc1bcc=
go.opentelemetry.io/collector/confmap/xconfmap v0.145.0 h1:ngbyfh4+SKlA+osgsak3AxUNPxVxaJTmA0Sl7VfJzwY=
go.opentelemetry.io/collector/confmap/xconfmap v0.145.0/go.mod h1:zTSK+c76NAy/tI1R3xfZjdoI04D9EYDnzAHQQwl6AmA=
go.opentelemetry.io/collector/confmap/xconfmap v0.148.0 h1:UW8MX5VlKJf67x4Et7J9kPwP9Rv4VSmJ+UUpgRcb//c=
go.opentelemetry.io/collector/confmap/xconfmap v0.148.0/go.mod h1:4qTMr3V0uSXXac9wVs/UD5fIqRKw5yIl58+Vjsc6RHM=
go.opentelemetry.io/collector/connector v0.144.0 h1:R8gL2run29q0XLEn4drqyyhHpfkCUUGeAZjwOItO7JI=
go.opentelemetry.io/collector/connector v0.144.0/go.mod h1:t47rnR/pkChjtQGdutvY/QtnNArJMK/lQ6CJ8JsX9JM=
go.opentelemetry.io/collector/connector/connectortest v0.144.0 h1:fB8DRVeVlaoa1S4LacjWJom3R+el7XTOuMfHC4J3DpE=
go.opentelemetry.io/collector/connector/connectortest v0.144.0/go.mod h1:Z2hUnaV6s3mEpG7UQoFkS3yOgMfNkwf7T2yK7uwsRUo=
go.opentelemetry.io/collector/connector/xconnector v0.144.0 h1:/NKehHGx/poXWm9usc9iKSfmBLOUD8IQqjxne4ztbFo=
go.opentelemetry.io/collector/connector/xconnector v0.144.0/go.mod h1:tpDZhPdJaoNk9HQm/CTMut2iGFB365e0Aw+a0eh0njM=
go.opentelemetry.io/collector/consumer v1.51.0 h1:Ex1x/k9VEEA2DOgt/eSc2Z9KTp0I6xBSruLmrYFfIFY=
go.opentelemetry.io/collector/consumer v1.51.0/go.mod h1:Erk6qdfVj+24QTrGCpurcrF+qdUlHkb4dgMy5wJxLvY=
go.opentelemetry.io/collector/consumer v1.54.0 h1:RGGtUN+GbkV1px3T6XdUHmgJ+ldJ1hAHdesFzW/wgL0=
go.opentelemetry.io/collector/consumer v1.54.0/go.mod h1:1PC6XINTL9DdT1bwvfMdHE72EB4RWU/WcPemUrhqKN8=
go.opentelemetry.io/collector/consumer/consumererror v0.144.0 h1:bDnvbqp/FSyErSt60HQmDYXEDbWiav49H6m872zbHnw=
go.opentelemetry.io/collector/consumer/consumererror v0.144.0/go.mod h1:gODumKlgGfW9s5XVnL5dp+glXipaX+PSKX7W4x+FkFI=
go.opentelemetry.io/collector/consumer/consumertest v0.145.0 h1:3+uMwuMHoXMAU+Z6mwCRA3AxWeL7SujcAQwqqHJ1gCc=
go.opentelemetry.io/collector/consumer/consumertest v0.145.0/go.mod h1:IFc/FeaIHQClb8KK0aVn0tFDNMc+/MmfQ+aBT1cJNeo=
go.opentelemetry.io/collector/consumer/xconsumer v0.145.0 h1:9w7KKv9lVJoHvMLC6SUJHenU/KySdEgFJXbB4JQOEsk=
go.opentelemetry.io/collector/consumer/xconsumer v0.145.0/go.mod h1:SryDCLP2ZaFeZJtA2CSksJ0XvjH8k3LmlfXvy/kC7Wc=
go.opentelemetry.io/collector/consumer/consumertest v0.148.0 h1:ms0HtWMj17tI1Yds0hSuUI5QYpNEqd11AAhwIoUY2HE=
go.opentelemetry.io/collector/consumer/consumertest v0.148.0/go.mod h1:wScw/OzKkf/ZzJn4ToI30OoI1kJiY16WNrcFToXSzK0=
go.opentelemetry.io/collector/consumer/xconsumer v0.148.0 h1:m3b9rY7CLD5Pcge6sSKHIT3OlcPN6xqYsdtVs9oJ528=
go.opentelemetry.io/collector/consumer/xconsumer v0.148.0/go.mod h1:bG+Wz6xmIBl/gHzq1sqvksWXqTLuTX17Wo//zIsdZpw=
go.opentelemetry.io/collector/exporter v1.50.0 h1:CgzSk8+nVki5pAHe9F2LR0hn8U5OD6LEtyslQuwT52k=
go.opentelemetry.io/collector/exporter v1.50.0/go.mod h1:0JyxyYufP9G2puO72T68/10qXfbZvOSUFDla/yXyGQM=
go.opentelemetry.io/collector/exporter/exporterhelper v0.144.0 h1:Ea1N1MVaaBUrsolDFazVF1PiT+uD5I/cbKxl5ezXLmw=
@@ -1254,38 +1254,38 @@ go.opentelemetry.io/collector/extension/xextension v0.144.0 h1:Ax2g4BF/YzrFB0WDr
go.opentelemetry.io/collector/extension/xextension v0.144.0/go.mod h1:ZJkgXgS5ECu8d5AuPu+yoKJdx7BonE+bp1LrLxd3o6g=
go.opentelemetry.io/collector/extension/zpagesextension v0.144.0 h1:NUlimtqhNBFu8lxVbz2bUfUzBuzblYgAVK1b8pbnR44=
go.opentelemetry.io/collector/extension/zpagesextension v0.144.0/go.mod h1:js0E78S2CNQYQzjBnR1b8rrO0SWdWWKOst+p4q5ZSHM=
go.opentelemetry.io/collector/featuregate v1.51.0 h1:dxJuv/3T84dhNKp7fz5+8srHz1dhquGzDpLW4OZTFBw=
go.opentelemetry.io/collector/featuregate v1.51.0/go.mod h1:/1bclXgP91pISaEeNulRxzzmzMTm4I5Xih2SnI4HRSo=
go.opentelemetry.io/collector/internal/componentalias v0.145.0 h1:A9V5IiETzz8FCtjxjRM5gf7RE3sOtA1h8phmpQjXTZ4=
go.opentelemetry.io/collector/internal/componentalias v0.145.0/go.mod h1:sEKEAwAn45ZiXRk3T/vbkvetw14tIRd0CJIxcEx9SsQ=
go.opentelemetry.io/collector/featuregate v1.54.0 h1:ufo5Hy4Co9pcHVg24hyanm8qFG3TkkYbVyQXPVAbwDc=
go.opentelemetry.io/collector/featuregate v1.54.0/go.mod h1:PS7zY/zaCb28EqciePVwRHVhc3oKortTFXsi3I6ee4g=
go.opentelemetry.io/collector/internal/componentalias v0.148.0 h1:Y6MftNIZSzOr47TTj6A2z2UR3IwbeG46sAQshicGtDg=
go.opentelemetry.io/collector/internal/componentalias v0.148.0/go.mod h1:uwKzfehzwRgHxdHgFXYSBHNBeWSSqsqQYGWr5fk08G0=
go.opentelemetry.io/collector/internal/fanoutconsumer v0.144.0 h1:M0fyotX5iOvoz7dvi7gCJsjeQdvdDuwNS7H1F3hPC3s=
go.opentelemetry.io/collector/internal/fanoutconsumer v0.144.0/go.mod h1:5iHSWoZHrE4wyGobLjr7hpsAGiksPpMDSXwAOJuauIY=
go.opentelemetry.io/collector/internal/telemetry v0.144.0 h1:NnUHDHDwywKn7ZkO+mjHr8s7cD2vL0tcrLjjFO+Psfg=
go.opentelemetry.io/collector/internal/telemetry v0.144.0/go.mod h1:yuaOr03DjENw6F0uA47TzpqFiBkFBZe/dKLI+bhMsqM=
go.opentelemetry.io/collector/internal/testutil v0.145.0 h1:H/KL0GH3kGqSMKxZvnQ0B0CulfO9xdTg4DZf28uV7fY=
go.opentelemetry.io/collector/internal/testutil v0.145.0/go.mod h1:YAD9EAkwh/l5asZNbEBEUCqEjoL1OKMjAMoPjPqH76c=
go.opentelemetry.io/collector/internal/testutil v0.148.0 h1:3Z9hperte3vSmbBTYeNndoEUICICrNz8hzx+v0FYXBQ=
go.opentelemetry.io/collector/internal/testutil v0.148.0/go.mod h1:Jkjs6rkqs973LqgZ0Fe3zrokQRKULYXPIf4HuqStiEE=
go.opentelemetry.io/collector/otelcol v0.144.0 h1:D2W8L2C3r7wgg28gTIrYEN2jhRpVnu4Nfqz5qL2bMd4=
go.opentelemetry.io/collector/otelcol v0.144.0/go.mod h1:auSP8QcnkNsUimAgCV3S78hZeuKh1CTIMopPoWcVGV0=
go.opentelemetry.io/collector/pdata v1.51.0 h1:DnDhSEuDXNdzGRB7f6oOfXpbDApwBX3tY+3K69oUrDA=
go.opentelemetry.io/collector/pdata v1.51.0/go.mod h1:GoX1bjKDR++mgFKdT7Hynv9+mdgQ1DDXbjs7/Ww209Q=
go.opentelemetry.io/collector/pdata/pprofile v0.145.0 h1:ASMKpoqokf8HhzjoeMKZf0K6UXLhufVwNXH0sSuUn5w=
go.opentelemetry.io/collector/pdata/pprofile v0.145.0/go.mod h1:a60GC7wQPhLAixWzKbbP51QLwwc+J0Cmp4SurOlhGUk=
go.opentelemetry.io/collector/pdata/testdata v0.145.0 h1:iFsxsCMtE3lnAc/5kZbhZHpRv1OMmM+O5ry46xdQHbg=
go.opentelemetry.io/collector/pdata/testdata v0.145.0/go.mod h1:0y2ERArdzqmYdJHdKLKue+AUubSEGlwK49F+23+Mbic=
go.opentelemetry.io/collector/pdata/xpdata v0.144.0 h1:83Eei0VYbGyThHB5BRBwGUMLZSePShjse2eHgm41NIM=
go.opentelemetry.io/collector/pdata/xpdata v0.144.0/go.mod h1:uKSjEHBBIKAx0udPjB40+xR4sUAhfnfzKfpWz+nIzik=
go.opentelemetry.io/collector/pipeline v1.51.0 h1:GZBNW+aaOE+zufGzAkXy0OI7n1cqepEa5J+beaOpS2k=
go.opentelemetry.io/collector/pipeline v1.51.0/go.mod h1:xUrAqiebzYbrgxyoXSkk6/Y3oi5Sy3im2iCA51LwUAI=
go.opentelemetry.io/collector/pdata v1.54.0 h1:3LharKb792cQ3VrUGxd3IcpWwfu3ST+GSTU382jVz1s=
go.opentelemetry.io/collector/pdata v1.54.0/go.mod h1:+MqC3VVOv/EX9YVFUo+mI4F0YmwJ+fXBYwjmu+mRiZ8=
go.opentelemetry.io/collector/pdata/pprofile v0.148.0 h1:MgrNZmqwhZGfiYwcKKtM/iXgTZqqvG5dUphriRXMZHU=
go.opentelemetry.io/collector/pdata/pprofile v0.148.0/go.mod h1:MTTMnZPqWX1S/rBDatU0W19udlycBkWuzVV5qnemHdc=
go.opentelemetry.io/collector/pdata/testdata v0.148.0 h1:yzakPuFgoKK8WcrlhyYHLMLA/kLScQKGsXkIgwieAQ8=
go.opentelemetry.io/collector/pdata/testdata v0.148.0/go.mod h1:2rFvxm8qwd3nlO90FtJw6ZGAjt+bLndxmQuJaMO9kfQ=
go.opentelemetry.io/collector/pdata/xpdata v0.148.0 h1:pTXz872QDl5oHByjlIEkQhIFvv0oeX/5cKNWsUg9KeY=
go.opentelemetry.io/collector/pdata/xpdata v0.148.0/go.mod h1:4iL8wugmu589aQNx0dFVT3Ecui/d3TEvVgMlAu8S//0=
go.opentelemetry.io/collector/pipeline v1.54.0 h1:jYlCkdFLITVBdeB+IGS07zXWywEgvT3Ky46vdKKT+Ks=
go.opentelemetry.io/collector/pipeline v1.54.0/go.mod h1:RD90NG3Jbk965Xaqym3JyHkuol4uZJjQVUkD9ddXJIs=
go.opentelemetry.io/collector/pipeline/xpipeline v0.144.0 h1:KoEWLrK7+qps+eo6paHpRWQat4FX1jy7XArrgOQoCXY=
go.opentelemetry.io/collector/pipeline/xpipeline v0.144.0/go.mod h1:2/giOwggQfWb6NY7shJe7Y/DjpKFsAD2m2PX3POuVnI=
go.opentelemetry.io/collector/processor v1.51.0 h1:PKpCzkLQmqaW08TOVh/zM0qx07Ihq+DR5J/OBkPiL9o=
go.opentelemetry.io/collector/processor v1.51.0/go.mod h1:rtIPFS+EFRAkG+CSwtjxs2IsIkuZStObvALeueD02XI=
go.opentelemetry.io/collector/processor v1.54.0 h1:zmHBFiEFmU9ZYuHhVP3lHIkbfy+ueapzGpTdXVMcWBg=
go.opentelemetry.io/collector/processor v1.54.0/go.mod h1:L0lA6DZ0VbrtQBg44cmYfSpRlgm4zxW1I6QfBnRizPw=
go.opentelemetry.io/collector/processor/processorhelper v0.144.0 h1:DZef7rGngEcy3ZuJ3zb4BdOAxK7xrYBm1pQu/zoWGA4=
go.opentelemetry.io/collector/processor/processorhelper v0.144.0/go.mod h1:B6lbjKY3t4UMjinR/sZWa6I9pwkObXOojqujVS79CeU=
go.opentelemetry.io/collector/processor/processortest v0.145.0 h1:RDGBmyZnHk7XVK/EdLt/8iPWj+QLStbbVi1nFTNR01s=
go.opentelemetry.io/collector/processor/processortest v0.145.0/go.mod h1:WAvxAzSojkdoZB915Z1lsVHCPDJBb2fepjJBjenrzjg=
go.opentelemetry.io/collector/processor/xprocessor v0.145.0 h1:DaIE7MxRlg0OL1o2P0GQZtmZeExAmVso3qWv8S0RLps=
go.opentelemetry.io/collector/processor/xprocessor v0.145.0/go.mod h1:kUwRyKBU/kjCmXodd+0z7CpvcP0A9G9/QL+MaJt4U2o=
go.opentelemetry.io/collector/processor/processortest v0.148.0 h1:p0k59frZxy/Z4fXe82i5eOJv/UyOH75XhI8nFD1ZWCE=
go.opentelemetry.io/collector/processor/processortest v0.148.0/go.mod h1:E2Li2gnkUXgvApvGyEtn3Eq5KyzV05ljfbFRsZ7sTC4=
go.opentelemetry.io/collector/processor/xprocessor v0.148.0 h1:v7Qv6k2b2cvgGWuTO5KN5QYDLl1r5sznt7Le4Fhpa4c=
go.opentelemetry.io/collector/processor/xprocessor v0.148.0/go.mod h1:r7ADpSX2nf0rZR9STxh956Qw1740QOWMXLnEM/ZiaF8=
go.opentelemetry.io/collector/receiver v1.50.0 h1:X6FDV7j0vf/9jm1+OIiUknj0LLBNvsKHQFXS42hKRzg=
go.opentelemetry.io/collector/receiver v1.50.0/go.mod h1:dPkxXydTdFHIYkPqHKPastKVzsRH6vCMkMEsguKMlKA=
go.opentelemetry.io/collector/receiver/receiverhelper v0.144.0 h1:AMCVnHOR+fBHdeH0GZ4coJ2haG7xGwVgsP5p/NV2Ok8=
@@ -1308,10 +1308,10 @@ go.opentelemetry.io/contrib/config v0.10.0 h1:2JknAzMaYjxrHkTnZh3eOme/Y2P5eHE2SW
go.opentelemetry.io/contrib/config v0.10.0/go.mod h1:aND2M6/KfNkntI5cyvHriR/zvZgPf8j9yETdSmvpfmc=
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.63.0 h1:rATLgFjv0P9qyXQR/aChJ6JVbMtXOQjt49GgT36cBbk=
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.63.0/go.mod h1:34csimR1lUhdT5HH4Rii9aKPrvBcnFRwxLwcevsU+Kk=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.65.0 h1:ab5U7DpTjjN8pNgwqlA/s0Csb+N2Raqo9eTSDhfg4Z8=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.65.0/go.mod h1:nwFJC46Dxhqz5R9k7IV8To/Z46JPvW+GNKhTxQQlUzg=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.67.0 h1:c9r/G1CSw4dPI1jaNNG9RnQP+q4SvZnHciDQJVIvchU=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.67.0/go.mod h1:gO9smoZe9KnZcJCqcB0lMmQ4Z5VEifYmjMTpnwtTSuQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
go.opentelemetry.io/contrib/otelconf v0.18.0 h1:ciF2Gf00BWs0DnexKFZXcxg9kJ8r3SUW1LOzW3CsKA8=
go.opentelemetry.io/contrib/otelconf v0.18.0/go.mod h1:FcP7k+JLwBLdOxS6qY6VQ/4b5VBntI6L6o80IMwhAeI=
go.opentelemetry.io/contrib/propagators/b3 v1.39.0 h1:PI7pt9pkSnimWcp5sQhUA9OzLbc3Ba4sL+VEUTNsxrk=
@@ -1330,8 +1330,8 @@ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo=
@@ -1361,12 +1361,12 @@ go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLh
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.opentelemetry.io/proto/slim/otlp v1.9.0 h1:fPVMv8tP3TrsqlkH1HWYUpbCY9cAIemx184VGkS6vlE=
go.opentelemetry.io/proto/slim/otlp v1.9.0/go.mod h1:xXdeJJ90Gqyll+orzUkY4bOd2HECo5JofeoLpymVqdI=
go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.2.0 h1:o13nadWDNkH/quoDomDUClnQBpdQQ2Qqv0lQBjIXjE8=
go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.2.0/go.mod h1:Gyb6Xe7FTi/6xBHwMmngGoHqL0w29Y4eW8TGFzpefGA=
go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.2.0 h1:EiUYvtwu6PMrMHVjcPfnsG3v+ajPkbUeH+IL93+QYyk=
go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.2.0/go.mod h1:mUUHKFiN2SST3AhJ8XhJxEoeVW12oqfXog0Bo8W3Ec4=
go.opentelemetry.io/proto/slim/otlp v1.10.0 h1:iR97Vs/ZDR+y9TfuP9b1XBtdPWeC+OMslIBmhcLU7jM=
go.opentelemetry.io/proto/slim/otlp v1.10.0/go.mod h1:lV9250stpjYLPNA5viFabIgP2QlUGRT1GdTgAf8SIUk=
go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.3.0 h1:RUF5rO0hAlgiJt1fzQVzcVs3vZVNHIcMLgOgG4rWNcQ=
go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.3.0/go.mod h1:I89cynRj8y+383o7tEQVg2SVA6SRgDVIouWPUVXjx0U=
go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.3.0 h1:CQvJSldHRUN6Z8jsUeYv8J0lXRvygALXIzsmAeCcZE0=
go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.3.0/go.mod h1:xSQ+mEfJe/GjK1LXEyVOoSI1N9JV9ZI923X5kup43W4=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
@@ -1383,8 +1383,8 @@ go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
@@ -1412,8 +1412,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -1511,8 +1511,8 @@ golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1634,8 +1634,8 @@ golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -1744,8 +1744,8 @@ google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRR
google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko=
google.golang.org/api v0.265.0 h1:FZvfUdI8nfmuNrE34aOWFPmLC+qRBEiNm3JdivTvAAU=
google.golang.org/api v0.265.0/go.mod h1:uAvfEl3SLUj/7n6k+lJutcswVojHPp2Sp08jWCu8hLY=
google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA=
google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -1833,8 +1833,8 @@ google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX
google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d h1:vsOm753cOAMkt76efriTCDKjpCbK18XGHMJHo0JUKhc=
google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:0oz9d7g9QLSdv9/lgbIjowW1JoxMbxmBVNe8i6tORJI=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
@@ -1930,14 +1930,14 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
k8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw=
k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60=
k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8=
k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o=
k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/api v0.35.3 h1:pA2fiBc6+N9PDf7SAiluKGEBuScsTzd2uYBkA5RzNWQ=
k8s.io/api v0.35.3/go.mod h1:9Y9tkBcFwKNq2sxwZTQh1Njh9qHl81D0As56tu42GA4=
k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8=
k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
k8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg=
k8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c=
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=

View File

@@ -20,6 +20,7 @@ type provider struct {
settings factory.ScopedProviderSettings
telemetryStore telemetrystore.TelemetryStore
engine *prometheus.Engine
parser prometheus.Parser
queryable storage.SampleAndChunkQueryable
}
@@ -38,6 +39,7 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
settings: settings,
telemetryStore: telemetryStore,
engine: prometheus.NewEngine(settings.Logger(), config),
parser: prometheus.NewParser(),
queryable: remote.NewSampleAndChunkQueryableClient(readClient, labels.EmptyLabels(), []*labels.Matcher{}, false, stCallback),
}, nil
}
@@ -46,6 +48,10 @@ func (provider *provider) Engine() *prometheus.Engine {
return provider.engine
}
func (provider *provider) Parser() prometheus.Parser {
return provider.parser
}
func (provider *provider) Storage() storage.Queryable {
return provider
}

9
pkg/prometheus/parser.go Normal file
View File

@@ -0,0 +1,9 @@
package prometheus
import (
"github.com/prometheus/prometheus/promql/parser"
)
func NewParser() Parser {
return parser.NewParser(parser.Options{})
}

View File

@@ -2,12 +2,16 @@ package prometheus
import (
"github.com/prometheus/prometheus/promql"
"github.com/prometheus/prometheus/promql/parser"
"github.com/prometheus/prometheus/storage"
)
type Engine = promql.Engine
type Parser = parser.Parser
type Prometheus interface {
Engine() *Engine
Storage() storage.Queryable
Parser() Parser
}

View File

@@ -18,6 +18,7 @@ var _ prometheus.Prometheus = (*Provider)(nil)
type Provider struct {
queryable storage.SampleAndChunkQueryable
engine *prometheus.Engine
parser prometheus.Parser
}
var stCallback = func() (int64, error) {
@@ -36,6 +37,7 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
return &Provider{
engine: engine,
parser: prometheus.NewParser(),
queryable: queryable,
}
}
@@ -48,6 +50,10 @@ func (provider *Provider) Storage() storage.Queryable {
return provider.queryable
}
func (provider *Provider) Parser() prometheus.Parser {
return provider.parser
}
func (provider *Provider) Close() error {
if provider.engine != nil {
provider.engine.Close()

View File

@@ -93,6 +93,7 @@ func enhancePromQLError(query string, parseErr error) error {
type promqlQuery struct {
logger *slog.Logger
promEngine prometheus.Prometheus
parser parser.Parser
query qbv5.PromQuery
tr qbv5.TimeRange
requestType qbv5.RequestType
@@ -109,7 +110,15 @@ func newPromqlQuery(
requestType qbv5.RequestType,
variables map[string]qbv5.VariableItem,
) *promqlQuery {
return &promqlQuery{logger, promEngine, query, tr, requestType, variables}
return &promqlQuery{
logger: logger,
promEngine: promEngine,
parser: promEngine.Parser(),
query: query,
tr: tr,
requestType: requestType,
vars: variables,
}
}
func (q *promqlQuery) Fingerprint() string {
@@ -150,7 +159,7 @@ func (q *promqlQuery) removeAllVarMatchers(query string, vars map[string]qbv5.Va
return query, nil
}
expr, err := parser.ParseExpr(query)
expr, err := q.parser.ParseExpr(query)
if err != nil {
return "", enhancePromQLError(query, err)
}

View File

@@ -6,13 +6,14 @@ import (
"testing"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/prometheus"
qbv5 "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/stretchr/testify/assert"
)
func TestRemoveAllVarMatchers(t *testing.T) {
logger := slog.Default()
q := &promqlQuery{logger: logger}
q := &promqlQuery{logger: logger, parser: prometheus.NewParser()}
tests := []struct {
name string

View File

@@ -1156,9 +1156,11 @@ func (r *ClickHouseReader) GetFlamegraphSpansForTrace(ctx context.Context, orgID
r.logger.Info("cache miss for getFlamegraphSpansForTrace", "traceID", traceID)
selectCols := "timestamp, duration_nano, span_id, trace_id, has_error, links as references, resource_string_service$$name, name, events"
if len(req.SelectFields) > 0 {
selectCols += ", attributes_string, attributes_number, attributes_bool, resources_string"
selectFieldCols := req.GetSelectedFieldsSourceColumns()
if len(selectFieldCols) > 0 {
selectCols = fmt.Sprintf("%s, %s", selectCols, strings.Join(selectFieldCols, ", "))
}
flamegraphQuery := fmt.Sprintf("SELECT %s FROM %s.%s WHERE trace_id=$1 and ts_bucket_start>=$2 and ts_bucket_start<=$3 ORDER BY timestamp ASC, name ASC", selectCols, r.TraceDB, r.traceTableName)
searchScanResponses, err := r.GetSpansForTrace(ctx, traceID, flamegraphQuery)

View File

@@ -285,7 +285,7 @@ func (q *querier) ValidateMetricNames(ctx context.Context, query *v3.CompositeQu
switch query.QueryType {
case v3.QueryTypePromQL:
for _, query := range query.PromQueries {
expr, err := parser.ParseExpr(query.Query)
expr, err := q.parser.ParseExpr(query.Query)
if err != nil {
q.logger.DebugContext(ctx, "error parsing promql expression", "query", query.Query, errors.Attr(err))
continue

View File

@@ -7,6 +7,8 @@ import (
"sync"
"time"
"github.com/SigNoz/signoz/pkg/prometheus"
logsV4 "github.com/SigNoz/signoz/pkg/query-service/app/logs/v4"
metricsV4 "github.com/SigNoz/signoz/pkg/query-service/app/metrics/v4"
"github.com/SigNoz/signoz/pkg/query-service/app/queryBuilder"
@@ -45,6 +47,7 @@ type querier struct {
builder *queryBuilder.QueryBuilder
parser prometheus.Parser
logger *slog.Logger
// used for testing
@@ -88,6 +91,7 @@ func NewQuerier(opts QuerierOptions) interfaces.Querier {
BuildMetricQuery: metricsV4.PrepareMetricQuery,
}),
parser: prometheus.NewParser(),
logger: slog.Default(),
testingMode: opts.TestingMode,

View File

@@ -346,6 +346,45 @@ type GetFlamegraphSpansForTraceParams struct {
SelectFields []telemetrytypes.TelemetryFieldKey `json:"selectFields"`
}
func (r *GetFlamegraphSpansForTraceParams) GetSelectedFieldsSourceColumns() []string {
var needsAttrString, needsAttrNumber, needsAttrBool, needsResourceMap bool
for _, f := range r.SelectFields {
switch f.FieldContext {
case telemetrytypes.FieldContextAttribute:
switch f.FieldDataType {
case telemetrytypes.FieldDataTypeString:
needsAttrString = true
case telemetrytypes.FieldDataTypeFloat64, telemetrytypes.FieldDataTypeNumber, telemetrytypes.FieldDataTypeInt64:
needsAttrNumber = true
case telemetrytypes.FieldDataTypeBool:
needsAttrBool = true
default:
// Unknown type: AttributeValue searches all three maps, so we need all.
needsAttrString = true
needsAttrNumber = true
needsAttrBool = true
}
case telemetrytypes.FieldContextResource:
needsResourceMap = true
}
}
var cols []string
if needsAttrString {
cols = append(cols, "attributes_string")
}
if needsAttrNumber {
cols = append(cols, "attributes_number")
}
if needsAttrBool {
cols = append(cols, "attributes_bool")
}
if needsResourceMap {
cols = append(cols, "resources_string")
}
return cols
}
type SpanFilterParams struct {
TraceID []string `json:"traceID"`
Status []string `json:"status"`

View File

@@ -4,22 +4,27 @@ import (
"sort"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/prometheus/common/model"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/promql/parser"
)
// PromQLFilterExtractor extracts metric names and grouping keys from PromQL queries.
type PromQLFilterExtractor struct{}
type PromQLFilterExtractor struct {
parser parser.Parser
}
// NewPromQLFilterExtractor creates a new PromQL filter extractor.
func NewPromQLFilterExtractor() *PromQLFilterExtractor {
return &PromQLFilterExtractor{}
return &PromQLFilterExtractor{
parser: prometheus.NewParser(),
}
}
// Extract parses a PromQL query and extracts metric names and grouping keys.
func (e *PromQLFilterExtractor) Extract(query string) (*FilterResult, error) {
expr, err := parser.ParseExpr(query)
expr, err := e.parser.ParseExpr(query)
if err != nil {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to parse promql query: %s", err.Error())
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"regexp"
"strings"
"time"
"github.com/SigNoz/signoz/pkg/errors"
@@ -26,7 +27,8 @@ var (
)
var (
roleNameRegex = regexp.MustCompile("^[a-z-]{1,50}$")
roleNameRegex = regexp.MustCompile("^[a-z-]{1,50}$")
managedRolePrefix = "signoz"
)
var (
@@ -140,7 +142,11 @@ func (role *PostableRole) UnmarshalJSON(data []byte) error {
}
if match := roleNameRegex.MatchString(shadowRole.Name); !match {
return errors.Newf(errors.TypeInvalidInput, ErrCodeRoleInvalidInput, "name must conform to the regex: %s", roleNameRegex.String())
return errors.New(errors.TypeInvalidInput, ErrCodeRoleInvalidInput, "name must contain only lowercase letters (a-z) and hyphens (-), and be at most 50 characters long.")
}
if strings.HasPrefix(shadowRole.Name, managedRolePrefix) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeRoleInvalidInput, "role name cannot start with %q as it is reserved for SigNoz managed roles.", managedRolePrefix)
}
role.Name = shadowRole.Name

View File

@@ -49,7 +49,29 @@ def test_apply_license(
# ---------------------------------------------------------------------------
# 2. Create custom role + user with read/list on roles
# 2. Reject role names starting with "signoz-"
# ---------------------------------------------------------------------------
def test_create_role_with_signoz_prefix_rejected(
signoz: types.SigNoz,
create_user_admin: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
for name in ("signoz-custom", "signozcustom"):
resp = requests.post(
signoz.self.host_configs["8080"].get(ROLES_BASE),
json={"name": name},
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert resp.status_code == HTTPStatus.BAD_REQUEST, f"expected 400 for role name '{name}', got {resp.status_code}: {resp.text}"
# ---------------------------------------------------------------------------
# 3. Create custom role + user with read/list on roles
# ---------------------------------------------------------------------------

6
tests/uv.lock generated
View File

@@ -1126,11 +1126,11 @@ wheels = [
[[package]]
name = "urllib3"
version = "2.6.3"
version = "2.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
]
[package.optional-dependencies]