Policy Authoring Tutorial

This tutorial demonstrates how to write policy that permits a user to create jobs and services in the system. Refer to the Policy Syntax and Policy Examples for additional guidance. You may also want to read the Apcera blog post on writing flexible policy.

Specifying the policy realm

You begin each policy with a realm declaration. A realm is the set of resources of the same type to which rules apply. In the example, the policy engine will enforce the policy rules on resources in the job::/dev namespace and the auth::/oauth2/http namespace to authenticate the user.

You use resource identifiers to define the realm. There are several resource types against which the policy engine can enforce policy, including jobs, packages, stagers, etc. To grant full system access a user needs all permissions on the root namespace of each resource type. By design there is no global authorization in the system. This gives you fine-grained control and tight governance over all resources in the system.

For example:

  • A realm that declares the root namespace (/) for a resource type such as job::/ instructs the policy engine to enforce the policy rules against all job resources in the system.

  • A realm that declares a sub-namespace such as job::/sandbox/john instructs the policy engine to enforce the rules on all resources in the namespace including sub-namespaces.

  • A local namespace realm such as job::/sandbox/john::my-job instructs the engine to enforce the rules on the job resource named my-job that is located in the /sandbox/john namespace.

Writing policy rules

Policy rules implement the business logic for the policy. In a policy block, policy rules follow the policy realm declaration. The policy engine executes rules on resources immediately when you apply policy to the system.

The building blocks of rules are claims. A policy rule has two types of claims: antecedent and consequent.

  • The antecedent claim is a boolean evaluation in the form of an if statement. Antecedent claims include a boolean operator.

  • The consequent claim is one or more permissions the policy engine (ENGINE) issues if the antecedent claim is true, or if antecedent claim is omitted.

Each policy claim consists of three elements: claim issuer, claim type, and claim value. If the issuer is the policy engine, it is omitted.

In the example, the policy engine is the issuer of all consequent claims, and the antecedant 3 claim.

Declaring claim issuer

The issuer is the asserter of the claim. The default issuer is the policy engine. It can also be some type of authority.

The issuer of consequent claims is always ENGINE, indicating the policy engine. The issuer of antecedent claims may be the policy engine, or it may be an authority, such as the Auth Server or Google Auth.

If the policy engine is the claim issuer, the ENGINE issuer is implied and is not specified in the claim. If the issuer is an authority other than the policy engine, you must explicitly declare the issuer type.

In the policy example, the issuer of the Rule 1 antecedent claim is Google Auth (google). The issuer of the Rule 2 antecedent claim is the Auth Server (auth_server@apcera.me). The issuer of all other claims is the policy engine which can be omitted because it is implicit.

Defining claim type and value

The type is set of allowed types. The value is the set of allowed values.

If the issuer is the policy engine, the claim types and values are usually specific to the resource type permissions declared for the policy realm. For example, a rule on the job::/ realm can contain a claim of type ssh, while a rule on the service::/ realm can contain a claim of type bind. Policy Syntax lists the few generic types and values for policy engine claims.

If the issuer is a principal other than the policy engine, the type and value is specific to the issuer. Claim types name and email and their string values, which identify users, are extracted from the token, so they apply to the issuer, not to the resource type. Other claim types and values are issued by the policy engine and are usually specific to the resource type.

In the example, the claims permit issue, name value, and role value are generic descriptors issued by the policy engine. The consequent permits are resource-dependent.

End-to-end policy example

This example walks you through the policy authoring process for a single user.

Create policy document

The first step is to create a policy document. You can create a new policy using the web console.

To create policy, you need permissions on the policy::/ realm. Without such permissions, you will see an error when you try to create a policy document:

screenshot

Check with your administrator that you have policy authoring permissions. See also Policy on Policy.

Authenticate the user

Once you have a policy document, the next step is to write policy that lets the user access the system.

Authentication involves verifying the identity and issuing the user an access token with the user name as the token subject. The name must be unqiue in the cluster.

The following policy authenticates the user john@gmail.com and issues the user an access token with subject "john" when the identity provider (Google Auth) asserts John's identity.

auth::/oauth2/http
{
  if (Google->email == "john-developer@gmail.com")
  {
    permit issue
    name "john"
  }
}

This policy allows John access to system, but he won't be able to do anything because he doesn't have any other permissions. This demonstrates the deny all approach of the policy engine.

Default namespace

When John logs in the system will automatically assign a default namespace so that John need not specify a full namespace for all activites. The generated default namespace is based on the name claim, so would be "/sandbox/john". If the automatic namespace isn't to your liking you may override it with a specified namesapce. The following policy accomplishes this.

auth::/
{
  if (auth_server@apcera.me->name == "john")
  {
    defaultNamespace "/sandbox/developer/john"
  }
}

Grant job permissions

John is a developer and will be deploying code, creating jobs, etc. He will need permissions on job::/ resources to do this.

Let's create a developer role that we can assign John membership in.

job::/sandbox/[name]
{
  if (auth_server@apcera.me->name == "john")
  {
    permit create, read, update, delete
    permit start, stop, promote, ssh
  }
}

The first rule in the policy checks for a token bearing the name "john." If true the policy engine dynamically creates the dev role and grants John membership. The next rule maps the specified permissions to the role. This illustrates how you can chain rules and underscores the flexibility of the policy language.

Rule summary

Here is a summary of the issuer, type, and value for each claim type from the policy example. See Policy Syntax for reference.

Rule 1

Claim Issuer Type Operator Value
Antecedent Google email == john@gmail.com
Consequent 1 ENGINE permit N/A issue
Consequent 2 ENGINE name N/A john

Rule 2

Claim Issuer Type Operator Value
Antecedent auth_server@apcera.me name == john
Consequent ENGINE defaultNamespace N/A /sandbox/developer/john

Rule 3

Claim Issuer Type Operator Value
Antecedent auth_server@apcera.me name == john
Consequent ENGINE role N/A dev

Rule 4

Claim Issuer Type Operator Value
Antecedent ENGINE role == dev
Consequent ENGINE permit N/A create, read, update, delete
Consequent ENGINE permit N/A start, stop, promote

Note that the consequent claim of the Rule 1 is the antecedent claim of Rules 2 and 3. Likewise the consequent of Rule 3 is the antecedent of Rule 4. The consequent claim of one rule can be the antecedent claim of another, which lets the system chain rules together until the policy engine has derived all the consequents that it can for each antecedent. The chaining mechanism lets you to implement role-based policies without basing policy exclusively on role.

Note also that while the claims are the same, the syntax is different. The consequent claim of Rule 3 (role dev) is an assertion by the policy engine. The antecedent claim of Rule 4 claim 1 (if (permit == "dev")) is a boolean evaluation. It is the same claim expressed differently because of its position in the rule. This lets you represent the same claim in different ways to keep the syntax natural.

Test policy

Using APC, target your cluster and login:

apc target skanderna.io

apc login

CD to local directory /sample-apps/example-java-hello. (Go here to set up your environment.

Deploy the program using the following command:

apc app create hello-world --batch

Expected results: App creation fails. Why? The error message states the reason:

Not allowed to create "package::/sandbox/john::hello-world" by policy: missing claim "permit create" on "package::/sandbox/john::hello-world"

Add package permissions

When you create a job you create a package in the same namespace as the job. Thus, the following policy added to the john-policy.pol document lets John create a package for his Java app:

package::/sandbox/[name]
{
  if (auth_server@apcera.me->name == "john")
  {
     permit create, read, update, delete
     permit use
  }
}

In addition, John will need permissions on the root package realm to use the Java Runtime Environment (JRE). Apps have dependencies which can be shared across the cluster. The following policy lets John use the packages in a shared namespace:

on package::/shared/pkg {
  { permit read, use }
}

And, optionally, the following policy to enforce package resolution:

job::/ {
  {
    package.allow "package::/shared/pkg"
  }

  if (dependency equals os.linux)
  {
    package.default "package::/apcera/pkg/os::ubuntu-14.04"
  }
}

Add resource quota

Using policy to set resource quota limits is as simple as the following:

quota::/ {
  { max.package.size 2GB }
}

Create routes

Say John wants to create a route for his app. The following policy lets him do this:

route::/http/<tld>/<domain-name>/sandbox/[name]
{
  if (job fqnMatch "job::/sandbox/john")
  {
    permit map
  }
}

In addition, the job must permit the mapping:

job::/sandbox/john
{
  if (auth_server@apcera.me->name == john)
  {
    permit create, read, update, delete
    permit start, stop, promote, ssh, map
  }
}

Create a service

Say John needs to create and bind to services. If he attempts to do so now, he would not be able to because he does not have policy. Adding the following policy lets John create services:

job::/sandbox/john
{
  if (auth_server@apcera.me->name == john)
  {
    permit create, read, update, delete
    permit start, stop, promote, ssh, map, link, bind
  }
}

But, if John were to run the following APC command to create a service and bind a job to that service:

apc service create mysql-service --provider /shared/providers::mysql --job my-app

It would fail with a policy error. Why?

For services, you need permissions to create and bind that service. The policy rule on the service realm authorizes service creation and binding, so what is the problem?

Error: Not allowed to bind “job::/sandbox/job::my-app” to “service::/sanbox/job::mysql” by policy: missing claim “permit bind” on “job::/sandbox/job::myapp”

Because the binding betweeen a service and a job is a two-way handshake. Both the service and the job must permit the binding.

To fix the error, update the job permissions to permit service binding:

job::/sandbox/[name]
{
  if (auth_server@apcera.me->name == "john")
  {
    permit create, read, update, delete
    permit start, stop, promote, bind
  }
}

The above service exmaple assumes that John also has permission to use service gateway and providers resources to create services. See the Policy Examples for details.

Policy on policy

Lastly, say you want to now allow John to edit other policies in the system. The following allows him to do this.

policy::/sandbox/[name] {

  if (auth_server@apcera.me->name=="[name]" && ResType == "audit") {
    permit read, update
  }

  if (auth_server@apcera.me->name=="[name]" && ResType == "cluster") {
    permit read, update
  }

  if (auth_server@apcera.me->name=="[name]" && ResType == "gateway") {
    permit read, update
  }

  if (auth_server@apcera.me->name=="[name]" && ResType == "job") {
    permit read, update
  }

  if (auth_server@apcera.me->name=="[name]" && ResType == "package") {
    permit read, update
  }

  if (auth_server@apcera.me->name=="[name]" && ResType == "policy") {
    permit read, update
  }

  if (auth_server@apcera.me->name=="[name]" && ResType == "provider") {
    permit read, update
  }

  if (auth_server@apcera.me->name=="[name]" && ResType == "route") {
    permit read, update
  }

  if (auth_server@apcera.me->name=="[name]" && ResType == "sempiperule") {
    permit read, update
  }

  if (auth_server@apcera.me->name=="[name]" && ResType == "service") {
    permit update
  }

  if (auth_server@apcera.me->name=="[name]" && ResType == "stagpipe") {
    permit update
  }
}