Taskcluster Scopes

Introduction to Scopes...

By Jonas Finnemann Jensen

Scope Design Goals

  • Delegate authority with a high degree of granularity
  • Facilitate lightweight authorization
  • Check if a scope-set is the subset of another scope-set

  • Keep it simple and stupid
  • Stay as declarative as possible
  • Be completely dynamic

Formal Definitions

Let $\Sigma$ be the alphabet of unicode characters and $\Sigma^*$ be the language of all strings over $\Sigma$.

Definition (Scope-Set): A scope-set $s \subseteq \Sigma^*$ is a set of strings.

Given a scope-set $s$ we write $C(s)$ to denote the complete scope-set: $$C(s) = s \cup \{ab \mid a* \in s \text{ and } ab \in \Sigma^*\}$$
Definition (Satisfiability):
A scope-set $s$ is said to satisfy $s'$ if $C(s') \subseteq \mathcal C(s)$, denoted $s' \sqsubseteq s$.


Note,

  • $(\sqsubseteq, \mathcal P(\Sigma^*))$ is a partially ordered set,
  • the scope set $\{*\}$ satisfies all other scope-sets, $C(\{*\}) = \Sigma^*$.

$\sqsubseteq$ Examples on Scope-Sets

$$ \begin{align} X &\sqsubseteq Y \quad \text{// } X \text{ is satisfied by } Y \\ \{\text{queue:create-task:*} \} &\sqsubseteq \{\text{queue:*}\}\\ \{\text{queue:*}, \text{auth:list-clients} \} &\sqsubseteq \{\text{queue:*}, \text{auth:*}\}\\ \{\text{auth:list-clients} \} &\sqsubseteq \{\text{queue:*}, \text{auth:list-clients} \}\\ &\phantom{\sqsubseteq \{} \text{// Separators are just eye-candy} \\ \{\text{queue:create}, \text{queue:d*} \} &\sqsubseteq \{\text{queue:*}\}\\ &\phantom{\sqsubseteq \{} \text{// But prefix must match} \\ \{\text{queue} \} &\not\sqsubseteq \{\text{queue:*}\}\\ &\phantom{\sqsubseteq \{} \text{// While any suffix is optional} \\ \{\text{queue:} \} &\sqsubseteq \{\text{queue:*}\}\\ &\phantom{\sqsubseteq \{} \text{// Expansion at end only} \\ \{\text{auth:list-clients} \} &\not\sqsubseteq \{\text{auth:*-clients}\} \\ \textit{required scopes} &\sqsubseteq \textit{possessed scopes} \text{ // in general} \end{align} $$

Authorizing Requests

We have $C = (\textit{clientId}, \textit{accessToken})$ with scopes: $\{\text{queue:*}, \text{index:*}\}$


var taskcluster = require('taskcluster-client');

// Create client object with credentials
var queue = new taskcluster.Queue({credentials: C});

// Create task
await queue.createTask(taskcluster.slugid(), {
  created:        taskcluster.fromNowJSON(),
  deadline:       taskcluster.fromNowJSON('2 days 3 hours'),
  provisionerId:  'aws-provisioner-v1',
  workerType:     'tutorial',
  payload:        {...},
  metadata:       {...}
});
Docs it says createTask requires:
queue:create-task:<provisionerId>/<workerType>
Hence, in this case we need:
queue:create-task:aws-provisioner-v1/tutorial
Which we have by $``\text{queue:*}``$.

Authorizing Requests w. Authorized Scopes

We have $C = (\textit{clientId}, \textit{accessToken})$ with scopes: $\{\text{queue:*}, \text{index:*}\}$


var taskcluster = require('taskcluster-client');

// Create client object with credentials
var queue = new taskcluster.Queue({
  credentials: C,
  authorizedScopes: ['queue:create-task:aws-provisioner-v1/*']
});

// Create task
await queue.createTask(taskcluster.slugid(), {
  // We've forced provisionerId using `authorizedScopes`
  provisionerId:  'aws-provisioner-v1',
  ...
});
Useful when creating a task on behalf of a third-party.
  • Proxying a request w. authorization
  • Creating task on behalf of a push

Temporary Credentials

We have $C = (\textit{clientId}, \textit{accessToken})$ with scopes: $\{\text{queue:*}, \text{index:*}\}$


var taskcluster = require('taskcluster-client');

// Create temporary credentials
var tempCreds = taskcluster.createTemporaryCredentials({
  start:        new Date(),
  expiry:       taskcluster.fromNow('4 hours'),
  credentials:  C,
  scopes:       ['queue:create-task:aws-provisioner-v1/*']
});

// Create client object with temporary credentials
var queue = new taskcluster.Queue({
  credentials: tempCreds
});

// Create task
await queue.createTask(taskcluster.slugid(), {
  // We've forced provisionerId using `scopes` given to temporary credentials
  provisionerId:  'aws-provisioner-v1',
  ...
});
Only works because $\text{queue:*}$ satisfies the scope given.

Delegating with task.scopes (1)

We have $C = (\textit{clientId}, \textit{accessToken})$ with scopes: $\{\text{queue:*}, \text{index:*}\}$


// Create task
await queue.createTask(taskcluster.slugid(), {
  provisionerId:  'aws-provisioner-v1',
  workerType:     'tutorial',
                  // Allows worker to know the creator have these scopes
  scopes:         ['index:insert-task:gecko.v1.*']
  ...
  payload: {
    features:     {taskclusterProxy: true} // Start auth proxy
    ...
  }
});
From docs createTask now requires:
queue:create-task:aws-provisioner-v1/tutorial
index:insert-task:gecko.v1.*
Docker-worker will use auth-proxy will use the authorizedScopes features to proxy requests.

Delegating with task.scopes (2)

We have $C = (\textit{clientId}, \textit{accessToken})$ with scopes: $\{\text{queue:*}, \text{index:*}\}$


// Create task
await queue.createTask(taskcluster.slugid(), {
  provisionerId:  'aws-provisioner-v1',
  workerType:     'tutorial',
                  // Allows worker to know the creator have this scope
  scopes:         ['docker-worker:cache:jonasfj-*']
  ...
  payload: {
    cache: {
      'jonasfj-cache': '/var/apt/cache/'
    }
    ...
  }
});
From docs createTask now requires:
queue:create-task:aws-provisioner-v1/tutorial
docker-worker:cache:jonasfj-*
To mount cache worker requires: $$ \{\text{docker-worker:cache:jonasfj-cache}\} \sqsubseteq \text{task.scopes} $$ Or it rejects the task.

Common Scope Patterns

Merely guidelines...

Service specific

    <service>:<action>:<resource>
- service: queue, index, auth, docker-worker - action: create-task, create-artifact, insert-task, list-clients - resource: <provisionerId>/<workerType>, <path>, <docker-image> * resource usually uses "/" as separator, * resource is sometimes omitted if not relevant.
Role specific

    assume:<role>:<qualifier>
- role: worker-type, scheduler-id, worker-id - qualifier: <provisionerId>/<workerType>, <schedulerId>/<taskGroupId>

Usually required by multiple API end-points.
Think of the scope "assume:worker-type:aws-provisioner-v1/tutorial" as the authority to act as a worker of this type.

Continuous Scopes

Idea:
We want a scope that limits the artifact size a client can upload.
Solution:
  • queue:artifact-size:100mb
  • queue:artifact-size:500mb
  • queue:artifact-size:1gb
  • queue:artifact-size:5gb
  • queue:artifact-size:10gb

Downside, satisfiability doesn't follow semantics, e.g. $$ \{\text{queue:artifact-size:100mb}\} \not\sqsubseteq \{\text{queue:artifact-size:1gb}\} $$
Hence, client with the 1gb scope can't create temporary credentials with the 500mb scope.

Summary

  • A scope is a string (you make a new scope by requiring it)
  • Clients have a set of scopes
  • APIs/resources requires a set of scopes (maybe dynamic)
  • A scope $``\text{a*}``$ satisfies any scopes prefixed $a$

Downside, We cannot have continuous scopes.
Given $Y$ we cannot check if the client has some scope "queue:artifact-size:X" where $Y \leq X$.

THE END

- docs.taskcluster.net/auth
- #taskcluster at irc.mozilla.org