Skip to content

Teams and security

View as Markdown

Teams, roles and invites

Glossary: Team (UI) = workspace (code). The workspaces table, workspace_id columns and policy.WorkspaceID(ctx) keep the internal name.

Model#

  • A user belongs to any number of teams through memberships(user_id, workspace_id, role, project_ids). Each team has its own projects, servers, integrations, API tokens, notification channels, backup destinations and audit log. Servers are never shared.
  • Setup creates the first team (optional teamName, default "NebulaCtrl") and its owner, who is also the instance admin (the first user created).
  • The browser session stores the current team (teamId). POST /teams/{id}/switch re-checks membership (404 for non-members). If the stored team is gone the session falls back to the user's oldest team.
  • Any user can create a team and becomes its owner, unless the instance admin turns off allowTeamCreation (PATCH /instance); the instance admin may always.

Roles#

Role Scopes Notes
Owner all Deletes the team, manages owners (there must always be one)
Admin all Everything except managing owners
Developer projects:read/write, deployments:write/rollback, servers:read Deploys, variables, terminal/files, tasks. No members, integrations, servers or other people's tokens
Viewer projects:read, servers:read Read-only; never sees secrets, reveal, terminal or files

Host access is an instance-admin decision. Only the instance admin can make a server unrestricted (devices, GPUs, a custom runtime, privileged mode and the other options that hand a container the host, and unrestricted Compose stacks: Docker options). A team owner, an admin or a developer cannot, whatever their scopes, so nobody below the instance admin can gain root on a host unless the instance admin allowed it for that server.

policy.ScopesForRole maps roles to scopes; every existing policy.Require keeps working. policy.RequireRole(ctx, projectID, roles...) covers role-only checks. Owners and admins always see every project; developers and viewers can be restricted to a project list (outside it: 404).

API tokens#

A token belongs to one team (api_tokens.workspace_id). Its scopes are intersected with the creator's role at creation and again on every use: demoting the creator demotes the token, removing them from the team makes it answer 401. admin on a token means "everything my role holds". Project lists are intersected with the member's restriction. Admins see all tokens of the team; other members manage only their own (browser session). MCP follows tokens.

Invites#

POST /teams/{id}/invites: with email the link is mailed (the team's enabled email notification channel, else the instance SMTP relay from PATCH /instance, else link-only; the response's delivery says which) and works once for that address. Without email it is a link invite with maxUses (default 1) and an expiry (default 7 days, max 90). The token (nci_…) is stored hashed and shown once. GET /invites/{token} previews it publicly (404 invalid, 410 expired/used); POST /invites/{token}/accept joins a signed-in user or creates an account (name + password) and signs it in. Single use is enforced atomically. Emailed links need NEBULACTRL_PUBLIC_URL.

CLI#

POST /teams/{teamId}/members/{userId}/password-reset (Owners and Admins, signed-in users only; admins cannot reset an Owner and nobody can reset themselves) returns a single-use link, valid for 30 minutes, shown once. In the web app: Settings → Members → Reset password. It needs no SMTP setup, so it is also the way to help someone when the instance cannot send email. Using the link signs the member out of every device. Members who only sign in through an identity provider have no password: 409 no_password. Audited as member.password_reset_link. Details: forgot password.

nebulactrl teams ls|create|switch, nebulactrl members ls|invite. A token belongs to one team, so login saves each token under its team slug and teams switch <slug> selects the saved one.