Compare commits

...

1 commit

Author SHA1 Message Date
Alex Clemmer 067bd47189 Implement admission policy
This commit introduces admission policies to Pulumi. An admission policy
allows users to specify a set of policies that all resource operations
must be validated against, and pass, in order for the deployment to
succeed.

This commit introduces a policy model that:

- Tagging primitives, allowing users to group policies by categories
  like "cost" and "security".
- Rich metadata, allowing users to write custom messages that are
  displayed when a policy failure occurs. Useful for explaining why
  somethign is disallowed, mitigation, etc.
- Reactive. Allows every resource operation to be rejected just befor
  it's preformed.
- Comprehensive. During preview, policy violations are aggregated and
  reported all at once, allowing users to get a high-level view of all
  violations known during planning.
2019-02-27 14:21:58 -08:00
6 changed files with 217 additions and 3 deletions

View file

@ -29,8 +29,9 @@ import * as asset from "./asset";
import * as dynamic from "./dynamic";
import * as iterable from "./iterable";
import * as log from "./log";
import * as policy from "./policy";
import * as runtime from "./runtime";
export { asset, dynamic, iterable, log, runtime };
export { asset, dynamic, iterable, log, policy, runtime };
// @pulumi is a deployment-only module. If someone tries to capture it, and we fail for some reason
// we want to give a good message about what the problem likely is. Note that capturing a

180
sdk/nodejs/policy/index.ts Normal file
View file

@ -0,0 +1,180 @@
// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as log from "../log";
/**
* Tags are informational categories that can be applied to a policy. This can be useful for
* generating views of policy failures, reports, etc.
*/
export enum Tags {
/** Cost indicates a policy is related to cost management. */
Cost = "cost",
/** Security indicates a policy is related to security. */
Security = "security",
}
/** Represents how a policy violation should be handled (e.g., blocking deployment). */
export enum EnforcementLevel {
/** Warning represents a policy that displays a warning on violation. */
Warning = "warning",
/**
* SoftMandatory represents a policy that prevents deployment on violation, but can be
* overridden with appropriate permissions.
*/
SoftMandatory = "mandatory",
/**
* HardMandatory represents a policy that prevents deployment on violation, and cannot be
* overridden.
*/
HardMandatory = "hardMandatory",
}
/** A policy rule that validates `inputs`, returning a boolean indicating success. */
export type Rule = (inputs: any) => boolean;
/**
* Decides whether a resource operation should proceed ("be admitted") or not. Admission policies
* are checked both in the preview stage, and just before any resource operation occurs, allowing
* the policy to prevent problematic resource operations from occurring. Metadata is provided to
* allow meaningful error messages when a policy violation occurs.
*/
export interface AdmissionPolicy {
/**
* A brief description of the policy rule. e.g., "S3 buckets should have default encryption
* enabled."
*/
description: string;
/**
* A detailed message to display on policy violation. Typically includes an explanation of the
* policy, and steps to take to remediate.
*/
message: string;
/**
* Tags to help sort and filter policy violations for reporting purposes, e.g., filtering on the
* "cost" label.
*/
tags: Tags[];
/**
* Indicates what to do on policy violation, e.g., block deployment but allow override with
* proper permissions.
*/
enforcementLevel: EnforcementLevel;
/** The core policy logic, checking whether a resource violates the policy. */
rule: Rule;
}
/**
* Decides whether a resource operation on a specific type (e.g., AWS S3 Bucket) should proceed ("be
* admitted") or not. Admission policies are checked both in the preview stage, and just before any
* resource operation occurs, allowing the policy to prevent problematic resource operations from
* occurring. Metadata is provided to allow meaningful error messages when a policy violation
* occurs.
*/
export interface TypedAdmissionPolicy extends AdmissionPolicy {
/**
* The type of the resource to apply the policy to. e.g., Kubernetes
* `kubernetes:core/v1:Service`.
*/
pulumiType: string;
}
/**
* A TypedAdmissionPolicy, paired with information about resource operations that have failed. This
* is used primarily to aggregate a list of resources that have failed a specific policy rule, so
* that they can be reported all at once (e.g., at the end of a preview).
*/
class AdmissionPolicyRecord implements TypedAdmissionPolicy {
public readonly description: string;
public readonly tags: Tags[];
public readonly message: string;
public readonly enforcementLevel: EnforcementLevel;
public readonly rule: Rule;
public readonly pulumiType: string;
public failures: string[];
constructor(policy: TypedAdmissionPolicy) {
Object.assign(this, policy);
this.failures = [];
}
public toString(): string {
return `${this.failures.length} violations of rule '${
this.description
}':\n - ${this.failures
.map(name => `${this.pulumiType} ${name}`)
.join("\n - ")}`;
}
}
/**
* Maintains a set of TypedAdmissionPolicy, a `validate` function that allows external sources to
* validate resources against the policy corpus as they please, and information about which
* resources have violated those policies, and the reporting of those policy violations.
*
* The reporting semantics differ in two important cases:
*
* - On preview, we will aggregate all known policy violations, and report all of them as errors
* at the end of the preview.
* - If the preview for a resource operation is skipped, any policy violation will occur
* immediately, halting the deployment.
*
* The "aggregated report" approach of the preview is accomplished by using the Node.js exit hooks.
*/
export class TypedAdmissionPolicySet {
private readonly policies: AdmissionPolicyRecord[] = [];
constructor(private readonly aggregateErrors: boolean) {
process.on("beforeExit", () => {
const violations = this.policies.filter(policy => policy.failures.length > 0);
if (violations.length > 0) {
violations.forEach(policy => {
log.error(policy.toString());
// Clear policy failures so that when `log.error` causes us to schedule the exit
// again, we don't have more work to do.
policy.failures = [];
});
log.error("One or more policy violations occurred");
}
});
}
public addPolicy(policy: TypedAdmissionPolicy) {
this.policies.push(new AdmissionPolicyRecord(policy));
}
public validate(typ: string, id: string, inputs: any) {
this.policies.forEach(policy => {
if (typ !== policy.pulumiType) {
return;
}
const policyViolated = policy.rule(inputs);
if (policyViolated) {
if (this.aggregateErrors) {
policy.failures.push(id);
} else {
throw Error(`Policy '${policy.description}' violated by resource ${id}`);
}
}
});
}
}

View file

@ -29,7 +29,14 @@ import {
transferProperties,
unknownValue,
} from "./rpc";
import { excessiveDebugOutput, getMonitor, getRootResource, rpcKeepAlive, serialize } from "./settings";
import {
excessiveDebugOutput,
getMonitor,
getRootResource,
rpcKeepAlive,
serialize,
validate,
} from "./settings";
const gstruct = require("google-protobuf/google/protobuf/struct_pb.js");
const resproto = require("../proto/resource_pb.js");
@ -128,7 +135,11 @@ export function registerResource(res: Resource, t: string, name: string, custom:
// trace will lead directly to user code. Throwing in `runAsyncResourceOp` results in an Error
// with a non-useful stack trace.
const preallocError = new Error();
debuggablePromise(resopAsync.then(async (resop) => {
const checkedOp = debuggablePromise(resopAsync.then(async (resop) => {
validate(t, name, resop.serializedProps);
return resop;
}), label);
debuggablePromise(checkedOp.then(async (resop) => {
log.debug(`RegisterResource RPC prepared: t=${t}, name=${name}` +
(excessiveDebugOutput ? `, obj=${JSON.stringify(resop.serializedProps)}` : ``));

View file

@ -206,6 +206,10 @@ export function resolveProperties(
* Unknown values are encoded as a distinguished string value.
*/
export const unknownValue = "04da6b54-80e4-46f7-96ec-b56ff0331ba9";
export type UnknownValue = "04da6b54-80e4-46f7-96ec-b56ff0331ba9";
export function isUnknownValue(o: any): o is UnknownValue {
return o === unknownValue;
}
/**
* specialSigKey is sometimes used to encode type identity inside of a map. See pkg/resource/properties.go.
*/

View file

@ -14,6 +14,7 @@
import * as grpc from "grpc";
import { RunError } from "../errors";
import * as policy from "../policy";
import { ComponentResource, URN } from "../resource";
import { debuggablePromise } from "./debuggable";
@ -284,3 +285,18 @@ export async function setRootResource(res: ComponentResource): Promise<void> {
});
});
}
/** Set of policies to validate all resource operations against in the current Pulumi run. */
const policies = new policy.TypedAdmissionPolicySet(isDryRun());
export function validate(typ: string, id: string, inputs: any) {
policies.validate(typ, id, inputs);
}
/**
* Adds an admission policy to the set of policies that will be used to validate all resource
* operations in the current Pulumi run.
*/
export function addAdmissionPolicy(rule: policy.TypedAdmissionPolicy) {
policies.addPolicy(rule);
}

View file

@ -35,6 +35,8 @@
"log/index.ts",
"policy/index.ts",
"runtime/index.ts",
"runtime/closure/v8.ts",
"runtime/closure/createClosure.ts",