Access control¶
Authentication¶
The TES API requires an access token of a trusted identity provider (OpenID Connect), sent as a bearer token, for all operations on tasks:
curl -X POST "http://localhost:8080/ga4gh/tes/v1/tasks" \
-H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d @task.json
GET /service-info, the specification (/ga4gh/tes/v1/openapi.json) and
the Swagger UI are public. Requests without a valid
token are rejected with 401 Unauthorized and a Bearer challenge
(WWW-Authenticate: Bearer, RFC 6750; with error="invalid_token" for
invalid tokens); requests with a valid token, but without the required role,
with 403 Forbidden (both as problem details).
proTES accepts only tokens that:
- are access tokens as per RFC 9068
(
typheaderat+jwt), not, e.g., ID tokens; - are issued by one of the trusted issuers (
security.auth.issuers, theissclaim), and verified with the issuer's keys (from its OpenID Connect discovery document); - are for one of the configured audiences (
security.auth.audience, theaudclaim), i.e., were issued to one of the apps that call proTES (e.g., its front ends, by their client IDs); - if
security.auth.clientsis set, were obtained by one of these clients (theclient_idclaim, orazpif there is none); - have not expired.
If your identity provider sets another typ header on its access tokens,
list it in security.auth.token_types; if it sets none, disable the check
with security.auth.verify_token_type: false (see FOCA's authentication
options).
Roles¶
| Role | May |
|---|---|
user |
create tasks, and list their own (POST /tasks, GET /tasks) |
owner (of a task) |
read and cancel the task (GET /tasks/{id}, POST /tasks/{id}:cancel) |
admin |
manage the access control rules (permissions API); not the tasks of others |
Roles are given to groups of the identity provider: the values of the claim
security.access_control.claim_groups (from the token, or from the user info
endpoint if listed in security.auth.userinfo_claims) are mapped to the
roles user and admin by security.access_control.group_mapping. Values
are matched exactly, so list each group whose members get a role, including
subgroups. The role owner is not mapped from groups: users get it on the
tasks they create (see Task ownership).
The roles and their permissions are the policies of
security.access_control.policies in the base config (see
FOCA's access control),
named rules (users-create-list-tasks, owners-get-cancel-tasks,
admins-list-add-permissions and admins-manage-permissions): a config
overlay adds, changes or removes (null) single rules by name, e.g., rules
that let users only list their tasks, and owners only read them:
security:
access_control:
policies:
users-create-list-tasks: [p, user, /ga4gh/tes/v1/tasks, (GET)|(HEAD)]
owners-get-cancel-tasks: [p, owner, "/ga4gh/tes/v1/tasks/:id", (GET)|(HEAD)]
The access control model of proTES (pro_tes/access_control/model.conf)
differs from FOCA's reference model in that the role admin may not do
everything: admins only have the rights that rules give them.
Task ownership¶
The user who creates a task (the sub claim of the access token, or the
claim set in security.access_control.claim_identity) becomes its owner:
GET /taskslists only the tasks of the caller, also for admins;GET /tasks/{id}andPOST /tasks/{id}:cancelare allowed for the owner only; everyone else gets403 Forbidden, also admins and for unknown tasks, so that responses don't tell which tasks exist. The owner gets404 Not Foundfor a task that was deleted from the database.
Tasks are not shared with other users, and admins can't read or cancel the
tasks of others through the API: tasks and their logs may hold sensitive
data, and users' entitlements to access data can't be passed on to others.
Owners keep access to their tasks if they lose the role user, but can't
create or list tasks then.
Owners are stored as resource-scoped roles of FOCA (g2 rules in the
collection policy_rules) on the task and on its cancellation, e.g., for
task ABC123:
g2, user:<sub>, owner, /ga4gh/tes/v1/tasks/ABC123
g2, user:<sub>, owner, /ga4gh/tes/v1/tasks/ABC123:cancel
The task document records its owner, too (user_id), e.g., for operators
looking for the tasks of a user (see Operations).
Each task adds two rules, and FOCA loads all rules on each request (and
creating or listing tasks loads them a few more times), so the time of each
request grows with the number of tasks (see FOCA's notes on
scaling).
Ownership is taken from the caller's own token only: proTES doesn't support
service clients, also acting for users, yet, and refuses to start if
security.auth.service_clients, security.auth.on_behalf_of_header or
security.access_control.client_mapping is set.
New tasks never get the identifier of a stored task, or of a task that owner roles are left on (e.g., of a deleted task), so that nobody gets access to the task of someone else with the same identifier.
A list replaces all rules of the base config. The configured rules replace
the stored ones atomically on each startup; if an overlay leaves no rules,
the stored ones are removed.
The role admin comes only from groups in group_mapping: the startup
check doesn't count g rules of the policies or of the permissions API.
Permissions API¶
Admins can list, add, change and delete access control rules at runtime
through FOCA's permissions
API at
/admin/access-control/permissions; everyone else gets 401 (no token) or
403. The rules are stored in the database (collection policy_rules of
taskStore, or of the database in MONGO_DBNAME if set); the configured
policies are restored on each startup. The Helm chart's Ingress (or
HTTPRoute, or Route) routes all paths to the app, including the permissions
API; to keep it off the internet, block /admin/ in front of the cluster
(see Exposing the app).
The permissions API can write any rule, also owner roles on tasks (g2
rules), so admins could make themselves owners of the tasks of others. FOCA
logs each request to it; give the role admin only to operators trusted
with the data of the tasks.
Deployment settings¶
Which identity providers are trusted, for which audiences, and which groups
have which role is deployment configuration: the base config has no
defaults, and the app refuses to start without trusted issuers, without an
audience (unless security.auth.verify_audience is false, which accepts
tokens for any app) and without a group mapped to the role admin (unless
custom.access_control.require_admin is false). Set them in a config
overlay, e.g.:
security:
auth:
issuers:
- https://login.example.org/oidc/
audience:
- my-portal-client-id
# if the identity provider releases groups via user info only
userinfo_claims:
- groups
access_control:
claim_groups: groups
group_mapping:
compute-users: user
compute-admins: admin
The app logs the groups of each role on startup, and warns if no group has
the role user (nobody can create tasks then) and if group_mapping is
set without claim_groups (no group gets a role then). It refuses to start
if access control is not enabled and enforced
(security.access_control.enabled and enforce), as users could access
the tasks of others then. The Docker Compose setup
uses config.dev.yaml, which accepts tokens for any audience and requires no
admins; set your issuer and groups there. For local tests without an
identity provider, the smoke test brings a fake one.
Credentials of TES instances¶
proTES authenticates at TES instances with credentials of its own, never
with those of its callers: the access tokens of requests are not forwarded,
and no credentials of callers are stored or queued for the Celery worker.
How proTES authenticates is set for each TES instance of
custom.tes.service_list, by auth:
client_credentials(the default): with an access token of proTES itself, which it gets from its identity provider with the OAuth 2.0 client credentials grant (RFC 6749, section 4.4). TES instances accept tokens of proTES' client (e.g., by theclient_idclaim of the token). proTES caches the token, and fetches a new one shortly before it expires (refresh_beforeseconds, at most half its lifetime), also in the Celery worker, which polls tasks for hours;basic: with HTTP Basic credentials from environment variables, whose names are set inbasic(username_env,password_env). This is a transitional mode for TES instances that don't accept tokens yet, and will be removed once all of them do;none: without credentials, e.g., for TES instances on an internal network that need none.
The client ID and secret of proTES are read from the environment variables
PROTES_CLIENT_ID and PROTES_CLIENT_SECRET, and Basic credentials from
the variables named in the config, e.g., set from Kubernetes Secrets; they
are never part of the config or the database. The app refuses to start if
one of them is not set for a TES instance that needs it, and warns of each
TES instance with basic or none. Set the TES instances and the identity
provider of proTES in a config overlay, e.g.:
custom:
tes:
service_list:
- url: https://tes.example.org
- url: https://tes.example.com
auth: basic
basic:
username_env: TES_EXAMPLE_COM_USERNAME
password_env: TES_EXAMPLE_COM_PASSWORD
client_credentials:
# The token endpoint is that of the issuer's OpenID Connect discovery
# document (`token_endpoint` sets it instead)
issuer: https://login.example.org/oidc/
# Scope of the tokens, if the identity provider needs one
scope: null
# `client_secret_basic` (HTTP Basic) or `client_secret_post`
auth_method: client_secret_basic
The Docker Compose setup passes PROTES_CLIENT_ID and
PROTES_CLIENT_SECRET of the host to the app and the Celery worker; with
the Helm chart, set them (and the variables of Basic credentials) from a
Secret with protes.extraEnv (see the chart's README). The Celery worker
checks the credentials on startup, too.
The client of proTES is registered at the identity provider for the client
credentials grant; it is a client of its own, not the client of the front
ends whose tokens proTES accepts (security.auth.audience). All TES
instances with client_credentials get the same token, so each of them
could use it at the others until it expires: configure only TES instances
that you trust, and keep the lifetime of the tokens short. Tasks are only
forwarded to configured TES instances, and to those discovered in a service
registry (custom.tes.discovery, which get the credentials of its auth;
only verified ones, see Discovery in a service
registry): TES instances
that middlewares add (tes_urls) are skipped, as proTES has no credentials
for them. Keep TES instances in the list until their tasks have finished:
tasks of TES instances that are no longer configured can't be tracked or
canceled (their state becomes UNKNOWN, and is tracked again once the TES
instance is configured again).
URLs of TES instances and of the identity provider must use https
(custom.tes.url_schemes); allow http for local tests only, as tokens and
credentials are sent unencrypted then. URLs of TES instances must not have
credentials (https://user:password@host).
Credentials in the URLs of inputs and outputs of tasks
(https://user:password@host/...) are not accepted either: such tasks are
rejected with 400 Bad Request, or, if
custom.controllers.post_task.url_credentials is strip, the credentials
are removed before the task is stored and forwarded (see
Tasks). Credentials that middlewares add are
always removed.
proTES doesn't tell TES instances on whose behalf it forwards tasks yet: they only see proTES. Tags with the identity of the user are planned, once their names are agreed with other services.