Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Add RBAC to Synthetic Monitoring #982

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
69f6650
feat: define new roles
VikaCep Nov 4, 2024
b68d7a8
feat: add role requirements for routes and pages
VikaCep Nov 4, 2024
493f2cb
fix: add plugin access permission for all roles
VikaCep Nov 12, 2024
db6353c
fix: configure general read/write permissions for ds plugin.json config
VikaCep Nov 12, 2024
372cd17
fix: remove redundant granted roles
VikaCep Nov 13, 2024
0c84cb6
fix: improve names and remove token writer access for editors
VikaCep Nov 13, 2024
f7a46b6
fix: rename edit permission
VikaCep Nov 13, 2024
737c03c
fix: rename tokens to access-tokens
VikaCep Nov 19, 2024
f78d04a
fix: restrict config page to writers
VikaCep Nov 19, 2024
7cc7424
fix: change required permissions to register a ds
VikaCep Nov 19, 2024
8b41cd5
fix: change access-token create for write to match convention
VikaCep Nov 19, 2024
33fc3f4
fix: add missing threshold: delete permission in Threshold Writer role
VikaCep Nov 25, 2024
f25ab15
fix: change required permissions to see SM home page
VikaCep Nov 27, 2024
24f9857
RBAC: enforce permissions in frontend using user roles (#986)
VikaCep Nov 28, 2024
af5f546
fix: lint
VikaCep Nov 28, 2024
129edc3
fix: add generic UnauthorizedPage and enforce permissions on home and…
VikaCep Nov 28, 2024
7939b87
fix: display missing write alerts permission
VikaCep Nov 28, 2024
c25791c
fix: lint
VikaCep Nov 28, 2024
a63e390
fix: consolidate enable/disable plugin actions into a single write one
VikaCep Nov 28, 2024
610133e
fix: lint
VikaCep Nov 28, 2024
ac4bfcb
fix: prevent displaying missing write message to readers
VikaCep Nov 28, 2024
387d0e5
fix: addressing review comments
VikaCep Nov 29, 2024
3e8822e
chore: remove ConfigActions as not user after rebase with main
VikaCep Nov 29, 2024
6d8702a
fix: add message when missing access token write permission
VikaCep Nov 29, 2024
2f0fa83
fix: remove Access Tokens Reader role
VikaCep Nov 29, 2024
3a0e12a
fix: remove access-tokens: read and delete actions
VikaCep Nov 29, 2024
672adec
fix: list missing permissions to initialize plugin
VikaCep Nov 29, 2024
c9437ba
fix: change Unauthorized page layout
VikaCep Nov 29, 2024
1e36e5a
fix: restrict terraform access
VikaCep Nov 29, 2024
955cd9c
fix: lint
VikaCep Nov 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions src/components/AddNewCheckButton/AddNewCheckButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@ import React from 'react';
import { Button } from '@grafana/ui';

import { ROUTES } from 'routing/types';
import { useCanWriteSM } from 'hooks/useDSPermission';
import { getUserPermissions } from 'data/permissions';
import { useNavigation } from 'hooks/useNavigation';

export function AddNewCheckButton() {
const navigate = useNavigation();
const canEdit = useCanWriteSM();

if (!canEdit) {
return null;
}
const { canWriteChecks } = getUserPermissions();

return (
<Button variant="primary" onClick={() => navigate(ROUTES.ChooseCheckGroup)} type="button">
<Button
variant="primary"
onClick={() => navigate(ROUTES.ChooseCheckGroup)}
type="button"
disabled={!canWriteChecks}
>
Add new check
</Button>
);
Expand Down
15 changes: 11 additions & 4 deletions src/components/AppInitializer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import { DataTestIds } from 'test/dataTestIds';

import { hasGlobalPermission } from 'utils';
import { ROUTES } from 'routing/types';
import { getUserPermissions } from 'data/permissions';
import { useAppInitializer } from 'hooks/useAppInitializer';
import { useMeta } from 'hooks/useMeta';
import { MismatchedDatasourceModal } from 'components/MismatchedDatasourceModal';
import { ContactAdminAlert } from 'page/ContactAdminAlert';

interface Props {
redirectTo?: ROUTES;
Expand All @@ -19,7 +21,10 @@ interface Props {
export const AppInitializer = ({ redirectTo, buttonText }: PropsWithChildren<Props>) => {
const { jsonData } = useMeta();
const styles = useStyles2(getStyles);
const canInitialize = hasGlobalPermission(`datasources:create`);
const { canWritePlugin } = getUserPermissions();

const canReadDs = hasGlobalPermission(`datasources:read`);
const canInitialize = canWritePlugin && hasGlobalPermission(`datasources:create`);

const {
error,
Expand All @@ -35,11 +40,13 @@ export const AppInitializer = ({ redirectTo, buttonText }: PropsWithChildren<Pro
setDataSouceModalOpen,
} = useAppInitializer(redirectTo);

if (!canReadDs) {
return <ContactAdminAlert missingPermissions={['datasources:read']} />;
}

if (!canInitialize) {
return (
<Alert title="" severity="info">
Contact your administrator to get you started.
</Alert>
<ContactAdminAlert missingPermissions={['grafana-synthetic-monitoring-app.plugin:write', 'datasources:create']} />
);
}

Expand Down
7 changes: 4 additions & 3 deletions src/components/CheckForm/CheckForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import { createNavModel } from 'utils';
import { ROUTES } from 'routing/types';
import { generateRoutePath } from 'routing/utils';
import { AdHocCheckResponse } from 'datasource/responses.types';
import { getUserPermissions } from 'data/permissions';
import { useCheckTypeGroupOption } from 'hooks/useCheckTypeGroupOptions';
import { useCheckTypeOptions } from 'hooks/useCheckTypeOptions';
import { useCanReadLogs, useCanWriteSM } from 'hooks/useDSPermission';
import { useCanReadLogs } from 'hooks/useDSPermission';
import { useLimits } from 'hooks/useLimits';
import { toFormValues } from 'components/CheckEditor/checkFormTransformations';
import { CheckJobName } from 'components/CheckEditor/FormComponents/CheckJobName';
Expand Down Expand Up @@ -72,7 +73,7 @@ type CheckFormProps = {
};

export const CheckForm = ({ check, disabled }: CheckFormProps) => {
const canEdit = useCanWriteSM();
const { canWriteChecks } = getUserPermissions();
const canReadLogs = useCanReadLogs();
const [openTestCheckModal, setOpenTestCheckModal] = useState(false);
const [adhocTestData, setAdhocTestData] = useState<AdHocCheckResponse>();
Expand All @@ -90,7 +91,7 @@ export const CheckForm = ({ check, disabled }: CheckFormProps) => {
isOverCheckLimit ||
(checkType === CheckType.Browser && isOverBrowserLimit) ||
([CheckType.MULTI_HTTP, CheckType.Scripted].includes(checkType) && isOverScriptedLimit);
const isDisabled = disabled || !canEdit || getLimitDisabled({ isExistingCheck, isLoading, overLimit });
const isDisabled = disabled || !canWriteChecks || getLimitDisabled({ isExistingCheck, isLoading, overLimit });

const formMethods = useForm<CheckFormValues>({
defaultValues: toFormValues(initialCheck, checkType),
Expand Down
61 changes: 0 additions & 61 deletions src/components/ConfigActions.test.tsx

This file was deleted.

59 changes: 0 additions & 59 deletions src/components/ConfigActions.tsx

This file was deleted.

10 changes: 6 additions & 4 deletions src/components/DeleteProbeButton/DeleteProbeButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ export function DeleteProbeButton({ probe, onDeleteSuccess: _onDeleteSuccess }:
}, [_onDeleteSuccess]);

const { mutateAsync: deleteProbe, isPending } = useDeleteProbe({ onSuccess: onDeleteSuccess });
const canEdit = useCanEditProbe(probe);
const canDelete = canEdit && !probe.checks.length;

const { canDeleteProbes } = useCanEditProbe();

const canDelete = canDeleteProbes && !probe.checks.length;
const styles = getStyles();
const [error, setError] = useState<undefined | { name: string; message: string }>();

Expand All @@ -37,7 +39,7 @@ export function DeleteProbeButton({ probe, onDeleteSuccess: _onDeleteSuccess }:
};

if (!canDelete) {
const tooltipContent = canEdit ? (
const tooltipContent = canDeleteProbes ? (
<>
Unable to delete the probe because it is currently in use.
<br />
Expand All @@ -53,7 +55,7 @@ export function DeleteProbeButton({ probe, onDeleteSuccess: _onDeleteSuccess }:

// Both tooltip component and button prob is used for accessibility reasons
return (
<Tooltip content={tooltipContent} interactive={canEdit && !canDelete}>
<Tooltip content={tooltipContent} interactive={canDeleteProbes && !canDelete}>
<Button type="button" variant="destructive" tooltip={tooltipContent} disabled>
Delete probe
</Button>
Expand Down
7 changes: 4 additions & 3 deletions src/components/LinkedDatasourceView.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react';
import { Alert, Card, Tag } from '@grafana/ui';

import { useCanWriteLogs, useCanWriteMetrics, useCanWriteSM } from 'hooks/useDSPermission';
import { getUserPermissions } from 'data/permissions';
import { useCanWriteLogs, useCanWriteMetrics } from 'hooks/useDSPermission';
import { useLogsDS } from 'hooks/useLogsDS';
import { useMetricsDS } from 'hooks/useMetricsDS';
import { useSMDS } from 'hooks/useSMDS';
Expand All @@ -15,14 +16,14 @@ export const LinkedDatasourceView = ({ type }: LinkedDatasourceViewProps) => {
const logsDS = useLogsDS();
const smDS = useSMDS();

const canEditSM = useCanWriteSM();
const { canWriteSM } = getUserPermissions();
const canEditLogs = useCanWriteLogs();
const canEditMetrics = useCanWriteMetrics();

const canEditMap = {
prometheus: canEditMetrics,
loki: canEditLogs,
'synthetic-monitoring-datasource': canEditSM,
'synthetic-monitoring-datasource': canWriteSM,
};

const dsMap = {
Expand Down
14 changes: 13 additions & 1 deletion src/components/ProbeCard/ProbeCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { userEvent } from '@testing-library/user-event';
import { DataTestIds } from 'test/dataTestIds';
import { OFFLINE_PROBE, ONLINE_PROBE, PRIVATE_PROBE, PUBLIC_PROBE } from 'test/fixtures/probes';
import { render } from 'test/render';
import { probeToExtendedProbe, runTestAsViewer } from 'test/utils';
import { probeToExtendedProbe, runTestAsRBACReader, runTestAsViewer } from 'test/utils';

import { type ExtendedProbe } from 'types';
import { ROUTES } from 'routing/types';
Expand Down Expand Up @@ -92,6 +92,18 @@ it(`Displays the correct information for a private probe as a viewer`, async ()
expect(button).toHaveTextContent('View');
});

it(`Displays the correct information for a private probe as a RBAC viewer`, async () => {
runTestAsRBACReader();
const probe = probeToExtendedProbe(PRIVATE_PROBE);

render(<ProbeCard probe={probe} />);
await screen.findByText(probe.name, { exact: false });

const button = screen.getByTestId('probe-card-action-button');
expect(button).toBeInTheDocument();
expect(button).toHaveTextContent('View');
});

it(`Displays the correct information for a public probe`, async () => {
const probe = probeToExtendedProbe(PUBLIC_PROBE);

Expand Down
6 changes: 3 additions & 3 deletions src/components/ProbeCard/ProbeCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import { ProbeLabels } from './ProbeLabels';
import { ProbeStatus } from './ProbeStatus';

export const ProbeCard = ({ probe }: { probe: ExtendedProbe }) => {
const canEdit = useCanEditProbe(probe);
const probeEditHref = generateRoutePath(canEdit ? ROUTES.EditProbe : ROUTES.ViewProbe, { id: probe.id! });
const { canWriteProbes } = useCanEditProbe(probe);
const probeEditHref = generateRoutePath(canWriteProbes ? ROUTES.EditProbe : ROUTES.ViewProbe, { id: probe.id! });
const labelsString = labelsToString(probe.labels);
const styles = useStyles2(getStyles2);

Expand Down Expand Up @@ -55,7 +55,7 @@ export const ProbeCard = ({ probe }: { probe: ExtendedProbe }) => {
</Card.Description>

<Card.Actions>
{canEdit ? (
{canWriteProbes ? (
<>
<LinkButton
data-testid="probe-card-action-button"
Expand Down
14 changes: 13 additions & 1 deletion src/components/ProbeEditor/ProbeEditor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { config } from '@grafana/runtime';
import { screen } from '@testing-library/react';
import { PRIVATE_PROBE, PUBLIC_PROBE } from 'test/fixtures/probes';
import { render } from 'test/render';
import { fillProbeForm, probeToExtendedProbe, runTestAsViewer, UPDATED_VALUES } from 'test/utils';
import { fillProbeForm, probeToExtendedProbe, runTestAsRBACReader, runTestAsViewer, UPDATED_VALUES } from 'test/utils';

import { ExtendedProbe, FeatureName, Probe } from 'types';
import { TEMPLATE_PROBE } from 'page/NewProbe';
Expand Down Expand Up @@ -108,6 +108,12 @@ it('the form is uneditable when logged in as a viewer', async () => {
await assertUneditable();
});

it('the form is uneditable when logged in as a RBAC viewer', async () => {
runTestAsRBACReader();
await renderProbeEditor();
await assertUneditable();
});

it('the form actions are unavailable when viewing a public probe', async () => {
await renderProbeEditor({ probe: PUBLIC_PROBE });
await assertNoActions();
Expand All @@ -124,6 +130,12 @@ it('should render the form in read mode when passing `forceReadMode`', async ()
await assertUneditable();
});

it('the form actions are unavailable as a RBAC viewer', async () => {
runTestAsRBACReader();
await renderProbeEditor();
await assertNoActions();
});

async function assertUneditable() {
const nameInput = await screen.findByLabelText('Probe Name', { exact: false });
expect(nameInput).toBeDisabled();
Expand Down
8 changes: 4 additions & 4 deletions src/components/ProbeEditor/ProbeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ export const ProbeEditor = ({
forceViewMode, // When true, the form is in view mode
}: ProbeEditorProps) => {
const styles = useStyles2(getStyles);
const canEdit = useCanEditProbe(probe);
const writeMode = canEdit && !forceViewMode;
const { canWriteProbes } = useCanEditProbe(probe);
const writeMode = canWriteProbes && !forceViewMode;
const form = useForm<Probe>({ defaultValues: probe, resolver: zodResolver(ProbeSchema) });
const { latitude, longitude } = form.watch();
const handleSubmit = form.handleSubmit((formValues: Probe) => onSubmit(formValues));
Expand Down Expand Up @@ -164,7 +164,7 @@ export const ProbeEditor = ({
/>
</Field>
</div>
{canEdit && <LabelField<Probe> disabled={!writeMode} labelDestination={'probe'} />}
{canWriteProbes && <LabelField<Probe> disabled={!writeMode} labelDestination={'probe'} />}
<div className={styles.marginBottom}>
<Legend>Capabilities</Legend>
<HorizontalCheckboxField
Expand All @@ -189,7 +189,7 @@ export const ProbeEditor = ({
</FeatureFlag>
</div>
<div className={styles.buttonWrapper}>
{canEdit && (
{canWriteProbes && (
<>
<Button
icon={loading ? 'fa fa-spinner' : undefined}
Expand Down
Loading