From c9913116d000c18a504f48b488b43a9fdd04947e Mon Sep 17 00:00:00 2001 From: f1ames Date: Tue, 31 Oct 2023 14:34:48 +0100 Subject: [PATCH] feat: support deny action in bindings --- .../server/src/utils/validation-server.ts | 27 +++++++++++++--- examples/policy-binding-sample-6.yaml | 14 +++++++++ .../templates/monokle-policy-binding-crd.yaml | 2 +- tests/src/standalone.e2e.spec.ts | 31 +++++++++++++++++++ 4 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 examples/policy-binding-sample-6.yaml diff --git a/admission-controller/server/src/utils/validation-server.ts b/admission-controller/server/src/utils/validation-server.ts index 41b451f..e9d3ae3 100644 --- a/admission-controller/server/src/utils/validation-server.ts +++ b/admission-controller/server/src/utils/validation-server.ts @@ -174,6 +174,13 @@ export class ValidationServer { } } + this._logger.trace({resourceForValidation, validationResponses}); + + if (violations.length === 0) { + this._logger.debug({msg: 'No violations', response}); + return response; + } + const violationsByAction = violations.reduce((acc: Record, violation: Violation) => { const actions = violation.actions; @@ -191,7 +198,6 @@ export class ValidationServer { const responseFull = this._handleViolationsByAction(violationsByAction, resourceForValidation, response); this._logger.debug({response}); - this._logger.trace({resourceForValidation, validationResponses}); return responseFull; }); @@ -221,6 +227,11 @@ export class ValidationServer { // - https://kubernetes.io/blog/2020/09/03/warnings/ if (action.toLowerCase() === 'warn') { response = this._handleViolationsAsWarn(violationsByAction[action], resource, response); + } else if (action.toLowerCase() === 'deny') { + const violationMessages = this._getViolationsMessages(violationsByAction[action], resource); + + response.response.allowed = false; + response.response.status.message = violationMessages.join("\n"); } } @@ -228,6 +239,15 @@ export class ValidationServer { } private _handleViolationsAsWarn(violations: Violation[], resource: Resource, response: AdmissionResponse) { + const violationMessages = this._getViolationsMessages(violations, resource); + if (violationMessages.length > 0) { + response.response.warnings = violationMessages; + } + + return response; + } + + private _getViolationsMessages(violations: Violation[], resource: Resource): string[] { const errors = violations .filter((v) => v.level === 'error') .map((e) => this._formatViolationMessage(e, resource)); @@ -236,9 +256,8 @@ export class ValidationServer { .filter((v) => v.level === 'warning') .map((e) => this._formatViolationMessage(e, resource)); - if (errors.length > 0 || warnings.length > 0) { - response.response.warnings = [ + return [ `Monokle Admission Controller found ${errors.length} errors and ${warnings.length} warnings:`, ...errors, ...warnings, @@ -246,7 +265,7 @@ export class ValidationServer { ]; } - return response; + return []; } private _getFullyQualifiedName(result: ValidationResult) { diff --git a/examples/policy-binding-sample-6.yaml b/examples/policy-binding-sample-6.yaml new file mode 100644 index 0000000..e60fc00 --- /dev/null +++ b/examples/policy-binding-sample-6.yaml @@ -0,0 +1,14 @@ +apiVersion: monokle.io/v1alpha1 +kind: MonoklePolicyBinding +metadata: + name: policy-binding-sample-5 +spec: + policyName: "policy-sample-1" + validationActions: [Deny] + matchResources: + namespaceSelector: + matchExpressions: + - key: name + operator: In + values: + - default \ No newline at end of file diff --git a/helm/templates/monokle-policy-binding-crd.yaml b/helm/templates/monokle-policy-binding-crd.yaml index 2e92053..161b76f 100644 --- a/helm/templates/monokle-policy-binding-crd.yaml +++ b/helm/templates/monokle-policy-binding-crd.yaml @@ -26,7 +26,7 @@ spec: type: array items: type: string - enum: [Warn] + enum: [Warn, Deny] matchResources: type: object properties: diff --git a/tests/src/standalone.e2e.spec.ts b/tests/src/standalone.e2e.spec.ts index 8477e6c..c01d9b0 100644 --- a/tests/src/standalone.e2e.spec.ts +++ b/tests/src/standalone.e2e.spec.ts @@ -168,6 +168,36 @@ describe(`Standalone (dir: ${mainDir})`, () => { assert.equal(warningsCount, 8); assert.equal(errorsCount, 4); }); + + it('creates resource (valid) when "Deny" policy defined', async () => { + await run(`cd "${mainDir}" && kubectl apply -f examples/policy-sample-1.yaml`); + await run(`cd "${mainDir}" && kubectl apply -f examples/policy-binding-sample-6.yaml`, 500); + + const output = await run(`cd "${mainDir}" && kubectl -n default apply -f examples/pod-valid.yaml`); + + assert.match(output, /pod\/pod-valid created/); + assert.notMatch(output, /\(warning\)/gi); + assert.notMatch(output, /\(error\)/gi); + }); + + it('blocks creating resource (misconfigured) with list of violations as output when "Deny" policy defined', async () => { + await run(`cd "${mainDir}" && kubectl apply -f examples/policy-sample-1.yaml`); + await run(`cd "${mainDir}" && kubectl apply -f examples/policy-binding-sample-6.yaml`, 500); + + try { + const output = await run(`cd "${mainDir}" && kubectl -n default apply -f examples/pod-warning.yaml`); + assert.fail(`Expected error but got success: ${output}`); + } catch (err: any) { + assert.notMatch(err, /pod\/pod-warning created/); + assert.match(err, /\(warning\)/gi); + + const warningsCount = (err.match(/\(warning\)/gi) || []).length; + const errorsCount = (err.match(/\(error\)/gi) || []).length; + + assert.equal(warningsCount, 8); + assert.equal(errorsCount, 3); + } + }); }); const run = async (command: string, timeoutMs?: number): Promise => { @@ -221,6 +251,7 @@ const cleanup = async () => { run(`cd "${mainDir}" && kubectl delete -f examples/policy-binding-sample-3.yaml`), run(`cd "${mainDir}" && kubectl delete -f examples/policy-binding-sample-4.yaml`), run(`cd "${mainDir}" && kubectl delete -f examples/policy-binding-sample-5.yaml`), + run(`cd "${mainDir}" && kubectl delete -f examples/policy-binding-sample-6.yaml`), run(`cd "${mainDir}" && kubectl delete -f examples/pod-valid.yaml -n default`), run(`cd "${mainDir}" && kubectl delete -f examples/pod-valid.yaml -n nstest1`), run(`cd "${mainDir}" && kubectl delete -f examples/pod-valid.yaml -n nstest2`),