Auth
Auth decides who can sign in and what they can do. You must choose a backend. If MARIMOHUB_AUTH_BACKEND is unset, marimohub refuses to start instead of falling back to local auth.
Selector: MARIMOHUB_AUTH_BACKEND. Full variables: Configuration -> Auth.
Project roles decide who can edit a notebook. The editor sandbox-sharing policy controls whether those editors share one live sandbox or use exclusive ownership.
Choose a backend
| Backend | Selector | Use for |
|---|---|---|
| OIDC | oidc | Production with Google, Okta, Auth0 |
| Trusted proxy headers | proxy-header | oauth2-proxy, Google IAP, Tailscale Serve |
| Cloudflare Access | cloudflare-access | Workers deployments behind Access |
| Dev bypass | dev | Local development only |
Configure it
OIDC (production)
App-native OpenID Connect is the production backend. marimohub discovers the provider endpoints from /.well-known/openid-configuration. You supply the issuer, client credentials, and redirect URI.
MARIMOHUB_AUTH_BACKEND=oidc
MARIMOHUB_AUTH_OIDC_ISSUER=https://accounts.example.com
MARIMOHUB_AUTH_OIDC_CLIENT_ID=…
MARIMOHUB_AUTH_OIDC_CLIENT_SECRET=…
MARIMOHUB_AUTH_OIDC_REDIRECT_URI=https://hub.example.com/api/auth/callback
MARIMOHUB_AUTH_SESSION_SECRET=… # signs the session cookie (HS256, ≥32 bytes)
MARIMOHUB_AUTH_ALLOWED_EMAIL_DOMAINS=example.com # REQUIRED allowlist (verified email); `*` allows all
# MARIMOHUB_AUTH_OIDC_AUDIENCE=… # deprecated and ignored; aud must contain the client ID
# MARIMOHUB_AUTH_OIDC_PROMPT=consent # optional: override the default (select_account) OAuth prompt
# MARIMOHUB_AUTH_OIDC_SCOPES="openid email profile groups" # add only provider-required scopesThe redirect URI is always https://<your-host>/api/auth/callback. Register this exact value with your provider. A different value causes a redirect_uri_mismatch error. ALLOWED_EMAIL_DOMAINS is required. Set one or more domains, or set * to allow all.
The login email comes from UserInfo when it supplies email, otherwise from the ID token. UserInfo must match the validated ID token's sub.
By default, the email source must supply email_verified. Every present claim in either source must be boolean true or the exact string "true" (Cognito compatibility). MARIMOHUB_AUTH_OIDC_EMAIL_VERIFICATION=trusted-issuer permits missing claims only, including with a domain allowlist.
The signed session JWT has a 3,800-byte limit. If necessary, marimohub omits the profile picture first and the display name second. Required identity and authorization claims are never omitted. Login fails if they exceed the limit.
The issuer, callback, discovered authorization, and discovered logout endpoints must use HTTPS and cannot contain embedded credentials.
Groups and roles
Group authorization is optional and uses exact provider group IDs. Set a JSON Pointer to the provider array. Then set at least one group policy:
MARIMOHUB_AUTH_OIDC_GROUPS_CLAIM=/groups
MARIMOHUB_AUTH_OIDC_ALLOWED_GROUPS=hub-users
MARIMOHUB_AUTH_OIDC_SUPER_ADMIN_GROUPS=hub-platform-admins
MARIMOHUB_AUTH_OIDC_PROJECT_CREATION_GROUPS=hub-project-creators
MARIMOHUB_AUTH_OIDC_DEFAULT_APP_USER_GROUPS=hub-app-users
MARIMOHUB_AUTH_OIDC_DEFAULT_VIEWER_GROUPS=hub-viewers
MARIMOHUB_AUTH_OIDC_DEFAULT_EDITOR_GROUPS=hub-editors
MARIMOHUB_AUTH_OIDC_DEFAULT_MANAGER_GROUPS=hub-project-managersNested claims use JSON Pointer syntax, such as /realm_access/roles. Array elements use zero-based indices, such as /identities/0/groups. ALLOWED_GROUPS controls login. The other lists map groups to internal entitlements. The session stores mapped entitlements and only the selected membership group IDs. Without membership selection, the session stores no group IDs.
If ALLOWED_GROUPS is set, it must contain at least one group ID. An empty list fails at startup. Unset it to disable the login group restriction.
PROJECT_CREATION_GROUPS controls who can create projects:
- If unset, project creation policy applies, including the app-only exception.
- If the value is empty, only super admins can create projects.
- If the value contains group IDs, super admins and matching users can create projects.
If no super admin is configured, an empty value prevents every user from creating projects. Setting the variable implies MARIMOHUB_PROJECT_CREATION=restricted, which also works without group mapping; combining it with MARIMOHUB_PROJECT_CREATION=open is rejected at startup.
An empty value does not require GROUPS_CLAIM. A non-empty value requires the claim and creates a group-derived session entitlement.
Group sessions last at most one hour by default. This limit bounds the delay after an IdP removes a user from a group. Kernels inherit the session JWT expiry as a fixed authorization deadline. Active editors cannot extend it. Session reuse keeps the earliest caller credential deadline. At expiry, the lifecycle destroys the kernel and the proxy closes WebSockets. This teardown skips the final capture so that the kernel stops promptly. Periodic snapshots limit potential data loss.
Missing, malformed, or oversized group data cannot satisfy the login policy. marimohub accepts at most 200 group IDs. It does not resolve group-overage references from the provider. Configure the IdP to emit only the groups that marimohub needs. Group-derived roles and project-creation access apply to browser sessions and enabled external access tokens. They do not transfer to personal access tokens.
After you enable project-creation groups, matching users must sign in again. Existing sessions do not contain the new entitlement.
For a strict rollout, first deploy the new version without the variable. Then set the variable after all replicas run the new version.
The user ID is the OIDC sub within the configured issuer. The same sub from another issuer can identify a different person. Therefore, an issuer URL change is an identity migration. Reconcile stored owners and members before the change.
Generate a session secret with openssl rand -base64 32.
Group membership
To retain selected group IDs in the current credential, configure exact IDs, prefixes, or both:
MARIMOHUB_AUTH_OIDC_GROUPS_CLAIM=/groups
MARIMOHUB_AUTH_OIDC_MEMBERSHIP_GROUPS=hub-team-data,hub-team-ml
MARIMOHUB_AUTH_OIDC_MEMBERSHIP_GROUP_PREFIXES=/marimohub/Selection is case-sensitive: exact IDs match whole values; prefixes match the start of a value without a path boundary. Either list can select a group. Configure at most 200 exact IDs and 20 prefixes. The host trims spaces around each comma-separated entry. Empty lists and empty entries are invalid.
Use immutable group IDs
Matching is exact-string, and a matching group grants project roles. Configure the groups claim to emit immutable, IdP-unique IDs, such as Microsoft Entra object IDs or Okta group IDs, not display names. Never select groups that users can create or rename.
To select all groups, set MARIMOHUB_AUTH_OIDC_MEMBERSHIP_GROUPS='*'. * must be the only entry, and MARIMOHUB_AUTH_OIDC_MEMBERSHIP_GROUP_PREFIXES must be unset. Otherwise, startup fails. The same ID limits and credential expiry apply. The raw provider claim remains limited to 200 entries, including duplicates.
Only this membership setting treats * as a wildcard. Prefixes, admission lists, and role mappings remain literal; team-* is not a glob pattern.
IDs and prefixes must contain 1–128 Unicode characters, with no control characters, commas, or leading or trailing whitespace. Internal spaces, @, and leading / are valid. The host discards selected IDs that fail these rules; malformed provider claims still fail admission. A missing claim produces no groups.
The host removes duplicate IDs and sorts them by code unit. The selected set must fit within 32 groups and 1,280 UTF-8 bytes of JSON. Overflow fails browser login with auth_failed or rejects external-token authentication. The host does not truncate the set. If authentication exceeds these limits, narrow the group selection.
The cookie is signed, not encrypted, and GET /api/v1/me exposes selected groups. Do not select sensitive group names. The identity directory stores no groups. Logs contain counts only. oidc_session_issued records entitlements, retained groups, and unretainable (selected values that fail the ID rules). oidc_membership_groups_exceeded is an error-level event for overflow on both browser login and external access tokens. It records the retained count. External tokens also log oidc_access_token_rejected with reason membership_groups_exceeded.
Group changes at the IdP take effect when the authenticated groups refresh. Existing browser sessions can retain groups until the credential expires, within one hour. External OIDC access tokens use the same selection rules and retain groups until token expiry. Personal access tokens, service accounts, background work, and non-OIDC backends have no group source. Their /me response contains groups: [].
A matching project group membership grants its assigned role. Groups do not supply security labels or subject compartments.
For Microsoft Entra, select Groups assigned to the application to limit the provider claim. See Microsoft's group limits and application assignment. Group overage omits the claim and produces no retained groups. The host does not resolve overage references through Microsoft Graph.
Login-policy module
When a group mapping cannot express your access rule — for example, an approved department AND a minimum level AND a set of required attribute values — load a trusted login-policy module instead:
MARIMOHUB_AUTH_OIDC_LOGIN_POLICY_BACKEND=library
MARIMOHUB_AUTH_OIDC_LOGIN_POLICY_LIBRARY=/etc/marimohub/oidc-login-policy.mjs
# MARIMOHUB_AUTH_OIDC_LOGIN_POLICY_TIMEOUT_SECONDS=5 # 1–30; a timeout denies login
# MARIMOHUB_AUTH_OIDC_LOGIN_POLICY_SESSION_TTL_SECONDS=3600 # 300–3600The built-in adapter still completes all OIDC protocol work: discovery, PKCE, state and nonce, ID-token verification, UserInfo subject binding, email verification, and the email-domain allowlist. The module runs after that validation and before session signing. It receives the validated ID-token and UserInfo claims as separate read-only objects and returns one bounded result: an allow or deny decision, plus the built-in entitlements (super-admin, project-creator, default-role:app-user, default-role:viewer, default-role:editor, default-role:manager). project-creator permits creation on restricted deployments and for app-only users. MARIMOHUB_AUTH_OIDC_LOGIN_POLICY_BACKEND=none (or unset) disables the module.
A login-policy module cannot be combined with the group configuration above. It applies only to browser sessions. Personal access tokens receive neither its entitlements nor its groups.
The module runs in-process with server privileges. Bundle its dependencies into one .mjs file, pin its version, and mount the same artifact on every replica. Load errors stop server startup. Policy denials show a generic access-policy message. Errors, timeouts, and malformed results fail login with a generic sign-in error and a bounded operator log event.
An allow result can include groups: ['team-data', 'team-ml']. The host validates these IDs against the membership limits, deduplicates them, and sorts them. Omission produces groups: []. The module selects groups itself; no host selection filter applies. Invalid IDs fail login rather than being discarded.
The host keeps raw claims out of storage and logs. Only selected groups enter the signed cookie. Review module code to prevent it from logging or storing claim values.
Invalid group results fail login with one of these bounded problem codes: groups_not_an_array, invalid_group, too_many_groups, or groups_on_deny. The policy analyzer shows selected groups and supports expected-group assertions. Its login stage evaluates modules only, not environment-based group mappings.
The optional field keeps login-policy API version 1, but older hosts reject groups as unknown_result_field. Deploy a host version that supports groups on every replica before a module returns them.
Policy sessions last at most one hour, like group sessions, which bounds the delay after an attribute or policy change. A module change requires a server restart and takes effect on the next login.
The module controls login eligibility and deployment roles. It cannot see projects or notebooks, and entitlements never bypass project-role checks. See Security and the example module.
Google
- In the Google Cloud Console, open APIs & Services → Credentials.
- Create Credentials → OAuth client ID, application type Web application.
- Under Authorized redirect URIs, add
https://hub.example.com/api/auth/callback. - Copy the Client ID and Client secret.
MARIMOHUB_AUTH_OIDC_ISSUER=https://accounts.google.com
MARIMOHUB_AUTH_OIDC_CLIENT_ID=…apps.googleusercontent.com
MARIMOHUB_AUTH_OIDC_CLIENT_SECRET=…
MARIMOHUB_AUTH_ALLOWED_EMAIL_DOMAINS=example.com # a single domain is also sent to Google as the `hd` hintThe default OAuth prompt is select_account, which displays the Google account chooser. Set MARIMOHUB_AUTH_OIDC_PROMPT=consent to display the consent screen again.
See Google's OpenID Connect docs.
Microsoft Entra ID
- In the Entra admin center (or Azure Portal), go to App registrations → New registration.
- Set a Web redirect URI of
https://hub.example.com/api/auth/callback. - From Overview, copy the Application (client) ID and Directory (tenant) ID; under Certificates & secrets, create a client secret.
# tenant-scoped issuer (use `organizations` or `common` for multi-tenant)
MARIMOHUB_AUTH_OIDC_ISSUER=https://login.microsoftonline.com/<tenant-id>/v2.0
MARIMOHUB_AUTH_OIDC_CLIENT_ID=<application-client-id>
MARIMOHUB_AUTH_OIDC_CLIENT_SECRET=…Okta
- In the Okta Admin Console, open Applications → Create App Integration.
- Choose OIDC - OpenID Connect and Web Application.
- Add
https://hub.example.com/api/auth/callbackas a Sign-in redirect URI. - Copy the Client ID and Client secret from the app's General tab.
MARIMOHUB_AUTH_OIDC_ISSUER=https://<your-org>.okta.com
MARIMOHUB_AUTH_OIDC_CLIENT_ID=…
MARIMOHUB_AUTH_OIDC_CLIENT_SECRET=…If you use an Okta authorization server, the issuer is https://<your-org>.okta.com/oauth2/<server-id>. See Okta's OIDC docs.
Auth0
- In the Auth0 Dashboard, open Applications → Create Application and pick Regular Web Application.
- Under Settings → Allowed Callback URLs, add
https://hub.example.com/api/auth/callback. - Copy the Domain, Client ID, and Client Secret from Settings.
# note the trailing slash on the issuer
MARIMOHUB_AUTH_OIDC_ISSUER=https://<tenant>.auth0.com/
MARIMOHUB_AUTH_OIDC_CLIENT_ID=…
MARIMOHUB_AUTH_OIDC_CLIENT_SECRET=…See Auth0's OIDC docs.
External access tokens
The Hub can accept JWT access tokens from the browser-login OIDC issuer. This optional feature supports the API and the Node MCP server. Browser login and existing Hub tokens continue to work.
After you configure OIDC browser login, add these variables:
MARIMOHUB_AUTH_OIDC_ACCESS_TOKENS=on
MARIMOHUB_AUTH_OIDC_ACCESS_TOKEN_AUDIENCE=https://hub.example.com/mcp
# Optional. Otherwise, the Hub discovers JWKS from the configured OIDC issuer.
# MARIMOHUB_AUTH_OIDC_ACCESS_TOKEN_JWKS_URL=https://accounts.example.com/jwksIf MCP is enabled, set the audience to the exact public MCP URL. For example, a base URL of https://hub.example.com/hub requires https://hub.example.com/hub/mcp. The API accepts this same audience. For API-only deployments, choose a Hub resource audience that differs from the browser client ID.
The issuer and JWKS endpoint must use HTTPS without embedded credentials. Startup fails for incomplete configuration, non-OIDC backends, or custom OIDC login-policy modules. The audience and JWKS variables require MARIMOHUB_AUTH_OIDC_ACCESS_TOKENS=on.
Issuer requirements
Configure the issuer to issue JWT access tokens for the Hub resource with these claims:
| Claim | Requirement |
|---|---|
iss | Exact configured OIDC issuer |
aud | Configured access-token audience. Must not include the browser client ID. |
sub | Same subject as browser login |
email | User email that satisfies the domain allowlist |
email_verified | true or "true". Omission requires trusted-issuer. |
client_id | Nonempty ID of the OAuth client that requested the token |
iat, exp | Integer issuance and expiry times, in Unix seconds |
scope | Space-separated OAuth scopes, including a Hub grant scope |
The total token lifetime (exp - iat) cannot exceed 3,600 seconds. If group authorization uses a shorter session lifetime, that limit applies instead. The Hub rejects future iat or nbf values and expired tokens.
The token must use an asymmetric signature and a key from the trusted JWKS. Supported algorithms are RS256/384/512, PS256/384/512, ES256/384/512, and EdDSA. The Hub accepts an absent typ, JWT, at+jwt, or application/at+jwt header. It rejects opaque tokens, browser ID tokens, and tokens with a cnf binding. The Hub does not support DPoP or mutual-TLS token bindings on this path.
If group policies are configured, include those groups in the access token. The Hub applies the same email and group admission policies as browser login. It does not fetch UserInfo or reuse stored group claims for external authentication. Missing group claims grant no group entitlements and cannot satisfy a group allowlist.
The Hub maps sub directly to the existing user ID. The issuer must supply the same subject across browser login and external clients. The Hub does not link accounts by email or translate pairwise subjects. This feature supports user identities, not machine identities or client-credentials grants.
Scope grants
| OAuth scope | Permitted actions |
|---|---|
marimohub:read | Read projects and integrations |
marimohub:run | Read, use integrations, and run sessions |
marimohub:edit | Run, edit notebooks, and publish change requests |
marimohub:full | All actions available to a bearer credential |
Clients must request at least one grant scope. Multiple grant scopes combine their actions. The Hub ignores unrelated scopes and rejects tokens without a recognized grant scope. MCP also requires mcp:tools.
These scopes belong in the access token. MARIMOHUB_AUTH_OIDC_SCOPES controls browser login and does not request scopes for external clients.
Grants apply to all projects that the user can already access. They cannot increase the user's project permissions. External tokens cannot manage Hub tokens or access session-only administration, even for a super admin. Per-token project selection is not supported.
To call the API, send the access token as a bearer credential:
curl https://hub.example.com/api/v1/me \
-H "Authorization: Bearer $ACCESS_TOKEN"Expiry and revocation
The Hub checks token signatures locally with cached signing keys. It retrieves keys only from the configured or discovered JWKS endpoint. Failed token or key checks deny authentication, even with a valid browser cookie in the same request.
The Hub does not store, revoke, or refresh external tokens. It does not query the issuer for revocation status. Issuer-side revocation generally takes effect at token expiry. Hub user suspension blocks API and MCP access, with a cache delay of up to 30 seconds across server instances.
For client discovery and gateway behavior, see MCP external authorization.
Trusted proxy headers
Use this backend behind oauth2-proxy, Tailscale Serve, Google IAP, or another SSO proxy.
CAUTION: In header mode, block direct access to marimohub. The proxy must remove client-supplied identity headers.
When the proxy runs on the same host, bind marimohub to loopback so other machines cannot reach it directly:
MARIMOHUB_BIND_HOST=127.0.0.1Local processes can still connect, so the proxy must still remove client-supplied identity headers.
Both modes require MARIMOHUB_AUTH_ALLOWED_EMAIL_DOMAINS. Set * only to allow all authenticated domains.
oauth2-proxy can supply the default marimohub headers:
MARIMOHUB_AUTH_BACKEND=proxy-header
MARIMOHUB_AUTH_ALLOWED_EMAIL_DOMAINS=example.comoauth2-proxy sends these headers when --pass-user-headers=true. Current releases enable this option by default.
If you disable this option, marimohub receives no identity and returns 401.
For Nginx auth_request, enable --set-xauthrequest. Copy both response headers upstream. Then set their names in marimohub:
MARIMOHUB_AUTH_PROXY_HEADER=X-Auth-Request-Email,X-Auth-Request-UserFor a custom pair, set two comma-separated names:
MARIMOHUB_AUTH_PROXY_HEADER=X-Auth-Email,X-Auth-SubjectTailscale Serve supplies both values in one header:
MARIMOHUB_AUTH_BACKEND=proxy-header
MARIMOHUB_AUTH_PROXY_HEADER=Tailscale-User-Login
MARIMOHUB_AUTH_ALLOWED_EMAIL_DOMAINS=example.comFor Google IAP, set the signed-header JWT audience:
MARIMOHUB_AUTH_BACKEND=proxy-header
MARIMOHUB_AUTH_PROXY_JWT_AUDIENCE=/projects/123456789/global/backendServices/987654321
MARIMOHUB_AUTH_ALLOWED_EMAIL_DOMAINS=example.comThe audience selects JWT mode. The adapter uses the IAP header, issuer, and JWKS URL by default.
You can override these values for an IAP-compatible deployment:
MARIMOHUB_AUTH_PROXY_HEADER=X-Verified-Assertion
MARIMOHUB_AUTH_PROXY_JWT_ISSUER=https://issuer.example.com
MARIMOHUB_AUTH_PROXY_JWKS_URL=https://issuer.example.com/.well-known/jwks.jsonJWT mode accepts ES256 assertions only. It verifies the signature, issuer, audience, lifetime, subject, and email.
See the provider guides for oauth2-proxy headers, Tailscale Serve headers, and Google IAP signed headers.
Cloudflare Access
Cloudflare Access is used by the Workers entrypoint. It reads unprefixed runtime variables (AUTH_MODE, ACCESS_TEAM, ACCESS_AUD) from the Worker environment. See Deploying on Cloudflare.
Dev bypass
A fixed, unauthenticated identity for local development only — never use it in production (it lets anyone in as the same user).
MARIMOHUB_AUTH_BACKEND=dev
# all optional — these are the defaults:
MARIMOHUB_AUTH_DEV_USER_ID=user
MARIMOHUB_AUTH_DEV_EMAIL=user@localhost
MARIMOHUB_AUTH_DEV_NAME='Local Dev'Verify it
After deployment:
- Start the server without an authentication configuration error.
- Sign in through the configured provider.
- Create a project.
- Add a second user with a lower role.
- Verify that the second user has only the permitted access.
Production cautions
- Do not use
devauth for any deployment that serves real users. - Set
MARIMOHUB_AUTH_ALLOWED_EMAIL_DOMAINSfor OIDC and proxy-header. Use*only to allow all domains. - In proxy-header mode, block proxy bypasses and remove client-supplied identity headers.
- Review
MARIMOHUB_DEFAULT_ROLEbefore launch. The default is permissive for a trusted single-tenant deployment. - A project default overrides the deployment default in both directions. A project manager can open a project to every signed-in user, even when
MARIMOHUB_DEFAULT_ROLE=none. Theproject.default_access_changedalert reports each change. Super admins can reset the project toinherit. - Treat auth errors as fail-closed until configuration proves otherwise.
Authorization roles
Roles apply per project and rank app-user < viewer < editor < manager < admin. This rank selects role grants. Runtime permissions differ: app users can always use apps, while viewer access depends on viewer mode. Project owners and super admins are admin. Members can receive any role up to manager; existing admin memberships remain valid.
| Capability | app-user | viewer | editor | manager | admin |
|---|---|---|---|---|---|
| Discover app titles and project names | x | x | x | x | x |
| Start and use live apps | x | * | x | x | x |
| Read notebook source, versions, and saved outputs | x | x | x | x | |
| Create notebooks; edit content and restore versions | x | x | x | ||
| Run persistent editors; stop or restart shared apps | x | x | x | ||
| Delete notebooks; manage projects, members, and app links | x | x |
* Viewer runtime access depends on MARIMOHUB_VIEWER_MODE. App users can always use apps but cannot open editors, including ephemeral sandboxes. See App user permissions and rollout for assignment and stakeholder navigation.
The server enforces permissions; insufficient write access returns 403 FORBIDDEN. Security labels and credential scopes can further restrict access.
Project creation is open by default, except for users with only app-user access. App-only users need super-admin status or the project-creator entitlement, even when creation is open. MARIMOHUB_PROJECT_CREATION=restricted requires those grants for everyone. An OIDC group mapping or login-policy module can grant project-creator. The creator becomes the project owner.
Members: users, email invites, and IdP groups
Each member row identifies one user id, email, or IdP group. Managers and admins can add members. The highest matching membership role overrides default access, even when the default is higher. Owners and super admins retain admin access.
Removing a row removes that grant. Other memberships, default access, ownership, or super-admin status can still grant access. Duplicate requests for an existing member return 409.
Users and email invites
User ids match exactly. The server resolves known emails to user ids and stores unknown emails as pending invites. An invite matches the login email without regard to case and grants access on the first sign-in. The login email grants access, so OIDC requires email verification by default.
After sign-in, the next membership write or maintenance sweep replaces the email row with a user-id row and preserves its role. If a legacy roster contains both forms, the server keeps one user-id row with the higher role. The email row grants access until this replacement occurs.
The add-member picker searches signed-in users by email, name, or id through GET /api/v1/users/search. Search requires a default role of viewer or higher, super-admin status, or membership in an active project (including ownership). User and group rows are visible to all project readers. Pending invite emails are visible only to managers, admins, and the invitee.
IdP groups
A group membership grants its role to callers whose authenticated groups contain the exact group id. Groups can receive app-user, viewer, editor, or manager. They cannot receive admin. A group named like a user id or email remains a separate member.
Group ids are case-sensitive. The server does not trim whitespace or normalize Unicode. See group selection and ID limits for valid IDs and OIDC configuration. Group creation requires OIDC membership selection or a login-policy module that can carry groups. The API reports this capability as project_groups.available in /api/v1/capabilities.
IdP group members differ from the deployment-wide OIDC group mappings. A mapping such as MARIMOHUB_AUTH_OIDC_DEFAULT_EDITOR_GROUPS grants a default role on every project. An IdP group member grants its role on one project only.
To add a group in Project Access:
- Select IdP group.
- Enter the exact group id.
- Choose a role.
- Select Add group.
The picker suggests your own authenticated groups. It cannot search the IdP directory or confirm that a group exists. You can add a group you do not belong to. Group ids are visible to project readers and subscribed alert destinations. Do not use secrets as group ids.
Group grants require browser SSO sessions or enabled external OIDC access tokens. PATs do not inherit IdP groups and need another applicable project grant within their credential scope. IdP changes take effect when the credential's groups refresh. See session freshness.
The API uses these routes:
| Action | Request |
|---|---|
| Add | POST /api/v1/projects/{pid}/group-members with { "group": "/teams/data", "role": "editor" } |
| Change role | PUT /api/v1/projects/{pid}/group-members?group=%2Fteams%2Fdata with { "role": "viewer" } |
| Remove | DELETE /api/v1/projects/{pid}/group-members?group=%2Fteams%2Fdata |
A successful add returns 201. Encode the group as a query parameter, including literal +, /, and & characters. POST /api/v1/projects/{pid}/members accepts only user_id or email. The /members/{uid} route addresses users and email invites only. GET /api/v1/projects/{pid}/members returns one list with user rows, pending email rows, and group rows. Existing group rows remain editable and removable if group creation is disabled.
Rolling out group membership
- Upgrade all replicas and stop all old replicas before adding group rows.
- Before rollback, remove all group rows with a compatible server.
Older versions reject project records with group rows and return 503. The bucket schema version remains 1. Project reads and listing visibility checks use the authoritative project record, even if catalog projections are stale. Listing data still comes from the catalog snapshot.
Email invites have the same rollout requirement. Before creating invites, upgrade all replicas. Before rollback to an unsupported release, remove pending invites.
What viewers see: MARIMOHUB_VIEWER_MODE
What a viewer gets depends on MARIMOHUB_VIEWER_MODE. The modes are ordered: each tier includes everything the previous one grants.
static(default): opening a notebook shows the last captured HTML snapshot. Viewers cannot start compute or use apps.applications: additionally, viewers can use notebook apps — start one, open it, and keep it alive while they have it open. The app is the same shared, per-notebook session editors use (viewers cannot stop or restart it). Note that the app kernel runs notebook code with the project's integration secrets and federated credentials, so enable this only for audiences you trust with what the app can reach. Opening a notebook (rather than its app) still shows the static snapshot.ephemeral-sandbox: additionally, opening a notebook provisions a real kernel in a temporary, private session. The viewer can run and edit code, but nothing is written back — no version, snapshot, or workspace changes. Edits are discarded when the session ends.
Ephemeral sessions are per-user: each viewer gets their own sandbox, isolated from every other user's, and only its owner can reach it. Refreshing or re-opening the notebook reconnects to the same live session, so in-session state survives a reload; the session ends on explicit Stop or after the idle timeout, and the next visit starts fresh from the notebook's saved version.
Default access for non-members
MARIMOHUB_DEFAULT_ROLE sets access for signed-in non-members (editor by default). Managers and admins can override it in Project Access → Default access for signed-in users.
| Setting | API value | Access for non-members |
|---|---|---|
| Inherit deployment and sign-in defaults | inherit | Higher of the deployment and OIDC group defaults. |
| Members only | none | No default access, including OIDC default-role entitlements. |
| App user, Viewer, Editor, or Manager | app-user, viewer, editor, manager | Selected role for this project. |
Existing projects inherit. Explicit memberships override defaults. Owners and super admins retain admin access. These rules apply to project listings, direct requests, and apps. They never grant anonymous access.
PATCH /api/v1/projects/{pid} accepts default_role with the values above. Omitting the field preserves its current value. Project responses always include default_role. The value is inherit when the project stores no override. A change sends the project.default_access_changed alert.
Rolling out default access
- Upgrade all replicas and stop all old replicas before setting any value other than
inherit. - Before rollback, reset every override to
inherit. A project that usednonethen grants nonmembers the inherited default again; explicit memberships do not suppress it. To keep such a project members-only, setMARIMOHUB_DEFAULT_ROLE=noneand remove the OIDC group and login-policydefault-role:*grants that would otherwise apply.
Older versions parse project.json with a schema that strips unknown fields. During a mixed-version rollout, an old replica that handles a member change or rename drops default_role. After a rollback, old versions ignore the field. In both cases the project falls back to the inherited default: the deployment default (editor out of the box) or a higher OIDC group or login-policy default-role grant.
Super admins: MARIMOHUB_SUPER_ADMINS
MARIMOHUB_SUPER_ADMINS is a comma-separated list of operators who are treated as admin on every project, regardless of membership or MARIMOHUB_DEFAULT_ROLE. A super admin can see and list all projects (even under MARIMOHUB_DEFAULT_ROLE=none), read and write every notebook, secret, and integration, control any session, and read the audit trail. It is the one grant that overrides the per-project role model. Only super admins can manage organization-wide integrations. Project roles never grant this access.
The web application gives super admins access to the users, settings, audit-log, and debug pages. They can suspend or reactivate any other user from the users page. The audit page uses GET /api/v1/events, which returns at most 30 UTC days per query. The debug page runs the sandbox startup diagnostic. Project managers retain access to each project's daily audit log.
Existing non-owner Admin memberships remain valid and can be demoted or removed, but the API does not allow new Admin assignments. A deployment introducing Manager must stop all old replicas before the first Manager row is stored; older versions cannot parse that role. Rolling back requires converting Manager rows first.
An entry containing @ matches the caller's login email, case-insensitively; any other entry matches the user id (the IdP sub) exactly. The two namespaces do not overlap — an email entry never elevates a caller whose id happens to equal that string, and vice versa. Email matching trusts the IdP-asserted login email, the same trust model as email invites.
Two bounds still hold for a super admin: a project owner cannot be demoted or removed, and a soft-deleted project stays unreachable (404) like it is for everyone else. Session and app rate caps are not bypassed. A personal access token minted by a super admin carries the same power, so scope those tokens accordingly. Unset (the default) means no super admins.
Deprovisioning and user suspension
Super admins can suspend a known user from Admin -> Users, or with PUT /api/v1/admin/users/{id}/suspension; DELETE on the same path reactivates the user. Suspension blocks both browser-session authentication and personal access tokens. Requests authenticated with a browser session receive 403 USER_SUSPENDED; PAT authentication fails as an invalid credential. Suspension and reactivation write user.suspended and user.unsuspended audit events with the operator and target user ids.
Enforcement uses a bounded per-user cache in each server process. An active result is fresh for 10 seconds, then served stale while it refreshes until a hard limit of 30 seconds. Past that limit the request waits for storage; if the status cannot be verified, the API fails closed with 503 SERVICE_UNAVAILABLE. A suspended result is cached for five minutes and remains denied while a stale entry refreshes. This asymmetry bounds unauthorized access without making a storage outage reactivate anyone. Pair suspension with session revocation at the identity provider when access must end immediately.
Profile and suspension updates use ETag compare-and-swap. An authenticated profile refresh therefore cannot overwrite a concurrent suspension change.
Suspension does not terminate an already-running notebook sandbox. Its normal lifetime and idle policies still apply. This lifecycle flag is also the intended target for future SCIM deprovisioning: a SCIM active: false update can suspend the same identity without changing the authentication-time enforcement path.