> ## Documentation Index
> Fetch the complete documentation index at: https://docs.basaltic.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Users and groups

> Adding people to an organization, what an invitation is, and why a group is almost always the right place to attach a policy.

A user is a **person** with a personal sign-in. Users belong to the
organization through Workspace. Unattended programs use an account
[service account or role](/iam/roles).

Users belong to an organization, not to an account. A user reaches account resources through
[assigned account roles](/workspace/accounts). Organization policies grant
organization permissions, not account resource access.

<CardGroup cols={2}>
  <Card title="Adding a user" icon="user-plus" href="#adding-a-user">
    Why this is always an invitation, and what a `201` does not promise.
  </Card>

  <Card title="Groups" icon="users" href="#groups">
    Organization policies and account role assignments for a team of users.
  </Card>

  <Card title="Removing a user" icon="user-minus" href="#removing-a-user">
    What it detaches, what it leaves behind, and when it takes effect.
  </Card>
</CardGroup>

## Adding a user

<Tabs>
  <Tab title="Console">
    Open **Organization** → **Users**, then **Invite users**. Enter one or
    more **Email addresses** and optionally select **Groups**. Each address
    receives its own invitation; the results show which invitations succeeded.

    Pending invitations are listed on the **Users** page with a
    **Cancel invitation** action.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST /v1/users
    { "email": "ana@example.com", "groups": ["<group>"] }
    ```
  </Tab>

  <Tab title="CLI">
    ```bash theme={null}
    basaltic workspace user add --email ana@example.com --groups '["platform-team"]'
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    u, err := workspace.New(cfg).AddUser(ctx, &workspace.UserAddRequest{
        Email:    "ana@example.com",
        Groups: []string{groupID},
    })
    ```
  </Tab>
</Tabs>

`email` is the only required field. `groups` is the useful one: it puts the
person in their groups at the moment they join, so there is no window where
they exist with no permissions and someone has to remember to fix it.

<Note>
  **This call always creates an invitation**, never a user. The response is the
  invitation, and the person becomes a user when they accept it — including
  when they already have a Basaltic login. There is no path that adds someone
  to an organization without their consent.

  Until they accept they are a row on the pending-invitations list, not on
  **Users**.
</Note>

It is refused with `409` in two cases, which are worth telling apart:

| Message                   | Meaning                                                                       |
| ------------------------- | ----------------------------------------------------------------------------- |
| User is already a member  | They are already in this organization.                                        |
| Pending invitation exists | An unaccepted invitation for this email is outstanding. Cancel it to re-send. |

<Warning>
  A `201` means the invitation was **created**, not that the email arrived.
  Sending is best-effort: if the mail fails the invitation still exists and the
  request still succeeds, because losing an invitation that was already
  recorded would be worse.

  So "they never got the email" is a real state, and the fix is to cancel the
  pending invitation and add them again rather than to wait.
</Warning>

### Invitations

An invitation records its actual inviter. `invited_by.type` distinguishes a
user, service account, or assumed-role session; machine inviters also include
their account identity and do not have a human email address.

An invitation is the pending half of the call above. There is no separate
"create invitation" endpoint on the public API — you add a user, and an
invitation is what you get when they do not exist yet.

<Tabs>
  <Tab title="Console">
    Pending invitations are listed on **Organization** → **Users**, below the users, with
    **Cancel invitation** on each row. When there are none the section reads
    "No pending invitations."
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    GET    /v1/invitations
    DELETE /v1/invitations/{invitation_id}
    ```
  </Tab>

  <Tab title="CLI">
    ```bash theme={null}
    basaltic workspace invitation list
    basaltic workspace invitation cancel <invitation-id>
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    page, err := workspace.New(cfg).ListInvitations(ctx, nil)
    err = workspace.New(cfg).CancelInvitation(ctx, invitationID)
    ```
  </Tab>
</Tabs>

Cancelling an invitation is not the same as removing a user: it withdraws an
offer nobody has accepted. Once accepted, use
[remove](#removing-a-user) instead.

## Groups

A group collects principals and holds policies. Attaching a policy to a group
rather than to each member is the difference between one change and *n*
changes when the team's permissions move.

<Tabs>
  <Tab title="Console">
    Open **Organization** → **Groups** and choose **Create Group**. Enter
    a **Name** and an optional **Description**.

    The group's **Users** and **Policies** tabs manage its user members and
    organization policy attachments.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST /v1/groups
    { "name": "platform-team" }

    POST /v1/users/{user_id}/groups         { "group": "<group>" }
    POST /v1/groups/{group_id}/policies     { "policy": "<policy>" }
    ```
  </Tab>

  <Tab title="CLI">
    ```bash theme={null}
    basaltic workspace group create --name platform-team
    basaltic workspace user add-group <user-id> --group <group-id>
    basaltic workspace group attach-policy <group-id> --policy <policy-id>
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    c := workspace.New(cfg)
    g, err := c.CreateGroup(ctx, &workspace.GroupCreateRequest{Name: "platform-team"})
    err = c.AddUserToGroup(ctx, userID, &workspace.UserGroupAddRequest{Group: g.ID})
    err = c.AttachGroupPolicy(ctx, g.ID, &workspace.PolicyAttachRequest{Policy: policyID})
    ```
  </Tab>
</Tabs>

Groups contain users only and do not nest. Service accounts and roles use
separate account policy and organization policy attachments.

A user's organization permissions include policies attached directly and
through their groups. An account role assignment to a group lets its members
request that role; its trust policy must still accept each assuming user.

### Where to attach a policy

Attach organization policies to groups when the grant describes a team or job.
Use direct user attachments for individual exceptions. Account permissions
belong on roles and service accounts, not on users or groups.

Inline policies are a third option and a narrower one — see
[managed and inline policies](/iam/policies#managed-and-inline-policies).

## Removing a user

<Tabs>
  <Tab title="Console">
    Open the user, choose **Settings**, then **Remove user** in the danger
    zone. Type the displayed confirmation value before confirming.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    DELETE /v1/users/{user_id}
    ```
  </Tab>

  <Tab title="CLI">
    ```bash theme={null}
    basaltic workspace user remove <user-id>
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    err := workspace.New(cfg).RemoveUser(ctx, userID)
    ```
  </Tab>
</Tabs>

Removing a user takes them out of **this organization**. It does not delete
their Basaltic login, which may belong to other organizations, and it does not
delete anything they created — resources belong to the account, not to the
person who made them.

The removal is atomic and takes their whole footprint in this organization
with it: group memberships, policy attachments, inline policies, their
permission boundary, and finally the membership itself.

<Warning>
  That means re-adding the same email later produces a user with **no
  permissions** — none of it comes back. If you are removing someone
  temporarily, write down which groups they were in first: nothing else does.
</Warning>

Removal ends membership in this organization. Review the user's role sessions
as part of offboarding; the account STS session pages show the source principal
and allow explicit revocation.

## Permissions

These APIs use `https://workspace.basaltic.sh`. Their actions are in the
`workspace:` namespace and must be granted through organization policies.

| Doing this                              | Needs                                               |
| --------------------------------------- | --------------------------------------------------- |
| Add a user                              | `workspace:AddUser`                                 |
| Remove a user                           | `workspace:RemoveUser`                              |
| Create a group                          | `workspace:CreateGroup`                             |
| Add or remove a member                  | `workspace:ManageGroupMembership`                   |
| Attach or detach an organization policy | `workspace:AttachPolicy` / `workspace:DetachPolicy` |

See [Workspace permissions](/workspace/permissions) for the resource checks
and account role assignment actions.

## Next

<CardGroup cols={2}>
  <Card title="Service accounts and roles" icon="key-round" href="/iam/roles">
    The identities that are not people.
  </Card>

  <Card title="Writing policies" icon="file-text" href="/iam/policies">
    What goes in the document you attach here.
  </Card>
</CardGroup>
