Connections to External Services¶
Organizations connect accounts at services they already use, such as a CDN, product analytics or monitoring, and applications use those connections without ever holding the credential. SystemAuth stores the connections, seals their credentials, tests them, and hands applications authenticated HTTP clients. The design is ADR-017; the mechanism is connectforge.
Enable connections¶
Add a connections section to the SystemAuth configuration. It needs a
database and the role-based authorization mode (the default).
| Key | Meaning |
|---|---|
keys_dir |
Directory holding the key ring and its registry. Back it up with the database: without it, stored credentials cannot be read |
vault |
omnivault URI of the deployment's secret store: env://, file:///etc/systemauth/secrets, or a registered provider |
root_key |
Path in the vault of the key ring's root key: 32 random bytes as base64 or hex |
allow_private_egress |
Development only: lets provider requests reach loopback and private addresses over plain HTTP |
Create the root key once and keep it in the secret store, not beside
keys_dir:
systemauth migrate (and --migrate at startup) creates the connection
tables. On PostgreSQL it also enables row-level security on them, keyed on
owner_id, the organization.
systemauth serve runs the key lifecycle jobs. A host that embeds SystemAuth
calls Service.RunJobs itself.
Register providers¶
Providers are registered by the application that embeds SystemAuth, through
systemauthsvc.Options.Providers. A static-token service needs only a
manifest and where its token goes:
reg := provider.NewRegistry()
p, err := apikey.New(apikey.Config{
Manifest: provider.Manifest{
Key: "cloudflare", DisplayName: "Cloudflare",
BaseURLs: []string{"https://api.cloudflare.com"},
AuthMethods: []provider.AuthMethod{{
Key: "api_token",
Fields: []provider.Field{{Name: "token", Label: "API token", Secret: true, Required: true}},
}},
},
Verify: apikey.Request{Path: "/client/v4/user/tokens/verify"},
})
if err != nil {
return err
}
if err := reg.Register(p); err != nil {
return err
}
svc, err := systemauthsvc.New(ctx, cfg, systemauthsvc.Options{Providers: reg})
Requests for a provider may reach only its manifest's BaseURLs, over
HTTPS, never a private address, and never by a redirect to another host.
Use a connection in process¶
An application in the same process gets an authenticated client from the broker and hands it to the provider's SDK:
ctx = rls.ContextWithTenant(ctx, orgID) // row-level security on PostgreSQL
hc, err := svc.Connections().Broker.HTTPClient(ctx,
connections.OwnerFor(orgID),
connectforge.Principal{Kind: connectforge.PrincipalUser, ID: userID.String()},
connectionID,
connectforge.Use{Consumer: connectforge.Consumer{Kind: "application", ID: "dashboards"}, Purpose: "traffic"},
)
if err != nil {
return err
}
client := providersdk.New(providersdk.WithHTTPClient(hc)) // any SDK that accepts an *http.Client
The broker checks that the principal may use the connection and that it is connected. Make a client per unit of work: a disconnect applies to the next client.
HTTP API¶
Every request carries a SystemAuth access token. Reads need the
systemforge:connections:read scope; changes need
systemforge:connections:write.
| Method and path | Action |
|---|---|
GET /api/v1/providers |
Provider manifests: methods, fields, scopes |
GET /api/v1/organizations/{org}/connections |
Connections the caller may view (?provider=, ?state=) |
POST /api/v1/organizations/{org}/connections |
Connect: {"provider", "authMethod", "displayName", "fields": {...}} |
GET /api/v1/organizations/{org}/connections/{id} |
One connection |
PATCH /api/v1/organizations/{org}/connections/{id} |
Rename: {"displayName"} |
POST /api/v1/organizations/{org}/connections/{id}/test |
Validate again; the result is in health |
POST /api/v1/organizations/{org}/connections/{id}/reconnect |
Replace the credential: {"fields": {...}} |
GET /api/v1/organizations/{org}/connections/{id}/bindings |
What depends on the connection |
PUT /api/v1/organizations/{org}/connections/{id}/bindings |
Record a dependency: {"consumerKind", "consumerId", "purpose"} |
DELETE /api/v1/organizations/{org}/connections/{id}/bindings?kind=&id=&purpose= |
Remove one |
GET /api/v1/organizations/{org}/connections/{id}/grants |
Who holds a role on the connection |
PUT /api/v1/organizations/{org}/connections/{id}/grants/{principal} |
Give a role: {"role": "viewer" \| "contributor" \| "maintainer"} |
DELETE /api/v1/organizations/{org}/connections/{id}/grants/{principal} |
Take it back |
DELETE /api/v1/organizations/{org}/connections/{id} |
Disconnect and destroy the credential |
A connection is validated with its provider before it is stored, and a
rejected credential stores nothing (422). No response contains a
credential. A connection the caller may not view is reported as 404.
Reconnecting¶
When a token expires or is revoked, replace it in place with reconnect. The
new credential is validated with the provider first and must belong to the
same account; otherwise nothing changes (422). Everything bound to the
connection keeps working.
Who may do what¶
| Role | View | Use | Manage, share, disconnect |
|---|---|---|---|
| Organization owner, admin | Yes | Yes | Yes |
| Member | Yes | With a grant | With a grant |
| Collaborator | With a grant | With a grant | With a grant |
| The connection's creator | Yes | Yes | Yes |
Creating a connection needs an owner or admin. Grants are resource roles
(ADR-008) on type systemforge_connection:
| Role | Lets the principal |
|---|---|
viewer |
See the connection |
contributor |
Use it, and record what depends on it |
maintainer |
Manage it: rename, test, reconnect, share, disconnect |
Giving someone a role on a connection who is not in the organization makes
them a collaborator, with access to that connection only. Granting needs the
share action, which is maintainer, an owner or an admin.
Roles are never visible to the connection's provider; they decide who in your organization may use the stored credential, not what the credential can do.
Credentials and keys¶
A connection's credential is sealed under keys that SystemAuth manages
through keyforge, using the connections.keys_dir and connections.root_key
settings above. Back up the keys directory with the database: without it,
stored credentials cannot be read. See Key management.
Current limits¶
- Static API tokens only; GitHub App and OAuth connections follow.
- The role-based authorization mode only.
- The API acts for the token's principal as a user; service principals and agents use the broker in process.
- Connection events go to the server log until an audit store is configured.
- A disconnect does not yet require acknowledging the bindings it would stop;
list them first with
GET .../bindings.