# 组织、团队与文件夹权限

把一家工作室的人、团队和空间放在一起：按团队给空间授权，限制谁能改某个文件夹，用公司的身份提供方登录，导出审计记录。

**组织**（organization）装着一家公司的人和它的**团队**。一个空间可以属于一个组织，之后组织的团队就能在空间里有角色：有人加入剪辑组，就一次性拿到剪辑组在所有项目里的权限，离开时也一起收回。不属于组织的空间和以前完全一样。

| 组织角色 | 能做什么 |
|---|---|
| `owner`（所有者） | 管理员能做的都能做，另外能任命所有者、设置单点登录。最后一个所有者不能离开。 |
| `admin`（管理员） | 加人、移人、建团队，把自己管理的空间放进组织，导出审计记录。 |
| `member`（成员） | 查看组织的成员、团队和自己所在的空间。 |

## 成员和团队

```sh tab=CLI
sidespace org create "Harbor Pictures"
sidespace org members "Harbor Pictures" --add ana@harbor.example --role admin
sidespace team create "Harbor Pictures" 剪辑组
sidespace team add "Harbor Pictures" 剪辑组 ben@harbor.example cleo@harbor.example
sidespace org attach "Film A" "Harbor Pictures"     # 你管理 Film A，并且是组织管理员
sidespace team grant "Film A" 剪辑组 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": "剪辑组"}'
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"}'
```

把人加进团队时，如果他还不在组织里，会以成员身份加入。新建组织只能由人来做（令牌需要 `admin` 权限范围）；给团队空间角色要空间管理员（令牌需要 `members:manage`）。

### 成员在空间里的角色

一个人在空间里的角色，取他自己的授权和他所在团队在这个空间的角色中最高的那个。`GET /v1/spaces/{sid}/members` 三样都给出：

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

只通过团队进入空间的人，`direct_role` 是 `null`。加入或离开团队，角色立刻跟着变。如果某人还通过团队有权限，从空间移除他会被拒绝（`409`，并说明是哪个团队），因为“移除”就该是真的不在了：先把他移出团队，或取消团队的角色。

## 文件夹权限

文件夹可以有规则，限制或放宽在里面能**改**什么。规则从不限制能看到什么。

- `default_role`：没有单独授权的人在里面最多能做什么（`viewer`、`commenter` 或 `editor`；`null` 表示按各自在空间里的角色）。
- `grants`：给空间成员（按邮箱）或组织的团队（按名字）单独的角色，可以高于他们在空间里的角色。比如让查看者能往某个交付文件夹上传。
- 离得最近的、有规则的文件夹说了算。上下层的规则不叠加，所以一个文件夹的规则在一个地方就能看清。
- 空间管理员不受限制，没人能把空间所有者锁在外面。

```sh tab=CLI
# 剪辑室能看 /final，只有交付组能改
sidespace access "Film A" /final --default viewer --grant team:交付组=editor
sidespace access "Film A" /final          # 你在那里是什么角色，由哪个文件夹决定
sidespace access "Film A" --list          # 所有有规则的文件夹
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": "交付组", "role": "editor"}]}'
```

每次写入都按所在位置检查角色：新建（看父文件夹）、保存新版本、改名和移动（看原位置和新的父文件夹）、删除、恢复、附加信息、预览文件、评论（评论者）、分享链接和 AI 运行。被拒绝的写入返回 `403`，消息里写明这个文件夹需要什么角色。`GET /v1/spaces/{sid}/nodes/{nid}/access` 返回你在那里的 `role`、你的 `space_role` 和决定它的规则（`governed_by`），应用可以在别人动手之前就说清楚；网页版会在这种文件夹上显示“这里你是查看者”，并收起你做不了的操作。

> [!NOTE]
> 文件夹规则不会隐藏文件夹。有些人完全不该看到的素材，请放进单独的空间。

## 单点登录

组织可以通过 OpenID Connect 让成员用公司的身份提供方登录：Okta、Microsoft Entra ID、Google Workspace、Keycloak、Authing，以及任何提供 `/.well-known/openid-configuration` 的服务。所有者设置，管理员可以查看。

1. 在身份提供方登记回调地址。`GET /v1/orgs/{oid}/sso` 会给出它；还没设置单点登录时，它返回的 `404` 里也写着。
2. 为每个邮箱域名添加 DNS TXT 记录 `_sidespace.<域名>`，值是同一个调用给出的 `domain_proof`（`sidespace-verify=…`），证明域名是你们的。否则谁都能声称 `gmail.com`。
3. 为这些域名接入身份提供方：

```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"          # 查看（不会显示密钥）
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}'
```

没有 TXT 记录的域名会被拒绝（`422`，code 为 `domain_unproven`）。Client secret 用服务器的密钥封存，从不返回；之后再 `PUT` 时不传它就保持不变。开启 `required` 后，这些域名不能再用邮箱验证码登录。通过单点登录进来的人会以成员身份加入组织。

### 在应用里登录

应用不接触身份提供方的令牌。用对方的邮箱开始登录，在浏览器里打开返回的 `url`，再用 `handle` 轮询，直到有结果：

```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 的笔记本"}'
# → {"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；成功返回 200，令牌和邮箱登录的一样；过期或已取走返回 410
```

没有组织为这个域名设置单点登录时，`start` 返回 `404`。`POST /v1/auth/email/start`（或 `verify`）返回 `403`、code 为 `sso_required` 时，就改走这个流程。`sidespace login EMAIL --sso` 和网页版的登录页都是这样做的。

## 审计导出

管理员可以导出组织里发生过的事，按时间从早到晚。令牌需要 `admin` 权限范围。

```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"
```

每条记录有 `at`、`source`、`kind`、`actor_id`、`actor_email`，适用时还有 `actor_kind`（人、AI agent 或插件）、`via`（令牌或 AI 动作）、`space_id`、`space_name`、`target`、`path` 和 `detail`。记录来自两处：

- `org`：组织的变化（成员、团队、空间角色、文件夹规则、单点登录设置）和每一次单点登录。
- `space`：组织里各个空间的每次文件改动，取自空间的变更日志，带着当时的文件名。空间放进组织时，它之前的历史也一起带进来。

`since` 和 `until` 接受 RFC 3339 时间或日期，默认是最近 30 天。一次最多返回 20 万条；更多时从最后一条的时间开始再导出。网页版里，管理员在组织的**审计导出**页下载。
