mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
A release-readiness QA pass over the whole product. The commits split into defects a user would hit and gates that were reporting green while measuring nothing. ## Fixes that change behaviour Rate limiting was bypassable on every install: TRUST_PROXY defaulted to true, so request.ip came from a client-set header and a forged X-Forwarded-For got past the login limiter. The default is now a private-network trust list. A transient Postgres outage stranded in-flight jobs, leaving finished output on disk with no row pointing at it. A reconciler now resolves those rows and adopts the bytes rather than dropping the work. A Redis connection that moved to a new address wedged every read-blocked consumer, so completions stopped signalling while health still answered 200. Socket timeouts plus subscriber pings recover it. Installing more than one AI bundle left the shared venv multi-versioned and silently broke three tools. The installer now reconciles distributions to one version each. Converting an image to JXL at quality 1 through 4 returned a 500, because libjxl 0.7 rejects the distance those values compute. The quality is floored at what the encoder honours. A missing ffmpeg was also reported to the user as a corrupt upload; it now says the engine is unavailable. RAW uploads reached an unpatched LibRaw on arm64, so it is built from source at 0.22.2, and the release scan was split so it can fail on an unfixed critical instead of hiding it behind ignore-unfixed. ## Gates that could not fail Two mutation lanes ran zero mutants because Stryker crawled the gitignored docs build; coverage discarded its whole report on any failing test; the lint gate skipped root tests, scripts, and two workspaces; and several generated matrices counted a host missing ffmpeg as a passing tool. Each now measures what it claims. Full evidence and the outstanding release items are tracked locally and are not part of this branch.
304 lines
12 KiB
Markdown
304 lines
12 KiB
Markdown
---
|
||
description: "设置 SCIM 2.0 预配以将用户和组从你的身份提供商同步到 SnapOtter。涵盖 Okta、Azure AD / Entra ID 以及自定义集成。"
|
||
i18n_source_hash: 06ee702b386e
|
||
i18n_provenance: human
|
||
i18n_output_hash: d318d3799726
|
||
i18n_hash_version: 2
|
||
---
|
||
|
||
# SCIM 预配 {#scim-provisioning}
|
||
|
||
SnapOtter 实现了 SCIM 2.0(System for Cross-domain Identity Management,跨域身份管理系统),用于自动化的用户和组预配。你的身份提供商可以自动创建、更新、停用和重新激活用户账户,并同步组成员关系。
|
||
|
||
::: tip 企业版功能
|
||
SCIM 预配需要带有 `scim` 功能的 **enterprise** 许可证。它在 team 计划中不可用。若没有该功能,所有 SCIM 端点(除发现端点外)都返回 403。
|
||
:::
|
||
|
||
## 先决条件 {#prerequisites}
|
||
|
||
- 一个可通过公网 URL 访问的正在运行的 SnapOtter 实例
|
||
- 一个带有 `scim` 功能的 enterprise 许可证密钥
|
||
- 内置 SnapOtter `admin` 帐户及其完整的有效权限集。委托的自定义角色或缺少任何管理权限的管理 API 密钥无法生成或撤销全局 SCIM 令牌。
|
||
- 你的身份提供商的预配设置的管理员访问权限
|
||
|
||
## 快速开始 {#quick-start}
|
||
|
||
1. 生成一个 SCIM bearer 令牌:
|
||
|
||
```bash
|
||
curl -X POST https://photos.example.com/api/v1/enterprise/scim/token \
|
||
-H "Cookie: snapotter-session=YOUR_SESSION" \
|
||
-H "Content-Type: application/json"
|
||
```
|
||
|
||
响应中包含该令牌。请立即保存;它无法再次获取。
|
||
|
||
```json
|
||
{
|
||
"token": "so_scim_v2_a1b2c3d4e5f6...",
|
||
"message": "Save this token - it cannot be retrieved again"
|
||
}
|
||
```
|
||
|
||
2. 在你的身份提供商中,配置 SCIM 预配,设置:
|
||
- **Base URL**:`https://photos.example.com/api/v1/scim/v2`
|
||
- **Authentication**:Bearer token(粘贴步骤 1 中的令牌)
|
||
|
||
## 身份验证 {#authentication}
|
||
|
||
SCIM 端点使用专用的 Bearer 令牌,与用户会话和 API 密钥分开。
|
||
|
||
### 生成令牌 {#generating-a-token}
|
||
|
||
`POST /api/v1/enterprise/scim/token` 生成新的 SCIM 代币。由于令牌可以跨实例配置和更改用户,因此此端点需要具有完整有效管理权限集的内置 `admin` 角色。将 `users:manage` 保留在自定义角色中是不够的。
|
||
|
||
令牌以明文形式仅返回一次。SnapOtter 只存储 scrypt 哈希值。如果你丢失了令牌,请吊销它并生成一个新的。
|
||
|
||
同一时间只有一个 SCIM 令牌处于活动状态。生成新令牌会替换先前的令牌。
|
||
|
||
::: warning 升级后重新发行令牌
|
||
旧版未版本控制的 SCIM 令牌将被拒绝。升级到颁发 `so_scim_v2_...` 令牌的版本后,请生成新令牌并更新您的身份提供商,然后再恢复配置。
|
||
:::
|
||
|
||
### 吊销令牌 {#revoking-a-token}
|
||
|
||
`DELETE /api/v1/enterprise/scim/token` 撤销当前的 SCIM 令牌。它具有与令牌生成相同的完整内置管理要求。
|
||
|
||
### 速率限制 {#rate-limiting}
|
||
|
||
SCIM 端点按每个令牌每分钟 1000 个请求进行速率限制。超过此限制会返回 HTTP 429。
|
||
|
||
## 支持的资源 {#supported-resources}
|
||
|
||
| SCIM 资源 | SnapOtter 概念 | 创建 | 读取 | 更新 | 删除 |
|
||
|---|---|---|---|---|---|
|
||
| User | 用户账户 | 是 | 是 | 是 | 软删除 |
|
||
| Group | 团队 | 是 | 是 | 是 | 是 |
|
||
|
||
::: warning
|
||
SCIM Group 映射到 SnapOtter **团队**,而非角色。SCIM 无法设置用户的角色。所有通过 SCIM 创建的用户都会被分配 `user` 角色。要更改用户的角色,请使用 SnapOtter 管理界面。
|
||
:::
|
||
|
||
## 用户操作 {#user-operations}
|
||
|
||
### 创建用户 {#create-user}
|
||
|
||
`POST /api/v1/scim/v2/Users`
|
||
|
||
创建一个新的用户账户,其 `authProvider` 设置为 `scim`,角色为 `user`。该用户会被分配到 Default 团队。如果 `active` 为 `false`,则角色改为设置为 `disabled`。
|
||
|
||
必需属性:`userName`。可选属性:`externalId`、`emails`、`active`(默认为 `true`)。
|
||
|
||
### 列出和筛选用户 {#list-and-filter-users}
|
||
|
||
`GET /api/v1/scim/v2/Users`
|
||
|
||
返回分页的用户列表。支持 `startIndex` 和 `count` 查询参数(每页最多 200 条结果)。
|
||
|
||
筛选仅支持 `eq`(等于),可用于以下属性:
|
||
|
||
- `userName eq "jane"`
|
||
- `externalId eq "ext-12345"`
|
||
|
||
其他筛选运算符和属性会返回 HTTP 400。
|
||
|
||
### 获取用户 {#get-user}
|
||
|
||
`GET /api/v1/scim/v2/Users/:id`
|
||
|
||
通过 SnapOtter 用户 ID 返回单个用户。
|
||
|
||
### 替换用户 {#replace-user}
|
||
|
||
`PUT /api/v1/scim/v2/Users/:id`
|
||
|
||
替换用户的属性。支持 `userName`、`externalId`、`emails` 和 `active`。用户名更改会检查冲突(如果新用户名已被另一用户占用,则返回 409)。
|
||
|
||
### 修补用户 {#patch-user}
|
||
|
||
`PATCH /api/v1/scim/v2/Users/:id`
|
||
|
||
使用 SCIM PatchOp 进行部分更新。支持的操作:
|
||
|
||
| 操作 | 路径 |
|
||
|---|---|
|
||
| `replace` | `active`、`userName`、`externalId`、`emails`、`emails[type eq "work"].value`、`name.formatted`、`displayName` |
|
||
| `add` | 同 `replace` |
|
||
| `remove` | `externalId`、`emails` |
|
||
|
||
`name.formatted` 和 `displayName` 路径为兼容性而被接受,但不会产生持久效果(SnapOtter 不单独存储显示名称)。
|
||
|
||
无值的 `replace` 操作(即值为不含 `path` 的对象)也受支持,可用键为 `userName`、`externalId`、`emails` 和 `active`。
|
||
|
||
### 停用用户(软删除) {#deactivate-user-soft-delete}
|
||
|
||
`DELETE /api/v1/scim/v2/Users/:id`
|
||
|
||
SnapOtter 不会通过 SCIM 硬删除用户。DELETE 会执行软停用:
|
||
|
||
1. 用户的角色从其当前值(例如 `editor`)更改为 `disabled:editor`,同时保留原始角色。
|
||
2. 用户的密码被清除。
|
||
3. 所有活动会话被吊销。
|
||
4. 所有 API 密钥被吊销。
|
||
|
||
该用户无法再登录或使用任何 API 密钥。其数据(文件、历史记录)会被保留。
|
||
|
||
### 重新激活用户 {#reactivate-user}
|
||
|
||
要重新激活先前已停用的用户,请发送带有 `active: true` 的 `PUT` 或 `PATCH` 请求。SnapOtter 会恢复停用前的原始角色(例如 `disabled:editor` 再次变为 `editor`)。如果无法确定原始角色,则回退为 `user`。
|
||
|
||
::: details 示例:通过 PATCH 停用和重新激活
|
||
```json
|
||
// Deactivate
|
||
{
|
||
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
||
"Operations": [
|
||
{ "op": "replace", "path": "active", "value": false }
|
||
]
|
||
}
|
||
|
||
// Reactivate
|
||
{
|
||
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
||
"Operations": [
|
||
{ "op": "replace", "path": "active", "value": true }
|
||
]
|
||
}
|
||
```
|
||
:::
|
||
|
||
## 组操作 {#group-operations}
|
||
|
||
SCIM Group 映射到 SnapOtter 团队。创建组会创建一个团队。组成员关系控制用户所属的团队。
|
||
|
||
### 创建组 {#create-group}
|
||
|
||
`POST /api/v1/scim/v2/Groups`
|
||
|
||
必需:`displayName`。可选:`members`(`{ value: userId }` 数组)。
|
||
|
||
### 列出和筛选组 {#list-and-filter-groups}
|
||
|
||
`GET /api/v1/scim/v2/Groups`
|
||
|
||
筛选仅支持 `displayName eq "..."`。分页使用 `startIndex` 和 `count`(每页最多 200 条结果)。
|
||
|
||
### 获取组 {#get-group}
|
||
|
||
`GET /api/v1/scim/v2/Groups/:id`
|
||
|
||
### 替换组 {#replace-group}
|
||
|
||
`PUT /api/v1/scim/v2/Groups/:id`
|
||
|
||
替换组名称和完整的成员列表。不在新列表中的现有成员会被移至 Default 团队。
|
||
|
||
### 修补组 {#patch-group}
|
||
|
||
`PATCH /api/v1/scim/v2/Groups/:id`
|
||
|
||
支持以下操作:
|
||
|
||
| 操作 | 路径 | 效果 |
|
||
|---|---|---|
|
||
| `add` | `members` | 将用户添加到团队 |
|
||
| `remove` | `members[value eq "userId"]` | 将用户移至 Default 团队 |
|
||
| `replace` | `displayName` | 重命名团队 |
|
||
| `replace` | `members` | 替换所有成员(被移除的成员移至 Default 团队) |
|
||
|
||
### 删除组 {#delete-group}
|
||
|
||
`DELETE /api/v1/scim/v2/Groups/:id`
|
||
|
||
删除团队。被删除团队的所有成员都会被移至 Default 团队。用户不会被停用或删除。
|
||
|
||
## IdP 设置 {#idp-setup}
|
||
|
||
### Okta {#okta}
|
||
|
||
1. 在 Okta 管理控制台中,打开你的 SnapOtter 应用程序(或创建一个)。
|
||
2. 转到 **Provisioning** 选项卡,然后点击 **Configure API Integration**。
|
||
3. 勾选 **Enable API Integration** 并输入:
|
||
- **Base URL**:`https://photos.example.com/api/v1/scim/v2`
|
||
- **API Token**:上面生成的 SCIM bearer 令牌
|
||
4. 点击 **Test API Credentials**,然后点击 **Save**。
|
||
5. 在 **Provisioning > To App** 下,启用:
|
||
- **Create Users**
|
||
- **Update User Attributes**
|
||
- **Deactivate Users**
|
||
6. 在 **Push Groups** 下,配置要作为 SnapOtter 团队同步的 Okta 组。
|
||
|
||
### Azure AD / Entra ID {#azure-ad-entra-id}
|
||
|
||
1. 在 Azure 门户中,转到你的 SnapOtter 企业应用程序。
|
||
2. 转到 **Provisioning**,并将 **Provisioning Mode** 设置为 **Automatic**。
|
||
3. 在 **Admin Credentials** 下,输入:
|
||
- **Tenant URL**:`https://photos.example.com/api/v1/scim/v2`
|
||
- **Secret Token**:上面生成的 SCIM bearer 令牌
|
||
4. 点击 **Test Connection**,然后点击 **Save**。
|
||
5. 在 **Mappings** 下,配置用户和组的属性映射。默认设置通常有效,但请确认 `userName` 按预期映射到 `userPrincipalName` 或 `mail`。
|
||
6. 将 **Provisioning Status** 设置为 **On** 并保存。
|
||
|
||
Azure 会按固定的同步周期(通常每 40 分钟)预配用户和组。
|
||
|
||
## 发现端点 {#discovery-endpoints}
|
||
|
||
以下三个端点无需身份验证即可访问,用于描述 SCIM 服务器的功能:
|
||
|
||
| 端点 | 说明 |
|
||
|---|---|
|
||
| `GET /api/v1/scim/v2/ServiceProviderConfig` | 服务器功能和支持的特性 |
|
||
| `GET /api/v1/scim/v2/Schemas` | User 和 Group 模式定义 |
|
||
| `GET /api/v1/scim/v2/ResourceTypes` | 可用的资源类型(User、Group) |
|
||
|
||
`ServiceProviderConfig` 会公告以下功能:
|
||
|
||
| 功能 | 是否支持 |
|
||
|---|---|
|
||
| Patch | 是 |
|
||
| Bulk | 否 |
|
||
| Filter | 是(最多 200 条结果,仅 `eq` 运算符) |
|
||
| Change password | 否 |
|
||
| Sort | 否 |
|
||
| ETag | 否 |
|
||
|
||
## 限制 {#limitations}
|
||
|
||
- **筛选**:仅支持 `eq` 运算符。复杂筛选、`and`/`or` 运算符、`co`(包含)和 `sw`(以...开头)均未实现。
|
||
- **批量操作**:不支持。
|
||
- **Sort 和 ETag**:不支持。
|
||
- **角色**:SCIM 无法分配 SnapOtter 角色。所有预配的用户都会获得 `user` 角色。
|
||
- **MAX_USERS**:`MAX_USERS` 环境变量限制不会在 SCIM 用户创建时强制执行。如果你需要限制用户数量,请在你的 IdP 中管理分配。
|
||
- **单一令牌**:同一时间只能有一个 SCIM 令牌处于活动状态。如果多个 IdP 需要 SCIM 访问权限,它们必须共享该令牌。
|
||
- **组即团队**:SCIM Group 对应团队,而非角色或权限组。
|
||
|
||
## 故障排除 {#troubleshooting}
|
||
|
||
### 403 "SCIM provisioning requires an enterprise license with the scim feature" {#_403-scim-provisioning-requires-an-enterprise-license-with-the-scim-feature}
|
||
|
||
你的许可证不包含 `scim` 功能,或者未配置许可证。SCIM 需要 enterprise 计划许可证。请确认已设置 `SNAPOTTER_LICENSE_KEY` 且许可证包含 `scim` 功能。
|
||
|
||
### 401 "Bearer token required" {#_401-bearer-token-required}
|
||
|
||
SCIM 请求未包含 `Authorization: Bearer <token>` 标头。请检查你的 IdP 的预配配置。
|
||
|
||
### 401 "Invalid token" {#_401-invalid-token}
|
||
|
||
令牌格式错误、使用已停用的未版本化格式或与存储的哈希不匹配。生成当前的 `so_scim_v2_...` 令牌并在 IdP 的配置设置中更新该令牌。
|
||
|
||
### 401 "SCIM not configured" {#_401-scim-not-configured}
|
||
|
||
尚未生成任何 SCIM 令牌。请使用 `POST /api/v1/enterprise/scim/token` 端点创建一个。
|
||
|
||
### 409 "User already exists" / "userName already taken" {#_409-user-already-exists-username-already-taken}
|
||
|
||
已存在同名用户名的用户。当 IdP 重试失败的创建操作时,可能会发生这种情况。请在 SnapOtter 管理面板中检查是否有重复的用户名。
|
||
|
||
### 429 "SCIM rate limit exceeded" {#_429-scim-rate-limit-exceeded}
|
||
|
||
IdP 发送的请求超过每分钟 1000 个。这通常发生在大型初始同步期间。大多数 IdP 会在速率限制窗口重置后自动重试。如果问题持续存在,请检查你的 IdP 的预配同步间隔。
|
||
|
||
### 用户已取消预配但未从界面中移除 {#users-deprovisioned-but-not-removed-from-the-ui}
|
||
|
||
SCIM DELETE 是软停用。已停用的用户仍会以禁用状态出现在管理员用户列表中。这是有意为之,以便保留他们的数据。他们的角色显示为 `disabled:<original-role>`。
|