---
date: 2026-05-10
type: tooling
tags: [gitea, api, secrets, pat, security]
---

# Gitea Secret + PAT API 是不透明的——别想用 API 查 token scope

## 场景

接 Gitea 自动化（ai-review、weekly-retro），想确认仓库 secret 里存的 token 是哪个 PAT、scope 够不够，结果发现**没有任何 API 路径能查**，除非是 site admin。

## 四道封死的路

### 1. Secret value 不可读
```
GET /api/v1/repos/{owner}/{repo}/actions/secrets
```
返回的每个 secret 只有 `name` + `created_at` + `updated_at`。**value 字段不存在**——写入后只能覆盖不能 inspect。设计如此（Gitea 把值 hash 存）。

### 2. Secret 写入后 `updated_at` 不刷新
```
PUT /api/v1/repos/{owner}/{repo}/actions/secrets/{name}  body={"data": "..."}
```
返回 `204 No Content` 表示成功。但**仓库 secrets 列表里的 `updated_at` 字段不会更新**——还是显示首次创建时间。所以连"刚才是不是覆写成功"都没法用时间戳验证。

**唯一成功凭据**：
- `201 Created`（首次新建）
- `204 No Content`（覆写已有）

记得检查 HTTP code，别看 list 输出。

### 3. PAT 自己不能列自己的 token
```
GET /api/v1/users/{me}/tokens
```
用 token auth 直接 `401 Unauthorized`。**Gitea PAT 只能用 basic auth（password + 2FA）来管理 token**——这是防止 compromised token 复制扩散的安全特性。

### 4. PAT 不能列别人的 token
```
GET /api/v1/users/{other-user}/tokens
```
非 site admin 返回 `403 doer should be the site admin or be same as the contextUser`。

## 推论

**只要不是 site admin，就完全没有 API 路径能：**
- 查"这个 secret 里存的是哪个 PAT"
- 查"这个 PAT 的 scope 包含哪些"
- 验证"刚才覆写 secret 是否成功"（除了 HTTP 204）

## 应对

判断现有 secret 够不够用，**必须**走人类路径，**别**试图通过 API 推断、**别**赌"反正当年建 PAT 应该勾了 repo scope":

1. 让用户登 Gitea web UI → 个人 Settings → Applications → 看 PAT scope 勾选项
2. 或者**直接覆写**：用户给一个新 PAT，PUT 进去，HTTP 204 就算搞定

## 实测脚本（chentao.jia 非 admin 场景）

```python
# 探了一圈全失败的命令清单
GET /api/v1/users/chentao.jia/tokens                # 401（PAT auth 不行）
GET /api/v1/users/hongwei.zhang/tokens              # 403（不是 admin）
GET /api/v1/repos/.../actions/secrets               # 200 但只有 name
PUT /api/v1/repos/.../actions/secrets/X             # 204 成功，但 list 时间戳不变
```

## 副产物：empirical scope check

要确认一个 token 实际有什么 scope，**只能跑实际操作探**：

```python
# write:user → 设头像
POST /user/avatar  body={"image": b64png}

# write:repository (read 部分) → 列 issues
GET /repos/.../issues

# write:issue → 创建 + 删 临时 label
POST /repos/.../labels  body={"name":"_probe","color":"888"}
DELETE /repos/.../labels/{id}
```

看返回 200/201 = 有；403 = 没有。
