# Organizations, teams and folder access

Put a studio's people, teams and spaces together, give teams roles in spaces, limit who may change a folder, sign in through your identity provider and export the audit trail.

An **organization** holds a company's people and its **teams**. A space can belong to one organization, and then its teams can have roles in it: someone who joins the editorial team gets the editorial team's access to every project at once, and loses it when they leave. Spaces outside organizations work exactly as before.

| Organization role | May |
|---|---|
| `owner` | Everything an admin may, plus appoint owners and set up single sign-on. The last owner can't leave. |
| `admin` | Add and remove people, make teams, bring spaces they manage into the organization, export the audit trail. |
| `member` | See the organization's people, teams and the spaces they are in. |

## People and teams

```sh tab=CLI
sidespace org create "Harbor Pictures"
sidespace org members "Harbor Pictures" --add ana@harbor.example --role admin
sidespace team create "Harbor Pictures" Editorial
sidespace team add "Harbor Pictures" Editorial ben@harbor.example cleo@harbor.example
sidespace org attach "Film A" "Harbor Pictures"     # you manage Film A and are an admin there
sidespace team grant "Film A" Editorial editor
```

```sh tab=curl
curl -X POST "https://side.example.com/v1/orgs" -H "Authorization: Bearer $SIDESPACE_TOKEN" \
  -H "Content-Type: application/json" -d '{"name": "Harbor Pictures"}'
curl -X POST "https://side.example.com/v1/orgs/$ORG/teams" -H "Authorization: Bearer $SIDESPACE_TOKEN" \
  -H "Content-Type: application/json" -d '{"name": "Editorial"}'
curl -X POST "https://side.example.com/v1/orgs/$ORG/teams/$TEAM/members" -H "Authorization: Bearer $SIDESPACE_TOKEN" \
  -H "Content-Type: application/json" -d '{"email": "ben@harbor.example"}'
curl -X PUT "https://side.example.com/v1/spaces/$SPACE/org" -H "Authorization: Bearer $SIDESPACE_TOKEN" \
  -H "Content-Type: application/json" -d '{"org_id": "'"$ORG"'"}'
curl -X PUT "https://side.example.com/v1/spaces/$SPACE/teams/$TEAM" -H "Authorization: Bearer $SIDESPACE_TOKEN" \
  -H "Content-Type: application/json" -d '{"role": "editor"}'
```

Adding someone to a team adds them to the organization as a member if they weren't. Creating an organization takes a person (tokens need the `admin` scope); giving a team a role in a space takes a space manager (tokens need `members:manage`).

### A member's role in a space

A person's role in a space is the best of their own grant and the roles of their teams there. `GET /v1/spaces/{sid}/members` shows all three:

```json
{ "user": { "email": "ben@harbor.example" }, "role": "editor", "direct_role": "viewer",
  "teams": [{ "id": "team_…", "name": "Editorial", "role": "editor" }] }
```

`direct_role` is `null` for someone who is in the space only through teams. Joining or leaving a team changes their role at once. Removing someone from a space while a team still gives them access is refused with `409`, naming the team, because "removed" has to mean gone: take them out of the team, or take the team's role away.

## Folder access

A folder can carry rules that limit, or widen, what people may **change** inside it. They never limit what people see.

- `default_role`: the most anyone without a grant may do inside (`viewer`, `commenter` or `editor`; `null` keeps each person's space role).
- `grants`: roles for members of the space (by email) or teams of its organization (by name), which may be above their space role. A viewer can be allowed to upload into one delivery folder.
- The nearest folder with rules decides. Rules don't add up across levels, so a folder's rules are read in one place.
- Space managers are never limited, so nobody can lock the space's owner out.

```sh tab=CLI
# The edit room may read /final, only Delivery may change it
sidespace access "Film A" /final --default viewer --grant team:Delivery=editor
sidespace access "Film A" /final          # your role there, and which folder decides it
sidespace access "Film A" --list          # every folder with rules
sidespace access "Film A" /final --clear
```

```sh tab=curl
curl -X PUT "https://side.example.com/v1/spaces/$SPACE/nodes/$FOLDER/access" \
  -H "Authorization: Bearer $SIDESPACE_TOKEN" -H "Content-Type: application/json" \
  -d '{"default_role": "viewer", "grants": [{"kind": "team", "name": "Delivery", "role": "editor"}]}'
```

Every write checks the role at its node: creating (at the parent folder), saving a version, renaming and moving (at the node and at the new parent), deleting, restoring, sidecars, renditions, comments (commenter), share links and runs. A refused write is a `403` whose message names the role the folder needs. `GET /v1/spaces/{sid}/nodes/{nid}/access` returns your `role` there, your `space_role` and the rules that decide it (`governed_by`), so an app can say so before anyone tries; the web app shows "You are viewer here" on such folders and hides what you can't do.

> [!NOTE]
> Folder rules don't hide folders. For material some people must not see at all, use a separate space.

## Single sign-on

An organization can sign its people in through its identity provider with OpenID Connect: Okta, Microsoft Entra ID, Google Workspace, Keycloak, Authing and any other provider that publishes `/.well-known/openid-configuration`. Owners set it up; admins can read it.

1. Register the redirect URI with the provider. `GET /v1/orgs/{oid}/sso` names it, and so does its `404` before single sign-on is set up.
2. Prove each email domain is yours with a DNS TXT record `_sidespace.<domain>` whose value is the `domain_proof` from the same call (`sidespace-verify=…`). Without it anyone could claim `gmail.com`.
3. Connect the provider for those domains:

```sh tab=CLI
sidespace org sso "Harbor Pictures" --issuer https://harbor.okta.com --client-id 0oa… \
  --client-secret - --domain harbor.example --required < secret.txt
sidespace org sso "Harbor Pictures"          # show it (never the secret)
sidespace org sso "Harbor Pictures" --off
```

```sh tab=curl
curl -X PUT "https://side.example.com/v1/orgs/$ORG/sso" -H "Authorization: Bearer $SIDESPACE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"issuer": "https://harbor.okta.com", "client_id": "0oa…", "client_secret": "…", "domains": ["harbor.example"], "required": true}'
```

A domain without its TXT record is refused with `422` code `domain_unproven`. The client secret is sealed with the server's keys and never returned; leave it out of a later `PUT` to keep it. With `required`, email codes stop working for those domains. People who sign in through single sign-on join the organization as members.

### Signing in from an app

Apps don't handle the provider's tokens. Start a sign-in with the person's address, open the returned `url` in a browser, and poll with the `handle` until it answers:

```sh
curl -X POST https://side.example.com/v1/auth/sso/start -H "Content-Type: application/json" \
  -d '{"email": "ben@harbor.example", "device_name": "Ben’s laptop"}'
# → {"url": "https://harbor.okta.com/…", "handle": "sso_…", "expires_at": "…"}
curl -X POST https://side.example.com/v1/auth/sso/poll -H "Content-Type: application/json" -d '{"handle": "sso_…"}'
# 202 while waiting, 200 with the same tokens as an email sign-in, 410 once expired or collected
```

`start` answers `404` when no organization signs that domain in. When `POST /v1/auth/email/start` (or `verify`) answers `403` with code `sso_required`, switch to this flow. `sidespace login EMAIL --sso` and the web app's sign-in page do exactly that.

## Audit export

Admins can export what happened in the organization, oldest first. Tokens need the `admin` scope.

```sh tab=CLI
sidespace org audit "Harbor Pictures" --since 2026-10-01 --format csv -o audit.csv
```

```sh tab=curl
curl "https://side.example.com/v1/orgs/$ORG/audit?since=2026-10-01&format=jsonl" -H "Authorization: Bearer $SIDESPACE_TOKEN"
```

Each record has `at`, `source`, `kind`, `actor_id`, `actor_email`, and where they apply `actor_kind` (person, agent or plugin), `via` (the token or AI action), `space_id`, `space_name`, `target`, `path` and `detail`. There are two sources:

- `org`: changes to the organization (people, teams, space roles, folder rules, single sign-on settings) and every single sign-on.
- `space`: every file change in the organization's spaces, from their journals, with the file name as it was. A space brought into the organization brings its history.

`since` and `until` take RFC 3339 times or dates; the default is the last 30 days. One export returns at most 200 000 records; for more, ask again from the last record's time. In the web app, admins download it from the organization's **Audit export** tab.
