Skip to content

Namespaces and Permissions

Workflow jobs run on execution environments.

Sometimes you want to limit some execution environments to some workflows. It can be that you have a preprod environment and a noprod environment, and they should not be mixed, or that you want to share an orchestrator instance with multiple departments within your organization.

namespaces are used to provide this functionality.

Sometimes you want to further control access to resources. You may want to give one of your users read-only access to your running workflows, or you may want to prevent another one from registering new agents.

Access control grants permission to access resources.

In the first example below you will learn how to assign namespaces to trusted authorities, and in the second example, you will learn to assign permissions to authentication tokens.

What is a Namespace?

A namespace has a name (letters, digits, and hyphens are allowed). It ties resources such as workflows and execution environments together.

You grant access permissions to namespaces.

There is no limit on the number of namespaces you can use in an orchestrator instance.

There is a default namespace, default, which is used if you do not specify a specific namespace.

Configuring

The OpenTestFactory orchestrator uses JWT tokens to ensure proper authorization. Each request to the orchestrator needs a bearer token.

Typically, on startup, you provide a public key or a set of public keys, and you use those public keys to verify incoming bearer tokens.

If the signature is verified, the request’s access is granted. If not otherwise specified, the request’s access is granted to the default namespace only.

To enable namespaces on your orchestrator instance, you have to declare which namespace is accessible to which token or set of tokens (tokens whose signatures are matched by a given public key).

Depending on your organization’s size, you can generate and allocate the tokens, or delegate the creation of those tokens to a trusted authority. You have finer control over accesses if you generate and allocate the tokens yourself, but this can be time-consuming.

Assigning Namespaces to Trusted Authorities

In this first example, you have an administration team and two departments, ‘Triangle’ and ‘Square’. Those 3 entities manage their tokens.

The administration team members must have full access to the orchestrator. Members of the Triangle department should have access to the triangle and triangle1 namespaces, and members of the Square department should only have access to the square namespace.

You will deploy this configuration using docker-compose.

Start by creating a directory in which you will put all the relevant elements:

mkdir example1
cd example1

Trusted Keys

In the real world, those teams would provide you with a public key to use to validate their tokens. Here, create three private/public key pairs in a data subdirectory:

mkdir data
cd data
openssl genrsa -out admin.pem 4096
openssl rsa -pubout -in admin.pem -out admin.pub
openssl genrsa -out triangle.pem 4096
openssl rsa -pubout -in triangle.pem -out triangle.pub
openssl genrsa -out square.pem 4096
openssl rsa -pubout -in square.pem -out square.pub
cd ..

Each orchestrator service has a trusted_authorities entry in its configuration file. This entry is typically something like:

# ...
contexts:
- context:
    trusted_authorities:
    - /etc/opentf/*
    # ...
  name: allinone

You will set up your docker-compose manifest to place the public keys there.

Defining Trusted Authorities’ Attributes

You then create a mapping file (a ‘trusted authorities’ file) with the following content, to match your access requirements:

trustedkeys_auth_file
/etc/opentf/admin.pub,Administrator,,"*"
/etc/opentf/triangle.pub,Department Triangle,,"triangle,triangle1"
/etc/opentf/square.pub,Department Square,,"square"

Trusted authorities are tested in order. If there are other public keys in the /etc/opentf directory, they will only allow access to the default namespace.

If you replace "square" in the example above with "square,square1" (be sure to keep the surrounding double quotes), tokens whose signatures are verified by square.pub will have access to both square and square1 namespaces.

If you replace "triangle,triangle1" in the example above with "*", tokens whose signatures are verified by triangle.pub will have access to all namespaces (including square and square1). In other words, they will have the same access as those whose signatures are verified by admin.pub.

This ‘trusted authorities’ file is specified by setting the OPENTF_TRUSTEDKEYS_AUTH_FILE environment variable in your orchestrator container instance.

Deployment

Your docker-compose.yml is straightforward. You create volumes so that the public keys and configuration file are available to your instance, you expose the standard ports, define the required variable, and that’s it:

docker-compose.yml
version: "3.4"
services:
  orchestrator:
    container_name: orchestrator
    image: opentestfactory/allinone:latest
    restart: always
    environment:
      OPENTF_TRUSTEDKEYS_AUTH_FILE: "/app/trustedkeys_auth_file"
    volumes:
    - type: bind
      source: ./data/admin.pub
      target: /etc/opentf/admin.pub
    - type: bind
      source: ./data/triangle.pub
      target: /etc/opentf/triangle.pub
    - type: bind
      source: ./data/square.pub
      target: /etc/opentf/square.pub
    - type: bind
      source: ./trustedkeys_auth_file
      target: /app/trustedkeys_auth_file
    ports:
    - "7774:7774"    # receptionist
    - "7775:7775"    # observer
    - "7776:7776"    # killswitch
    - "38368:38368"  # eventbus
    - "34537:34537"  # localstore
    - "24368:24368"  # agent channel
    - "12312:12312"  # quality gate

If you have many public keys to configure, you may want to bind a directory, not each public key, using something like the following. Be sure to move the private keys (*.pem) out of your local data directory, though, as they do not have to be on your orchestrator instance.

    - type: bind
      source: ./data
      target: /etc/opentf
    # ...

Running docker-compose up -d will start your instance.

For more information, see “Authenticating.”

Observable Effects

In this section, you will use the opentf-ctl tool and a token generated from each of your above-trusted keys. You will tie them to users ‘alice’, ‘carol’, and ‘dave’.

You can use the opentf-ctl tool to generate your tokens from your private keys:

opentf-ctl generate token using data/admin.pem
opentf-ctl generate token using data/triangle.pem
opentf-ctl generate token using data/square.pem

The configuration file below, once completed with the tokens you generated above, can be saved to ~/.opentf/config (or %HOME%\.opentf\config if you are using Windows):

~/.opentf/config
apiVersion: opentestfactory.org/v1alpha1
kind: CtlConfig
contexts:
- context:
    orchestrator: default
    user: alice
  name: default
current-context: default
orchestrators:
- name: default
  orchestrator:
    insecure-skip-tls-verify: true
    ports:
      eventbus: 38368
      killswitch: 7776
      observer: 7775
      receptionist: 7774
      localstore: 34537
      qualitygate: 12312
    server: http://127.0.0.1
users:
- name: alice
  user:
    token: ey...
- name: carol
  user:
    token: ey...
- name: dave
  user:
    token: ey...

When Carol attempts to list available execution environments, she will get an empty list, as there are currently no execution environments accessible from the triangle or triangle1 namespaces:

opentf-ctl get channels --user carol
NAME  NAMESPACES  TAGS  LAST_REFRESH_TIMESTAMP  STATUS

Performing the same command using Alice’s token will provide results, as Alice has access to the default namespace:

opentf-ctl get channels --user alice
NAME            NAMESPACES  TAGS                      LAST_REFRESH_TIMESTAMP  STATUS
robotframework  default     ssh:linux:robotframework  2022-06-08T10:14:50.39  IDLE
cypress         default     ssh:linux:cypress         2022-06-08T10:14:50.39  IDLE
cucumber        default     ssh:linux:cucumber        2022-06-08T10:14:50.39  IDLE
junit           foo:bar     ssh:linux:junit           2022-06-08T10:14:50.39  IDLE

When Dave tries to run a workflow on the foo namespace, he gets an error:

bad.yaml
metadata:
  name: Oops
  namespace: foo
jobs:
  job_1:
    runs-on: inception
    steps:
      - run: echo 'oh no'
opentf-ctl run workflow bad.yaml --user dave
Error: Token not allowed to run workflows in namespace foo.

Assigning Permissions to Tokens

You may want finer control on namespace accesses, or finer token/namespace associations.

To do so, you need to know the tokens you want to grant specific permissions to, and enable the Attributes-based access control (ABAC) mode.

This second example builds on the first one.

Enabling ABAC

This mode is enabled by setting the OPENTF_AUTHORIZATION_MODE environment variable to ABAC,JWT in your orchestrator container instance.

Note

The order is important here. Using JWT,ABAC results in different effects: if JWT is specified first, a token verified by a public key known to the orchestrator would match, and the ABAC policies would be skipped: the token would have no specific permissions.

Conversely, using ABAC,JWT implies that a token known to the ABAC module does not inherit the permissions granted by a verifying public key.

Token Association

You then have to define the mapping you want between the tokens and their permissions.

Here, you will reuse the tokens you generated in the previous example. Carol would normally only have access to namespaces triangle and triangle1, but you will grant her more privileges and Dave has left the company, so you will remove his privileges.

For each token you want to grant specific permissions to, give it a name and a unique ID:

token_auth_file
ey...,Carol Doe,carol
ey...,Dave Doe,dave

You only add entries in this file for tokens you want to refine. Other tokens, as long as they are verified by one of your trusted authorities, will have access to the resources granted by that trusted authority.

Make this ‘static tokens’ file available to your orchestrator container instance and set the OPENTF_TOKEN_AUTH_FILE environment variable to its location.

For more information, see “Static Tokens File.”

Policies

Finally, you have to define the policies that apply to those unique IDs.

For example, you want to grant Carol read-only access to all namespaces’ workflows, and full access to the triangle, square, and circle namespaces. And Dave should have his privileges removed.

policy.jsonl
{"apiVersion": "abac.opentestfactory.org/v1alpha1", "kind": "Policy", "spec": {"user": "carol", "namespace": "*", "resource": "workflows", "apiGroup": "*", "readonly": true}}
{"apiVersion": "abac.opentestfactory.org/v1alpha1", "kind": "Policy", "spec": {"user": "carol", "namespace": "triangle", "resource": "*", "apiGroup": "*"}}
{"apiVersion": "abac.opentestfactory.org/v1alpha1", "kind": "Policy", "spec": {"user": "carol", "namespace": "square", "resource": "*", "apiGroup": "*"}}
{"apiVersion": "abac.opentestfactory.org/v1alpha1", "kind": "Policy", "spec": {"user": "carol", "namespace": "circle", "resource": "*", "apiGroup": "*"}}

Policies are checked in order. If a policy matches the request, access is granted. If no policy matches the request, access is denied.

Make this ‘policy’ file available to your orchestrator container instance, and set the OPENTF_AUTHORIZATION_POLICY_FILE environment variable to this file’s path.

For more information, see “Policy File Format.”

Deployment

Your docker-compose.yml is straightforward. You create volumes so that the public keys and configuration files are available to your instance, you expose the standard ports, define the required variables, and that’s it:

docker-compose.yml
version: "3.4"
services:
  orchestrator:
    container_name: orchestrator
    image: opentestfactory/allinone:latest
    restart: always
    environment:
      OPENTF_TRUSTEDKEYS_AUTH_FILE: "/app/trustedkeys_auth_file"
      OPENTF_AUTHORIZATION_MODE: "ABAC,JWT"
      OPENTF_TOKEN_AUTH_FILE: "/app/token_auth_file"
      OPENTF_AUTHORIZATION_POLICY_FILE: "/app/policy.jsonl"
      DEBUG_LEVEL: "DEBUG"
    volumes:
    - type: bind
      source: ./data/admin.pub
      target: /etc/opentf/admin.pub
    - type: bind
      source: ./data/triangle.pub
      target: /etc/opentf/triangle.pub
    - type: bind
      source: ./data/square.pub
      target: /etc/opentf/square.pub
    - type: bind
      source: ./trustedkeys_auth_file
      target: /app/trustedkeys_auth_file
    - type: bind
      source: ./token_auth_file
      target: /app/token_auth_file
    - type: bind
      source: ./policy.jsonl
      target: /app/policy.jsonl
    ports:
    - "7774:7774"    # receptionist
    - "7775:7775"    # observer
    - "7776:7776"    # killswitch
    - "38368:38368"  # eventbus
    - "34537:34537"  # localstore
    - "24368:24368"  # agent channel
    - "12312:12312"  # quality gate

Running docker-compose up -d will start your instance.

Observable Effects

Carol no longer has full access to resources on namespace triangle1. She is listed in the static token file; hence her permissions are granted by the defined policies, overriding those granted by the trusted authority used to sign her token.

She can still view workflows on namespace triangle1 (as well as on all other namespaces) due to the first policy, though.

Dave is listed in the static token file above but has no policy, so his requests are rejected.

And Alice, not being listed in the static token file but having her token validated by the admin.pub trusted authority, will keep her full access to all resources of this orchestrator’s instance.

Next Steps

Namespaces are only the beginning of what you can do to control access to your orchestrator resources. Here are some helpful resources for taking your next steps with namespaces and permissions: