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

Introduce fingerprint-based suppressions code action provider #92

Merged
merged 19 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@
"test": "concurrently -s command-1 -k \"npm run test:server\" \"node ./out/test/run-test.js\"",
"test:cc": "concurrently -s command-1 -k \"npm run test:server\" \"c8 node ./out/test/run-test.js\"",
"test:server": "node ./out/test/run-server.js",
"test:ensure-coverage": "npx c8 check-coverage --lines 68 --functions 75 --branches 75 --statements 68"
"test:ensure-coverage": "npx c8 check-coverage --lines 68 --functions 76 --branches 82 --statements 68"
},
"devDependencies": {
"@graphql-tools/mock": "^9.0.0",
Expand Down Expand Up @@ -184,7 +184,7 @@
},
"dependencies": {
"@monokle/parser": "^0.3.2",
"@monokle/synchronizer": "^0.13.0",
"@monokle/synchronizer": "^0.14.2",
"@monokle/validation": "^0.33.0",
"@segment/analytics-node": "^1.1.0",
"diff": "^5.1.0",
Expand Down
67 changes: 67 additions & 0 deletions src/commands/suppress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { canRun } from '../utils/commands';
import { COMMAND_NAMES } from '../constants';
import { raiseWarning } from '../utils/errors';
import { trackEvent } from '../utils/telemetry';
import { MONOKLE_FINGERPRINT_FIELD, ValidationResultExtended } from '../core/code-actions/base-code-actions-provider';
import { SuppressionPermissions, generateSuppression } from '../core';
import { applySuppressions } from '../utils/validation';
import { Folder } from '../utils/workspace';
import globals from '../utils/globals';
import type { RuntimeContext } from '../utils/runtime-context';

export function getSuppressCommand(context: RuntimeContext) {
return async (result: ValidationResultExtended, permissions: SuppressionPermissions, root: Folder) => {
if (!canRun()) {
return null;
}

if (permissions === 'NONE') {
// Should not happen, since permissions are checked in CodeActionProvider. Still handle gracefully.
raiseWarning('You do not have permissions to suppress this misconfiguration');

trackEvent('command/suppress', {
status: 'failure',
error: 'Suppression command run despite no permissions.'
});

return null;
}

trackEvent('command/suppress', {
status: 'started'
});

const user = await globals.getUser();

if (!user.isAuthenticated) {
raiseWarning(`You need to be logged in to use suppressions. Run ${COMMAND_NAMES.LOGIN} to authenticate first.}`);

trackEvent('command/suppress', {
status: 'failure',
error: 'User not authenticated.'
});

return null;
}


const localSuppression = generateSuppression(result.fingerprints?.[MONOKLE_FINGERPRINT_FIELD], permissions);

await applySuppressions(root, localSuppression);

await context.synchronizer.toggleSuppression(
user.tokenInfo,
result.fingerprints?.[MONOKLE_FINGERPRINT_FIELD],
`${result.ruleId} - ${result.message.text}`,
result.locations.at(1).logicalLocations?.at(0)?.fullyQualifiedName,
root.uri.fsPath,
globals.project || undefined
);

await applySuppressions(root);

trackEvent('command/suppress', {
status: 'success'
});
};
}
2 changes: 1 addition & 1 deletion src/commands/synchronize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function getSynchronizeCommand(context: RuntimeContext) {
return null;
}

await context.refreshPolicyPuller();
await context.refreshPolicyPuller(true);

trackEvent('command/synchronize', {
status: 'success'
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const COMMANDS = {
TRACK: 'monokle.track',
RAISE_AUTHENTICATION_ERROR: 'monokle.raiseAuthenticationError',
RUN_COMMANDS: 'monokle.runCommands',
SUPPRESS: 'monokle.suppress',
};

export const COMMAND_NAMES = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import { CodeAction, CodeActionKind, TextDocument, Range, languages } from 'vscode';
import { BaseCodeActionsProvider, CodeActionContextExtended, DiagnosticExtended, ValidationResultExtended } from './base-code-actions-provider';
import { COMMANDS } from '../../constants';
import { getOwnerWorkspace } from '../../utils/workspace';
import { shouldUseFingerprintSuppressions } from '../suppressions/suppressions';

class AnnotationSuppressionsCodeActionsProvider extends BaseCodeActionsProvider<AnnotationSuppressionsCodeAction> {
public async provideCodeActions(document: TextDocument, _range: Range, context: CodeActionContextExtended) {
const workspaceRoot = getOwnerWorkspace(document);
const fingerprintSuppressionsPermissions = shouldUseFingerprintSuppressions(workspaceRoot.uri.fsPath);

// We allow either annotation or fingerprint suppressions at the same time, but not both.
if (fingerprintSuppressionsPermissions.allowed) {
return [];
}

return this.getMonokleDiagnostics(context).map((diagnostic: DiagnosticExtended) => {
return new AnnotationSuppressionsCodeAction(document, diagnostic);
});
Expand Down
5 changes: 3 additions & 2 deletions src/core/code-actions/base-code-actions-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ValidationResult, ValidationRule } from './../../utils/validation';
export type ValidationResultExtended = ValidationResult & {
_id: [string, number, number];
_rule: ValidationRule;
_uri: string;
};

export type DiagnosticExtended = Diagnostic & {
Expand All @@ -28,11 +29,11 @@ export abstract class BaseCodeActionsProvider<T_ACTION extends CodeAction> imple

public abstract resolveCodeAction(codeAction: T_ACTION);

protected getMonokleDiagnostics(context: CodeActionContextExtended) {
protected getMonokleDiagnostics(context: CodeActionContextExtended, groupByRule = true) {
// Filter out diagnostic objects without Monokle fingerprint, because this is not Monokle related diagnostics.
const monokleDiagnostics = context.diagnostics.filter(diagnostic => diagnostic?.result?.fingerprints?.[MONOKLE_FINGERPRINT_FIELD]);

return this.getUniqueDiagnosticsByRule(monokleDiagnostics);
return groupByRule ? this.getUniqueDiagnosticsByRule(monokleDiagnostics) : monokleDiagnostics;
}

protected getParsedDocument(document: TextDocument, result: ValidationResult) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { CodeAction, CodeActionKind, TextDocument, Range, languages, Uri } from 'vscode';
import { BaseCodeActionsProvider, CodeActionContextExtended, DiagnosticExtended, ValidationResultExtended } from './base-code-actions-provider';
import { COMMANDS } from '../../constants';
import { Folder, getOwnerWorkspace } from '../../utils/workspace';
import globals from '../../utils/globals';
import { SuppressionPermissions, isUnderReview, shouldUseFingerprintSuppressions } from '../suppressions/suppressions';

class FingerprintSuppressionsCodeActionsProvider extends BaseCodeActionsProvider<FingerprintSuppressionsCodeAction> {
public async provideCodeActions(document: TextDocument, _range: Range, context: CodeActionContextExtended) {
const workspaceRoot = getOwnerWorkspace(document);
const fingerprintSuppressionsPermissions = shouldUseFingerprintSuppressions(workspaceRoot.uri.fsPath);

if (!fingerprintSuppressionsPermissions.allowed) {
return [];
}

const diagnostics = this.getMonokleDiagnostics(context, false);

// Filter out 'under review' violations if user has only 'request suppression' rights. There is no sense in requesting again.
// However, for 'Admin' there should be still an ability to suppress such violation (so it's like accepting a request).
const roleRelevantDiagnostics = fingerprintSuppressionsPermissions.permissions === 'ADD' ?
diagnostics :
diagnostics.filter((diagnostic: DiagnosticExtended) => !isUnderReview(diagnostic.result));

return roleRelevantDiagnostics.map((diagnostic: DiagnosticExtended) => {
return new FingerprintSuppressionsCodeAction(diagnostic, fingerprintSuppressionsPermissions.permissions, workspaceRoot);
});
}

public async resolveCodeAction(codeAction: FingerprintSuppressionsCodeAction) {
const user = await globals.getUser();

if (!user.isAuthenticated) {
// This should not happen, since we don't show this code action if user is not authenticated.
// Still handle it if something unpredictable happens.
codeAction.command = {
command: COMMANDS.RAISE_AUTHENTICATION_ERROR,
title: 'Raise Authentication Error',
arguments: ['Suppressing a problem requires authentication.', {
event: 'code_action/fingerprint_suppression',
data: {
status: 'cancelled',
ruleId: codeAction.result.ruleId,
error: 'Unauthenticated',
}
}]
};
} else {
codeAction.command = {
command: COMMANDS.SUPPRESS,
title: 'Suppress misconfiguration',
arguments: [codeAction.result, codeAction.permissions, codeAction.root]
};
}

return codeAction;
}
}

class FingerprintSuppressionsCodeAction extends CodeAction {
private readonly _result: ValidationResultExtended;
private readonly _permissions: SuppressionPermissions;
private readonly _root: Folder;

constructor(diagnostic: DiagnosticExtended, permissions: SuppressionPermissions, root: Folder) {
super(`${permissions === 'ADD' ? 'Suppress' : 'Request suppression of'}: ${diagnostic.result.message.text}`, CodeActionKind.QuickFix);

this.diagnostics = [diagnostic];
this._result = diagnostic.result;
this._permissions = permissions;
this._root = root;
}

get result() {
return this._result;
}

get permissions() {
return this._permissions;
}

get root() {
return this._root;
}
}

export function registerFingerprintSuppressionsCodeActionsProvider() {
return languages.registerCodeActionsProvider({language: 'yaml'}, new FingerprintSuppressionsCodeActionsProvider(), {
providedCodeActionKinds: FingerprintSuppressionsCodeActionsProvider.providedCodeActionKinds
});
}
1 change: 1 addition & 0 deletions src/core/code-actions/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './annotation-suppressions-code-actions-provider';
export * from './fix-code-actions-provider';
export * from './show-details-code-actions-provider';
export * from './fingerprint-suppressions-code-actions-provider';
2 changes: 2 additions & 0 deletions src/core/code-actions/show-details-code-actions-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ class ShowDetailsCodeActionsProvider extends BaseCodeActionsProvider<ShowDetails
}

class ShowDetailsCodeAction extends CodeAction {
isPreferred = true;

get firstResult() {
return (this.diagnostics[0] as DiagnosticExtended).result;
}
Expand Down
1 change: 1 addition & 0 deletions src/core/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './code-actions';
export * from './suppressions';
1 change: 1 addition & 0 deletions src/core/suppressions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './suppressions';
63 changes: 63 additions & 0 deletions src/core/suppressions/suppressions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { FingerprintSuppression } from '@monokle/types';
import globals from '../../utils/globals';
import { ValidationResult } from '../../utils/validation';
import { ValidationResultExtended } from '../code-actions/base-code-actions-provider';

export type SuppressionPermissions = 'ADD' | 'REQUEST' | 'NONE';
export type SuppressionsStatus = {
permissions: SuppressionPermissions;
allowed: boolean;
};

export function getSuppressions(path: string) {
const fingerprintSuppressions: FingerprintSuppression[] = globals.getSuppressions(path).map((suppression) => {
return {
guid: suppression.id,
kind: 'external',
status: toSuppressionStatus(suppression.status),
fingerprint: suppression.fingerprint,
} as FingerprintSuppression;
});

return {
suppressions: fingerprintSuppressions,
};
}

export function generateSuppression(fingerprint: string, permissions: SuppressionPermissions) {
return {
guid: `sup-${Date.now()}`,
kind: 'external',
status: toSuppressionStatus(permissions === 'ADD' ? 'accepted' : 'underReview'),
fingerprint: fingerprint,
} as FingerprintSuppression;
}

export function shouldUseFingerprintSuppressions(repoRootPath: string): SuppressionsStatus {
const projectPermissions = globals.getProjectPermissions(repoRootPath);

return {
permissions: projectPermissions,
allowed: projectPermissions !== 'NONE',
};
}

export function isUnderReview(result: ValidationResult | ValidationResultExtended) {
return result.suppressions.length > 0 && result.suppressions.every(s => s.status === 'underReview');
}

function toSuppressionStatus(status: string) {
switch (status) {
case 'ACCEPTED':
case 'accepted':
return 'accepted';
case 'REJECTED':
case 'rejected':
return 'rejected';
case 'UNDER_REVIEW':
case 'underReview':
return 'underReview';
default:
return status;
}
}
Loading
Loading