Skip to content
mcpd
no daemon

Accounts and organisations

People log in, belong to organisations, and reach projects through a role.

Two ways to turn it on

--signup lets anyone create an account. This is how the hosted mcpd at mcpd.sh runs. --accounts turns on the same accounts, organisations, roles, single sign-on and SCIM without public sign-up, so a deployment on your own cluster lets people in by invitation, by single sign-on or by SCIM. In both, configured keys keep working beside accounts, and a request with no credential is refused.

your own cluster, with single sign-on from the config file

mcpd serve --accounts --config-file mcpd.json \ --public-url https://mcpd.example.com \ --smtp-host smtp.example.com --smtp-from no-reply@example.com

Organisations and roles

Every account project belongs to an organisation. An account made by sign-up gets an organisation of its own with one project in it. An organisation can own up to 100 projects, and members join it by invitation. Each member has one role, and the role maps onto the six key scopes on every project the organisation owns:

RoleScopesManages the organisation
viewerreadno
memberingest, read, write, execute, workerno
adminall sixmembers below owner, projects, keys, SSO, SCIM
ownerall sixeverything, including owners, quotas and deleting it

A project role replaces a viewer's or member's role on one project: none, viewer, member or admin. Admins and owners keep their reach on every project.

A session carries the scopes the account's role allows on each project of its organisations. An API key carries one scope on one project, and works only while the role of the account that made it still allows that scope, so demoting or removing a member disarms their keys at once. Nothing an account holds reaches a project of an organisation it does not belong to. An organisation id that is not yours answers 404, so the answer does not say which organisations exist.

inviting a viewer
$ curl -s -X POST $MCPD/v1/api/orgs/org_7c1e08b53a9f/invites -H "Authorization: Bearer $SESSION" -d '{"email":"kim@example.com","role":"viewer"}'{"id":"inv_3f9c2e1b","email":"kim@example.com","role":"viewer",...}$ curl -s -X POST $MCPD/v1/api/account/keys -H "Authorization: Bearer $KIM" -d '{"scope":"ingest","project":"prj_a3f9c2e1b7d4"}'{"error":"forbidden","message":"your role on the project \"prj_a3f9c2e1b7d4\" does not allow the ingest scope"}

The account lifecycle

Sign-up answers the same way whether or not the address is in use, and mails a link: for a new account it confirms the address and logs in; for an address in use it says so and offers a reset. Password reset works the same way. An address is confirmed before the account can log in. An account can change its password, which ends its other sessions, and its address, which changes when a link sent to the new address is opened. It can list its sessions and end any of them, and delete itself: every organisation it is the only member of goes with it, and the stored signals and runs of those organisations' projects are erased.

Two-step log-in uses an authenticator app. Setting it up returns a secret and a QR code, confirming a code turns it on and returns ten single-use recovery codes, and from then on log-in, password reset and account deletion need a code. A code used once is refused if sent again.

Mail goes through the SMTP server in --smtp-host. With none, links are written to the daemon's log at info level, which is what a laptop or a closed cluster needs. With --signup on a public address and no SMTP server, the doors that need mail answer 503 and the daemon warns at start, because a link in a log is one the operator could use.

Where SMTP ports are closed, post mail over HTTPS instead: --mail-http-url names an endpoint and --mail-http-token its bearer token. Each mail is one JSON POST of to, subject and text, and the endpoint picks the sender. A Cloudflare Email Worker is a few lines:

export default {
  async fetch(request, env) {
    if (request.headers.get('authorization') !== `Bearer ${env.MAIL_TOKEN}`)
      return new Response('unauthorized', { status: 401 })
    const { to, subject, text } = await request.json()
    await env.EMAIL.send({ from: 'no-reply@example.com', to, subject, text })
    return Response.json({ sent: true })
  },
}

Single sign-on

An organisation can log its members in with any OpenID Connect provider: Google, Microsoft Entra ID, Okta, Auth0 or your own. GitHub works through an OAuth app. An admin sets the issuer, client id and client secret under the organisation's SSO settings, and registers the redirect URL shown there with the provider. On your own cluster the same settings go in the config file, with the secret read from an environment variable.

The flow is the authorization code flow with PKCE, a one-time state and a nonce, and a secret the starting browser keeps, so a login link cannot be handed to somebody else. The ID token's signature is checked against the provider's published keys, with its issuer, audience, expiry and nonce.

An organisation claims email domains. "Log in with SSO" finds the organisation by the address's domain, and a new account is created only for an address in a claimed domain. A person is found by their identity at the provider, and an existing account is never taken over: an account that is not a member must be invited and accept first. A single sign-on session reaches that organisation's projects only, and lasts 24 hours. With enforcement on, members whose address is in a claimed domain cannot log in with a password, and turning it on ends their password sessions.

On the hosted mcpd an admin can claim the domain of their own confirmed address. A domain belongs to one organisation.

Groups and SCIM

Group roles map identity provider groups onto roles. With a groups claim set, single sign-on gives a member the highest role among their groups, or the default role when none maps. A group never grants owner, and an owner keeps their role.

SCIM 2.0 lets your identity provider create, change and remove users and groups. Create a SCIM token under the organisation's SSO settings and give your provider the base URL shown with it. The service answers /scim/v2/Users, /scim/v2/Groups, /scim/v2/ServiceProviderConfig, /scim/v2/Schemas and /scim/v2/ResourceTypes, with filtering on userName, externalId, emails.value and displayName, and PATCH.

  • A user in a claimed domain with no account gets one, which the organisation manages.
  • A user whose address already has an account is sent an invitation, and becomes a member when the account accepts it.
  • active: false ends the membership, the account's keys on the organisation's projects and its sessions. An account the organisation manages is deactivated.
  • Deleting a user deprovisions it the same way, and deletes an account the organisation manages.

Quotas and usage

An organisation's projects share one ceiling on signals per minute, so one tenant cannot starve the others and adding projects does not add allowance. --org-quota-per-minute sets the ceiling an organisation gets and the most it may set; with --signup it is 60000. --org-retention-days sets how long its projects keep data, and an owner may choose a shorter window. The usage panel shows the ceiling, what was stored in the current minute, and what was stored and refused since the daemon started.

The audit log

Every change to membership, roles, projects, keys, single sign-on and SCIM is written to the audit log against the organisation's id, with the acting account's email, whether it was allowed or refused. Admins and owners read it in the interface or with GET /v1/api/orgs/{org_id}/audit. See security for every action.