mirror of https://github.com/grafana/grafana.git
				
				
				
			Alerting: Allow groups and namespaces with slashes (#92183)
* Allow rule groups and namespaces with slashes * Fix lint
This commit is contained in:
		
							parent
							
								
									130a86d9c7
								
							
						
					
					
						commit
						db711d6a21
					
				| 
						 | 
				
			
			@ -5,7 +5,7 @@ import { FetchResponse, getBackendSrv } from '@grafana/runtime';
 | 
			
		|||
import { RulerDataSourceConfig } from 'app/types/unified-alerting';
 | 
			
		||||
import { PostableRulerRuleGroupDTO, RulerRuleGroupDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
 | 
			
		||||
 | 
			
		||||
import { checkForPathSeparator } from '../components/rule-editor/util';
 | 
			
		||||
import { containsPathSeparator } from '../components/rule-editor/util';
 | 
			
		||||
import { RULER_NOT_SUPPORTED_MSG } from '../utils/constants';
 | 
			
		||||
import { getDatasourceAPIUid, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -73,11 +73,15 @@ interface RulerQueryDetailsProvider {
 | 
			
		|||
  group: (group: string) => GroupUrlParams;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// some gateways (like Istio) will decode "/" and "\" characters – this will cause 404 errors for any API call
 | 
			
		||||
// that includes these values in the URL (ie. /my/path%2fto/resource -> /my/path/to/resource)
 | 
			
		||||
//
 | 
			
		||||
// see https://istio.io/latest/docs/ops/best-practices/security/#customize-your-system-on-path-normalization
 | 
			
		||||
function getQueryDetailsProvider(rulerConfig: RulerDataSourceConfig): RulerQueryDetailsProvider {
 | 
			
		||||
  const isGrafanaDatasource = rulerConfig.dataSourceName === GRAFANA_RULES_SOURCE_NAME;
 | 
			
		||||
 | 
			
		||||
  const groupParamRewrite = (group: string): GroupUrlParams => {
 | 
			
		||||
    if (checkForPathSeparator(group) !== true) {
 | 
			
		||||
    if (containsPathSeparator(group) === true) {
 | 
			
		||||
      return { group: QUERY_GROUP_TAG, searchParams: { group } };
 | 
			
		||||
    }
 | 
			
		||||
    return { group, searchParams: {} };
 | 
			
		||||
| 
						 | 
				
			
			@ -93,7 +97,7 @@ function getQueryDetailsProvider(rulerConfig: RulerDataSourceConfig): RulerQuery
 | 
			
		|||
 | 
			
		||||
  return {
 | 
			
		||||
    namespace: (namespace: string): NamespaceUrlParams => {
 | 
			
		||||
      if (checkForPathSeparator(namespace) !== true) {
 | 
			
		||||
      if (containsPathSeparator(namespace) === true) {
 | 
			
		||||
        return { namespace: QUERY_NAMESPACE_TAG, searchParams: { namespace } };
 | 
			
		||||
      }
 | 
			
		||||
      return { namespace, searchParams: {} };
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,7 +23,6 @@ import { evaluateEveryValidationOptions } from '../rules/EditRuleGroupModal';
 | 
			
		|||
 | 
			
		||||
import { EvaluationGroupQuickPick } from './EvaluationGroupQuickPick';
 | 
			
		||||
import { containsSlashes, Folder, RuleFolderPicker } from './RuleFolderPicker';
 | 
			
		||||
import { checkForPathSeparator } from './util';
 | 
			
		||||
 | 
			
		||||
export const MAX_GROUP_RESULTS = 1000;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -175,9 +174,6 @@ export function FolderAndGroup({
 | 
			
		|||
                    name="folder"
 | 
			
		||||
                    rules={{
 | 
			
		||||
                      required: { value: true, message: 'Select a folder' },
 | 
			
		||||
                      validate: {
 | 
			
		||||
                        pathSeparator: (folder: Folder) => checkForPathSeparator(folder.uid),
 | 
			
		||||
                      },
 | 
			
		||||
                    }}
 | 
			
		||||
                  />
 | 
			
		||||
                  <Text color="secondary">or</Text>
 | 
			
		||||
| 
						 | 
				
			
			@ -251,9 +247,6 @@ export function FolderAndGroup({
 | 
			
		|||
              control={control}
 | 
			
		||||
              rules={{
 | 
			
		||||
                required: { value: true, message: 'Must enter a group name' },
 | 
			
		||||
                validate: {
 | 
			
		||||
                  pathSeparator: (group_: string) => checkForPathSeparator(group_),
 | 
			
		||||
                },
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          </Field>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,8 +10,6 @@ import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelect
 | 
			
		|||
import { fetchRulerRulesAction } from '../../state/actions';
 | 
			
		||||
import { RuleFormValues } from '../../types/rule-form';
 | 
			
		||||
 | 
			
		||||
import { checkForPathSeparator } from './util';
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  rulesSourceName: string;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -74,9 +72,6 @@ export const GroupAndNamespaceFields = ({ rulesSourceName }: Props) => {
 | 
			
		|||
          control={control}
 | 
			
		||||
          rules={{
 | 
			
		||||
            required: { value: true, message: 'Required.' },
 | 
			
		||||
            validate: {
 | 
			
		||||
              pathSeparator: checkForPathSeparator,
 | 
			
		||||
            },
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      </Field>
 | 
			
		||||
| 
						 | 
				
			
			@ -98,9 +93,6 @@ export const GroupAndNamespaceFields = ({ rulesSourceName }: Props) => {
 | 
			
		|||
          control={control}
 | 
			
		||||
          rules={{
 | 
			
		||||
            required: { value: true, message: 'Required.' },
 | 
			
		||||
            validate: {
 | 
			
		||||
              pathSeparator: checkForPathSeparator,
 | 
			
		||||
            },
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      </Field>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@ import { ClassicCondition, ExpressionQuery } from 'app/features/expressions/type
 | 
			
		|||
import { AlertQuery } from 'app/types/unified-alerting-dto';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  checkForPathSeparator,
 | 
			
		||||
  containsPathSeparator,
 | 
			
		||||
  findRenamedDataQueryReferences,
 | 
			
		||||
  getThresholdsForQueries,
 | 
			
		||||
  queriesWithUpdatedReferences,
 | 
			
		||||
| 
						 | 
				
			
			@ -229,17 +229,15 @@ describe('rule-editor', () => {
 | 
			
		|||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe('checkForPathSeparator', () => {
 | 
			
		||||
  it('should not allow strings with /', () => {
 | 
			
		||||
    expect(checkForPathSeparator('foo / bar')).not.toBe(true);
 | 
			
		||||
    expect(typeof checkForPathSeparator('foo / bar')).toBe('string');
 | 
			
		||||
describe('containsPathSeparator', () => {
 | 
			
		||||
  it('should return true for strings with /', () => {
 | 
			
		||||
    expect(containsPathSeparator('foo / bar')).toBe(true);
 | 
			
		||||
  });
 | 
			
		||||
  it('should not allow strings with \\', () => {
 | 
			
		||||
    expect(checkForPathSeparator('foo \\ bar')).not.toBe(true);
 | 
			
		||||
    expect(typeof checkForPathSeparator('foo \\ bar')).toBe('string');
 | 
			
		||||
  it('should return true for strings with \\', () => {
 | 
			
		||||
    expect(containsPathSeparator('foo \\ bar')).toBe(true);
 | 
			
		||||
  });
 | 
			
		||||
  it('should allow anything without / or \\', () => {
 | 
			
		||||
    expect(checkForPathSeparator('foo bar')).toBe(true);
 | 
			
		||||
  it('should return false for strings without / or \\', () => {
 | 
			
		||||
    expect(containsPathSeparator('foo !@#$%^&*() <> [] {} bar')).toBe(false);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,4 @@
 | 
			
		|||
import { xor } from 'lodash';
 | 
			
		||||
import { ValidateResult } from 'react-hook-form';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  DataFrame,
 | 
			
		||||
| 
						 | 
				
			
			@ -89,17 +88,8 @@ export function refIdExists(queries: AlertQuery[], refId: string | null): boolea
 | 
			
		|||
  return queries.find((query) => query.refId === refId) !== undefined;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// some gateways (like Istio) will decode "/" and "\" characters – this will cause 404 errors for any API call
 | 
			
		||||
// that includes these values in the URL (ie. /my/path%2fto/resource -> /my/path/to/resource)
 | 
			
		||||
//
 | 
			
		||||
// see https://istio.io/latest/docs/ops/best-practices/security/#customize-your-system-on-path-normalization
 | 
			
		||||
export function checkForPathSeparator(value: string): ValidateResult {
 | 
			
		||||
  const containsPathSeparator = value.includes('/') || value.includes('\\');
 | 
			
		||||
  if (containsPathSeparator) {
 | 
			
		||||
    return 'Cannot contain "/" or "\\" characters';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return true;
 | 
			
		||||
export function containsPathSeparator(value: string): boolean {
 | 
			
		||||
  return value.includes('/') || value.includes('\\');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// this function assumes we've already checked if the data passed in to the function is of the alert condition
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import { render, screen, userEvent } from 'test/test-utils';
 | 
			
		||||
import { render } from 'test/test-utils';
 | 
			
		||||
import { byLabelText, byTestId, byText, byTitle } from 'testing-library-selector';
 | 
			
		||||
 | 
			
		||||
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
 | 
			
		||||
| 
						 | 
				
			
			@ -158,12 +158,4 @@ describe('EditGroupModal component on grafana-managed alert rules', () => {
 | 
			
		|||
    expect(await ui.input.namespace.find()).toHaveValue('namespace1');
 | 
			
		||||
    expect(ui.folderLink.query()).not.toBeInTheDocument();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('does not allow slashes in the group name', async () => {
 | 
			
		||||
    const user = userEvent.setup();
 | 
			
		||||
    renderWithGrafanaGroup();
 | 
			
		||||
    await user.type(await ui.input.group.find(), 'group/with/slashes');
 | 
			
		||||
    await user.click(ui.input.interval.get());
 | 
			
		||||
    expect(await screen.findByText(/cannot contain \"\/\"/i)).toBeInTheDocument();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,7 +28,6 @@ import { EvaluationIntervalLimitExceeded } from '../InvalidIntervalWarning';
 | 
			
		|||
import { decodeGrafanaNamespace, encodeGrafanaNamespace } from '../expressions/util';
 | 
			
		||||
import { EvaluationGroupQuickPick } from '../rule-editor/EvaluationGroupQuickPick';
 | 
			
		||||
import { MIN_TIME_RANGE_STEP_S } from '../rule-editor/GrafanaEvaluationBehavior';
 | 
			
		||||
import { checkForPathSeparator } from '../rule-editor/util';
 | 
			
		||||
 | 
			
		||||
const ITEMS_PER_PAGE = 10;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -300,11 +299,6 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
 | 
			
		|||
                    readOnly={intervalEditOnly || isGrafanaManagedGroup}
 | 
			
		||||
                    {...register('namespaceName', {
 | 
			
		||||
                      required: 'Namespace name is required.',
 | 
			
		||||
                      validate: {
 | 
			
		||||
                        // for Grafana-managed we do not validate the name of the folder because we use the UID anyway
 | 
			
		||||
                        pathSeparator: (namespaceName) =>
 | 
			
		||||
                          isGrafanaManagedGroup ? true : checkForPathSeparator(namespaceName),
 | 
			
		||||
                      },
 | 
			
		||||
                    })}
 | 
			
		||||
                  />
 | 
			
		||||
                </Field>
 | 
			
		||||
| 
						 | 
				
			
			@ -337,9 +331,6 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
 | 
			
		|||
                readOnly={intervalEditOnly}
 | 
			
		||||
                {...register('groupName', {
 | 
			
		||||
                  required: 'Evaluation group name is required.',
 | 
			
		||||
                  validate: {
 | 
			
		||||
                    pathSeparator: (namespace) => checkForPathSeparator(namespace),
 | 
			
		||||
                  },
 | 
			
		||||
                })}
 | 
			
		||||
              />
 | 
			
		||||
            </Field>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue