Configuration¶
The app is configured by
cloud_registry/config.yaml,
the base config, which documents its values. To override some of them, write
them to a config overlay file and list it in environment variable
FOCA_CONFIG_OVERLAY (several files separated by :); FOCA merges overlays
into the config on startup (see the FOCA deployment
guide). The Helm
chart uses the same mechanism: it renders its config value
(in a values file per deployment) into an overlay. Overlays cannot change the
address the image listens on: server.host and server.port apply to the
development server only, not to Gunicorn (see Server).
Deployment configuration¶
The registry works with any OpenID Connect provider that issues access tokens
as per RFC 9068 (JWTs). The
identity provider, its claims, the groups mapped to roles and the roles of
users on services are deployment configuration: the base config has no
defaults for them, so each deployment sets them in an overlay (or in the
chart's config value), otherwise the app refuses to start:
security.auth.issuers: the trusted token issuers (theissclaim of their tokens, e.g.,https://login.example.org/oidc/); tokens of other issuers are rejected (see Authentication). Usually one; for several, see Several token issuers;security.auth.audience: the client IDs of the apps whose tokens are accepted (theaudclaim, e.g., of the registry's front end), unlesssecurity.auth.verify_audienceisfalse, which accepts tokens for any audience (not recommended);security.access_control.claim_groups: the claim with the groups of users (e.g.,groups), needed for admins and roles of groups; add it tosecurity.auth.userinfo_claimsif the identity provider releases it via user info only, not in access tokens (asname, which the registry shows to owners of services);security.access_control.group_mapping: the group(s) whose members are admins (values of the group claim, see Access control), orsecurity.access_control.require_admin: falseto run without admins.
For example:
security:
auth:
issuers:
- https://login.example.org/oidc/
audience:
- registry-portal
userinfo_claims:
- groups
- name
access_control:
claim_groups: groups
group_mapping:
registry-admins: admin
Entitlement-style groups¶
Some identity providers release groups as entitlements, i.e., URNs, in a
multi-valued claim (here entitlement) other than groups, and only via user
info:
security:
auth:
issuers:
- https://idp.example.org/
audience:
- <client ID of the front end>
userinfo_claims:
- entitlement
- name
- email
- email_verified
access_control:
claim_groups: entitlement
group_mapping:
"urn:example:group:admins#issuer.example.org": admin
Settings for specific identity providers belong to the documentation of the deployment, not here.
Notes:
audiencelists the client IDs of the apps that call the registry, usually the front end's (theaudclaim of its access tokens).- The group (the URN above is illustrative) is a value of the claim. Quote
such keys in YAML, as they contain
:and#. - Without the claim in
userinfo_claims, no user gets the admin role or roles of groups; withoutemailandemail_verified, the email addresses of users who accept invitations are not stored (see Invitations). - Clients must request the scope that releases the claim when logging in
(here the one for
entitlement, besidesopenidand the scopes forname,emailandemail_verified), and the identity provider must allow it for the client. - The registry accepts only access tokens with the
typheaderat+jwt(security.auth.verify_token_type, enabled by default). Before going live, check that the identity provider issues such tokens to the front end, and that theiraudmatchesaudience: otherwise requests fail with401 Unauthorized. - Roles of groups are granted with the URL-encoded group in the path (see Roles of groups).
Roles of users and groups on services are granted by owners and admins via
the API, not configured (see Roles on
services). The policies
(security.access_control.policies, see FOCA's access
control)
say what each role may do. They are named, so that an overlay can change,
add or remove single rules by name (FOCA's named
entries),
e.g., remove the rule that lets any authenticated user register services, so
that only admins can:
An overlay that gives the policies as a list replaces all rules of the base
config instead; FOCA refuses to start if an overlay gives named rules after
one that gives a list. Don't rename the roles owner and maintainer
(security.access_control.resource_roles), as the API is written for them.
The API specification (api.specs) is named registry, too, so that an
overlay can change it by name, e.g., to require login for
reads.
FOCA's service clients (security.auth.service_clients,
security.auth.on_behalf_of_header) are not supported; the app refuses to
start with them (see Access control).
Several token issuers¶
Deployments can trust several token issuers, e.g., an institutional login
besides a community one. Their users are then told apart by issuer: FOCA
namespaces the identifiers of users (sub) by issuer, as
<namespace>|<sub> (by default, the namespace is the issuer, e.g.,
https://login.example.org/oidc/|abc123), so equal identifiers of
different issuers are different users, also as owners and maintainers of
services and as the users who added service types. GET /me returns the
namespaced identifier (userId), which owners use to grant roles (see
Roles on services).
Issuers can be listed with settings of their own (FOCA's IssuerConfig),
which replace the global ones for their tokens: namespace, audience,
claim_groups and userinfo_claims, e.g.:
security:
auth:
issuers:
# Trusted so far: keeps its users' identifiers bare, so that
# their stored roles stay valid
- issuer: https://login.example.org/oidc/
namespace: ""
# Added: its users are namespaced (`https://login.other.org/|<sub>`)
- issuer: https://login.other.org/
audience:
- registry-portal-other
# Its groups don't count (e.g., for admins), as they could
# collide with those of the first issuer
claim_groups: ""
audience:
- registry-portal
Group claim values are not namespaced: groups of different issuers with
the same name are the same group, also for the admin groups
(security.access_control.group_mapping) and roles of
groups. Only take groups from issuers
that can't issue each other's groups, e.g., by setting claim_groups: ""
for the others. To add an issuer to a running deployment, see
Upgrading.
Local overrides¶
Docker Compose mounts
config.dev.yaml,
an overlay for local development that trusts a fictitious token issuer
(https://login.example.org/oidc/; set your own in a local override), sets an
example group claim (groups), accepts tokens for any audience and http
service URLs, and requires no admins. To override more values locally, put
them in config.local.yaml and mount it in a docker-compose.override.yaml
(both are ignored by Git):
services:
cloud-registry:
environment:
FOCA_CONFIG_OVERLAY: /etc/cloud-registry/config.dev.yaml:/etc/cloud-registry/config.local.yaml
volumes:
- ./config.local.yaml:/etc/cloud-registry/config.local.yaml:ro
Public URLs¶
Public URLs, e.g., in Location headers, are built from the public URL of
the app as clients see it, FOCA's
server.public_url,
including any path prefix of a reverse proxy (e.g.,
https://registry.example.org/ or https://example.org/registry/). The app
refuses to start without it. The Helm chart sets it to
https://<hostName>/, or to its publicUrl value; other deployments must
set it (the Docker Compose setup sets http://localhost:8080 in
config.dev.yaml).
It replaces custom.endpoints.service of earlier versions; see
Upgrading.
Service URL schemes¶
Clients send requests to the registered services, often with access tokens,
so service URLs (url) must use one of the schemes listed in
custom.endpoints.services.url_schemes: https only by default; http must
be allowed explicitly (e.g., [http, https]). Registrations (POST
/services, PUT /services/{serviceId}) of services with other schemes are
rejected with 400 Bad Request (code scheme-not-allowed, see
Errors), naming the allowed schemes. Only http and
https can be listed, and the list must not be empty. Links
(documentationUrl, organization.url) can be HTTP(S) URLs either way, and
contactUrl also a mailto: URL (see Registrations).
Clients read the allowed schemes from the service info (see Registration
rules). The Docker Compose setup allows http
too, in
config.dev.yaml,
e.g., for services running locally; the Helm chart keeps the default (set
config.custom.endpoints.services.url_schemes to change it).
On startup, the app warns of stored services whose URLs use other schemes
(e.g., http URLs registered while http was allowed): they are still
served, but can only be updated with an allowed scheme.
Service info import¶
The registry fetches the service info of services when users import it, from public addresses only:
custom:
endpoints:
services:
service_info:
# Networks to fetch from besides public addresses; none by default
private_networks:
- 10.1.0.0/16
# How often each user may have service info fetched per hour
fetches_per_hour: 30
private_networks (CIDR notation, IPv4 or IPv6) is for services in private
networks, e.g., in tests or inside an organization. Whatever it permits, users can
reach through the registry, and tell from its responses whether something there
serves service info, so keep it narrow, and never permit the addresses of cloud
metadata (169.254.169.254) or of the registry's own infrastructure.
The registry connects directly: proxy settings of the environment (e.g.,
HTTPS_PROXY) don't apply. Above fetches_per_hour (a positive number; each user
on their own, admins too; failed fetches count, as they also limit probing), users
get 429 Too Many Requests. So do requests while two others of the same worker are
fetching, so that slow addresses leave threads for other requests. These responses have
Retry-After, which the default security.cors.expose_headers lets browser apps on
other origins read: keep it if you set your own list. As a second layer, the
Helm chart can restrict the app's
connections.
Service URLs must use an allowed scheme, so http services
can only be imported if http is allowed (with the chart's policy, also add port 80).
Service info sync¶
Services can be synced with their service info, which is on by default, every 24 hours:
custom:
endpoints:
services:
service_info:
# Whether services can be synced
sync: true
# Time between syncs of a service, in hours (1 to 8760)
sync_interval_hours: 24
The interval must be at least an hour, so that the registry doesn't fetch from the registered services more often than their service info changes, and at most 8760 hours (365 days); the app refuses to start with other values.
Clients read both from the service info (serviceInfoSync of the registration
rules), e.g., to show how often services are synced.
Without sync (sync: false), nothing is fetched periodically, PUT
/services/{serviceId}/sync returns 409 Conflict, no service has a label, and URLs
aren't locked; what was stored stays, and applies again when sync is turned back on,
except for services whose URL changed meanwhile: their sync ends.
Imports keep working, as users start them; to stop all fetches, block the app's
connections.
How it runs: each worker runs a background thread, without a task queue, and whether or not requests come in. Once a minute (or each interval, if shorter), it syncs the services that are due, one after the other. Workers and replicas share the work through the database, without any setup: a service is claimed for five minutes in a single write, so that only one of them syncs it. With a single worker, this costs one indexed query per minute, besides the fetches.
On restarts, nothing is lost, as the times are stored: services that became due
meanwhile are synced at the first check, and those of a worker that died when their
claim expires. A shorter interval applies at once, on startup. A longer one applies
after the next sync of each service. If the app is down for longer than two
intervals, labels are out-of-sync until the next sync.
What reasons reveal: the reasons of syncs are served,
and a registrant controls the DNS of their host name. By pointing it elsewhere after
turning sync on, they can learn whether a name resolves to a public address and, for
permitted networks, whether something answers there: slowly,
as it takes one probe per service and interval. To limit who can read them, require
login for reads and restrict the rules
get-service-sync and list-syncs.
Service environments¶
The GA4GH specification describes the environment of a service
(environment) as a free string, and suggests prod, test, dev and
staging. So that filters by environment work (e.g., without Prod or
production besides prod), registrations may only use the values listed
in custom.endpoints.services.environments: these four by default.
Registrations (POST /services, PUT /services/{serviceId}) with other
values are rejected with 400 Bad Request (code invalid-value, see
Errors), naming the allowed values
(case-sensitive); services without environment are accepted. An empty
list allows any value. For example, in a config overlay (with the Helm
chart, in config), with an environment of the registry from the list, as
the app requires (see below):
custom:
endpoints:
services:
environments:
- production
- testing
service_info:
service:
environment: production
Clients that let users choose an environment (e.g., UIs) must offer the same values: they read them from the service info rather than configuring them (see Registration rules). Stored services with other values are served as they are, and only checked when they are next written; the app logs a warning on startup that names them.
The registry must describe itself with an allowed value, too: the app refuses
to start if the environment of its service info
(service_info.service.environment) isn't in the list, with an error that
names both settings. An empty list allows any value, and a service info
without environment (null in an overlay) always passes. The defaults fit
each other (dev is in the default list), so this only concerns deployments
that change one of the two: environments without dev, for example, needs
service_info.service.environment set to one of the listed values, too.
Service types¶
custom.access_control.service_types sets who may add service
types:
open(the default): any authenticated user, explicitly (POST /services/types) or by registering a service with a new type;controlled: admins only; other users can only register services with registered types, so that the registry offers a curated list of types;moderated: any authenticated user, but the types of users other than admins, and the services that have them, are pending until an admin approves them (see Pending approvals), so that admins curate the list without being asked for each type.
Users other than admins may only have added
custom.access_control.max_unused_service_types types that no service has
(default: 10; null for no limit), in modes open and moderated (in mode
controlled, they can't add types at all): further types are rejected
(409), whether added explicitly or by registering or changing services,
until services have them or admins remove them (approved types count too).
Types become unused when their services are deleted or change their type
(except for versions that syncs leave behind), so
the limit bounds how many types each user can add without services (users
can still leave more types unused by registering services with new types and
deleting them, one registration per type). Set 0 to let only admins add
types: users other than admins can then only register services with types
that are registered already, as in mode controlled. Concurrent requests of
a user can exceed the limit by their number. Clients read the mode and the
limit from the service info, rather than configuring them (see
Registration rules), which gives the limit as
0 in mode controlled, whatever its value.
A version of a type that a sync leaves behind stops counting for the user the service counts for, if that user added it and no other service has it, so that services that release new versions keep syncing (versions that other users added still count for them). An owner can therefore leave types behind by changing the version in the service info of their service before each sync:
- in mode
open, one per sync: per service and interval (sync_interval_hours) for the syncs of the app, and up tofetches_per_hour(default: 30) per hour and user for syncs that users start (PUT /services/{serviceId}/sync); - in mode
moderated, new versions from syncs are pending until an admin approves them, and pending ones still count; - in mode
controlled, or with the limit0, syncs don't add types.
The limit is per user, so accounts that anyone can create (e.g., at an open
identity provider) can still add types, and pending types in mode
moderated, without bound. Reads don't slow down with the number of
pending types, as services are marked as pending when stored, but GET
/services/types and GET /approvals list them all. Rate-limit write
requests, at least POST /services/types and POST /services, at the
ingress or reverse proxy in front of the app (see Rate
limits), and remove unwanted types as an admin
(DELETE /services/types).
Only admins can remove types, in every mode. Set this option rather than
removing the policy add-service-types (in security.access_control.policies),
which only applies to POST /services/types: users also add types by
registering services, and GET /me (mayAddServiceTypes) only reflects
this option. Overlays that give the policies as a list must include that
rule, and list-approvals for GET /approvals. For example, in a config
overlay (with the Helm chart, in config):
Services per user¶
Users other than admins may only have registered
custom.access_control.max_services_per_user of the registered services
(default: 50; null for no limit; 0 to let only admins register
services): further registrations (POST /services) are rejected with 409
Conflict and the code services-limit, until the user deletes services (or
admins do). Updates, renames, roles and accepted invitations never fail on
the limit, and services registered by admins (in admin
mode) count for nobody. A service counts for
the user who registered it as long as it is registered, whatever its roles
later: giving a service to other users or a group doesn't free a place. The
limit holds also for concurrent registrations of a user (only
registrations and deletions while an instance of the app starts may make a
counter differ by one each, until the next start). Clients read it
from the service info (see Registration rules).
The app stores who registered each service with the service (not served),
and keeps a counter per user (collection service_counts). On startup, it
corrects the counters (e.g., after a crash between storing a service and
counting it). For example, in a config overlay (with the Helm chart, in
config):
The limit is per account, so users who can create accounts at will (e.g., with an identity provider open to anyone) can still register more services; rate-limit write requests at the ingress as well (see Rate limits).
Invitations¶
custom.access_control.invitations sets how long invitations to
services are valid (expiry_days, default:
7), for how many days expired ones that weren't accepted are kept before they
are removed (keep_expired_days, default: 30, 0 removes them when they
expire), and how many that haven't expired, open or accepted, a service may
have (max_per_service, default: 20; null for no limit). Invitations expire
after at most 3650 days and are kept for at most 365 days: the app refuses to
start with larger values, and there is no value for keeping them forever. For
example, in a config overlay (with the Helm chart, in config):
Changes of keep_expired_days take effect when the app starts, without manual
steps in the database: the app changes the TTL index that removes them, and logs
a warning with the old and the new value (the chart restarts the app when its
config changes). Lowering the value removes invitations that are past the new
value soon after the start; raising it can't bring back those removed already.
The value is in the database once for all instances of the app, and the last
instance to start sets it, so an instance with the previous config that restarts
during a rollout sets it back, until the next start; check for that warning if
you rely on a higher value.
Expired invitations don't count towards max_per_service, and lists of
invitations aren't paginated: a service can have about max_per_service * (1 +
keep_expired_days / expiry_days) stored invitations, about 105 by default, so
avoid combining a few expiry_days with many keep_expired_days.
Request size limit¶
Request bodies are limited to server.max_content_length bytes (default:
1048576, i.e., 1 MiB), by
FOCA. Larger
requests are rejected with 413 Content Too Large before the access token is
checked and the body is parsed: requests whose Content-Length exceeds the
limit are rejected without reading the body (and requests with an invalid
Content-Length with 400). Bodies of unknown size (Transfer-Encoding:
chunked) are read up to one byte over the limit, and rejected if they are
larger. The app refuses to start with null (which would remove the limit in
FOCA).
Set the same limit at the ingress or reverse proxy in front of the app, so that it rejects larger requests itself; the Helm chart sets none, as it depends on the proxy (e.g., ingress-nginx limits request bodies to 1 MiB by default, the app's default; see Ingress).
Server¶
The image serves the app with Gunicorn on port 8080,
with FOCA's settings for apps in containers (foca.gunicorn_config; see
Running in
production
in FOCA's docs): one worker process with 4 threads, and idle keep-alive
connections closed after 100 seconds. Keep the keep-alive timeout longer than
that of a reverse proxy in front of the app for its connections to the app
(e.g., 60 seconds for ingress-nginx, 90 seconds for Traefik): otherwise, the
proxy may reuse a connection just as Gunicorn closes it, and the request
fails (502). To change the settings, set GUNICORN_CMD_ARGS to Gunicorn
options (e.g., --workers 2
--threads 8); Docker Compose passes it on from the host, and the Helm chart
sets it from its cloudRegistry.gunicorn values.
Each worker loads the whole app (about 55 MiB of memory), with its own
database client, and runs the startup checks (e.g., of the service info and
the admin groups); if they fail, Gunicorn exits. Gunicorn always listens on
0.0.0.0:8080 (its bind setting): server.host and server.port of the
config apply only to Flask's development server
(python -m cloud_registry.app), which is not for production, and changing
them does not move the image's traffic.
Serve the app behind a reverse proxy that buffers requests (e.g.,
ingress-nginx, or Traefik with its buffering middleware): Gunicorn's
threads read requests themselves, so slow clients can tie them up.
Service info¶
GET /service-info serves the service info from the config
(service_info.service), without the database; it is served by
FOCA. On
startup, the service info is checked against the GA4GH Service schema, and
the app refuses to start if it is invalid, naming the invalid field. version
defaults to the version of the installed app (service_info.package);
createdAt and updatedAt are served only if set, and must be RFC 3339
date-times (e.g., 2020-11-08T00:00:00Z). The defaults describe the app, not a
deployment: id (org.example.cloud-registry) and organization (Example
Organization, https://example.org) are placeholders with reserved example
domains, and there is no contactUrl. Deployments set their own id (unique,
e.g., in reverse domain name notation), name, description, organization,
contactUrl and environment (the default is dev; it must be one of the
allowed values, or the app refuses to start); while
id or the name or URL of organization are the placeholders, the app logs a
warning on startup, naming the fields to set (also with Docker Compose, whose
development config keeps them). Fields and list items set to null
(e.g., in a config overlay) are not served. The app adds the registration
rules (registrationRules), built from the
custom config on each request (service_info.runtime_fields): keep that
option, and don't set registrationRules in service_info.service (not
even to null), so that they are always the rules the registry enforces;
the app refuses to start otherwise. To change the service info, change
the config and restart the app (the Helm chart restarts it when the config
value changes). If admins listed the
registry among its services, the app then
also brings that entry up to date.
Previously, service info was set via the API; see
Upgrading.