Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 32 additions & 9 deletions cmd/synaps3/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ func adminS3UserCommand() *cli.Command {
Name: "create",
Usage: "create an S3 user",
Flags: []cli.Flag{
&cli.StringFlag{Name: "name", Usage: "optional S3 user name"},
&cli.StringFlag{Name: "role", Usage: "S3 user role: user, userplus, or admin"},
&cli.BoolFlag{Name: "yes", Usage: "confirm high-risk admin user creation"},
},
Expand All @@ -162,6 +163,9 @@ func adminS3UserCommand() *cli.Command {
return err
}
payload := map[string]string{}
if cmd.IsSet("name") {
payload["name"] = cmd.String("name")
}
if role != "" {
payload["role"] = role
}
Expand All @@ -177,9 +181,10 @@ func adminS3UserCommand() *cli.Command {
},
{
Name: "update",
Usage: "update an S3 user's role",
Usage: "update an S3 user's name or role",
ArgsUsage: "<access-key>",
Flags: []cli.Flag{
&cli.StringFlag{Name: "name", Usage: "S3 user name; empty clears the name"},
&cli.StringFlag{Name: "role", Usage: "S3 user role: user, userplus, or admin"},
&cli.BoolFlag{Name: "yes", Usage: "confirm admin role assignment"},
},
Expand All @@ -188,11 +193,16 @@ func adminS3UserCommand() *cli.Command {
if err != nil {
return err
}
if !cmd.IsSet("name") && !cmd.IsSet("role") {
return errors.New("specify --name or --role")
}
role := strings.TrimSpace(cmd.String("role"))
if err := validateAdminRole(role, false); err != nil {
return err
if cmd.IsSet("role") {
if err := validateAdminRole(role, false); err != nil {
return err
}
}
if role == "admin" && !cmd.Bool("yes") {
if cmd.IsSet("role") && role == "admin" && !cmd.Bool("yes") {
return errors.New("assigning the admin role requires --yes")
}
client, opts, err := newAdminClientFromCommand(ctx, cmd)
Expand All @@ -201,7 +211,14 @@ func adminS3UserCommand() *cli.Command {
}
var updated adminS3User
path := "/api/v1/s3-users/" + url.PathEscape(accessKey)
if err := client.putJSON(ctx, path, map[string]string{"role": role}, &updated, true); err != nil {
payload := map[string]string{}
if cmd.IsSet("name") {
payload["name"] = cmd.String("name")
}
if cmd.IsSet("role") {
payload["role"] = role
}
if err := client.putJSON(ctx, path, payload, &updated, true); err != nil {
return err
}
if opts.JSON {
Expand Down Expand Up @@ -884,12 +901,14 @@ type adminCacheStats struct {

type adminS3User struct {
AccessKey string `json:"access_key"`
Name string `json:"name"`
Role string `json:"role"`
BucketCount int `json:"bucket_count"`
}

type adminS3Credentials struct {
AccessKey string `json:"access_key"`
Name string `json:"name"`
SecretKey string `json:"secret_key"`
Role string `json:"role"`
}
Expand Down Expand Up @@ -1372,9 +1391,9 @@ func writeAdminS3UsersTable(w io.Writer, users []adminS3User) error {
return err
}
tw := tabwriter.NewWriter(w, 0, 4, 2, ' ', 0)
_, _ = fmt.Fprintln(tw, "ACCESS_KEY\tROLE\tBUCKETS")
_, _ = fmt.Fprintln(tw, "NAME\tACCESS_KEY\tROLE\tBUCKETS")
for _, user := range users {
_, _ = fmt.Fprintf(tw, "%s\t%s\t%d\n", user.AccessKey, user.Role, user.BucketCount)
_, _ = fmt.Fprintf(tw, "%s\t%s\t%s\t%d\n", user.Name, user.AccessKey, user.Role, user.BucketCount)
}
return tw.Flush()
}
Expand All @@ -1383,11 +1402,15 @@ func writeAdminCredentials(w io.Writer, credentials adminS3Credentials) error {
if _, err := fmt.Fprintln(w, "S3 User Credentials"); err != nil {
return err
}
return writeAdminRows(w, "", []adminOutputRow{
rows := []adminOutputRow{
{Name: "Access key", Value: credentials.AccessKey},
{Name: "Secret key", Value: credentials.SecretKey},
{Name: "Role", Value: credentials.Role},
})
}
if credentials.Name != "" {
rows = append([]adminOutputRow{{Name: "Name", Value: credentials.Name}}, rows...)
}
return writeAdminRows(w, "", rows)
}

func writeAdminSettingsSummary(w io.Writer, settings adminSettingsResponse) error {
Expand Down
57 changes: 53 additions & 4 deletions cmd/synaps3/admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,31 +498,80 @@ func TestAdminS3UserCommands(t *testing.T) {
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("Decode body: %v", err)
}
if body["role"] != "admin" {
t.Fatalf("role = %q, want admin", body["role"])
if body["role"] != "admin" || body["name"] != "Backup client" {
t.Fatalf("create body = %#v", body)
}
writeAdminTestJSON(t, w, http.StatusCreated, map[string]string{
"access_key": "ak",
"name": "Backup client",
"secret_key": "sk",
"role": "admin",
})
}))
defer ts.Close()

out, err := runAdminCommand(t, []string{"synaps3", "admin", "--admin-url", ts.URL, "s3-user", "create", "--role", "admin", "--yes"})
out, err := runAdminCommand(t, []string{"synaps3", "admin", "--admin-url", ts.URL, "s3-user", "create", "--name", "Backup client", "--role", "admin", "--yes"})
if err != nil {
t.Fatalf("admin s3-user create: %v\n%s", err, out)
}
if !strings.Contains(out, "ak") || !strings.Contains(out, "sk") {
t.Fatalf("create output missing credentials:\n%s", out)
}
for _, want := range []string{"S3 User Credentials", "Access key: ak", "Secret key: sk", "Role: admin"} {
for _, want := range []string{"S3 User Credentials", "Name: Backup client", "Access key: ak", "Secret key: sk", "Role: admin"} {
if !strings.Contains(out, want) {
t.Fatalf("create output missing %q:\n%s", want, out)
}
}
})

t.Run("update name without role and clear it", func(t *testing.T) {
var names []string
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut || r.URL.Path != "/api/v1/s3-users/ak" {
t.Fatalf("request = %s %s", r.Method, r.URL.Path)
}
var body map[string]string
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatal(err)
}
if _, hasRole := body["role"]; hasRole {
t.Fatalf("name-only update sent role: %#v", body)
}
names = append(names, body["name"])
writeAdminTestJSON(t, w, http.StatusOK, map[string]any{
"access_key": "ak", "name": body["name"], "role": "user", "bucket_count": 0,
})
}))
defer ts.Close()
for _, name := range []string{"Archive client", ""} {
out, err := runAdminCommand(t, []string{"synaps3", "admin", "--admin-url", ts.URL, "s3-user", "update", "ak", "--name", name})
if err != nil {
t.Fatalf("update name %q: %v\n%s", name, err, out)
}
}
if !slices.Equal(names, []string{"Archive client", ""}) {
t.Fatalf("names = %#v", names)
}
})

t.Run("list shows name and full access key", func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
writeAdminTestJSON(t, w, http.StatusOK, []map[string]any{{
"access_key": "full-access-key", "name": "Backup client", "role": "user", "bucket_count": 1,
}})
}))
defer ts.Close()
out, err := runAdminCommand(t, []string{"synaps3", "admin", "--admin-url", ts.URL, "s3-user", "list"})
if err != nil {
t.Fatalf("list users: %v\n%s", err, out)
}
for _, want := range []string{"NAME", "ACCESS_KEY", "Backup client", "full-access-key"} {
if !strings.Contains(out, want) {
t.Fatalf("list missing %q:\n%s", want, out)
}
}
})

t.Run("update admin requires yes", func(t *testing.T) {
var called bool
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down
4 changes: 3 additions & 1 deletion docs/en/reference/admin-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,10 +343,12 @@ Provider listings include the optional `upload_speed_test` for the latest manual
| `POST` | `/api/v1/settings/validate` | Validate a settings payload without saving. |
| `GET` | `/api/v1/s3-users` | List S3 users. |
| `POST` | `/api/v1/s3-users` | Create an S3 user. |
| `PUT` | `/api/v1/s3-users/{accessKey}` | Update an S3 user role. |
| `PUT` | `/api/v1/s3-users/{accessKey}` | Update an S3 user name or role. |
| `POST` | `/api/v1/s3-users/{accessKey}/secret` | Rotate an S3 secret key. |
| `DELETE` | `/api/v1/s3-users/{accessKey}` | Delete an S3 user. |

`POST /api/v1/s3-users` accepts an optional `name` alongside `role`. `PUT /api/v1/s3-users/{accessKey}` accepts either field; omit `name` to leave it unchanged or send `"name": ""` to clear it. Names are trimmed, limited to 128 Unicode characters, cannot contain control characters, and must be unique among nonempty names (case-sensitive). List, create, and update responses include `name`. Access keys remain the credential and bucket-owner identity.

Cache settings expose `eviction_policy`, `lru_high_watermark_percent`, and `lru_low_watermark_percent` under `cache`. Valid policies are `lru`, `after_upload`, and `none`. Watermarks must satisfy `0 <= low < high <= 100` and only affect `lru`.

When the full runtime is available, `GET /api/v1/settings` also returns `runtime_filecoin_default_copies`. This is the value used by the current process. `config.filecoin.default_copies` remains the saved value that takes effect after the next restart.
Expand Down
5 changes: 4 additions & 1 deletion docs/en/reference/cli-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,10 @@ Admin commands use HTTP Basic auth. The username comes from `admin.auth.username

```bash
synaps3 admin status
synaps3 admin s3-user create
synaps3 admin s3-user create --name "Backup client"
synaps3 admin s3-user list
synaps3 admin s3-user update <access-key> --role userplus
synaps3 admin s3-user update <access-key> --name "Archive client"
synaps3 admin s3-user rotate-secret <access-key>
synaps3 admin settings get
synaps3 admin settings set cache.max_size_gb=200
Expand All @@ -91,6 +92,8 @@ synaps3 admin storage-confirmation release 42 --attempt-id current-attempt-id --

If no protected password file is available, enter the Admin password at the no-echo prompt. Do not place the password directly in shell history. S3 user creation and secret rotation show the secret only once; store it in a client credential file protected with `0600`.

S3 user names are optional and unique. Use `admin s3-user update <access-key> --name ''` to clear a name. The user list shows names alongside full access keys.

Admin global flags must appear after `admin` and before the subcommand:

| Flag | Purpose |
Expand Down
4 changes: 3 additions & 1 deletion docs/zh/reference/admin-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,10 +343,12 @@ curl -s "$ADMIN/api/v1/tasks/acknowledge/preview?type=storage_store"
| `POST` | `/api/v1/settings/validate` | 验证设置请求内容,但不保存。 |
| `GET` | `/api/v1/s3-users` | 列出 S3 用户。 |
| `POST` | `/api/v1/s3-users` | 创建 S3 用户。 |
| `PUT` | `/api/v1/s3-users/{accessKey}` | 更新 S3 用户 role。 |
| `PUT` | `/api/v1/s3-users/{accessKey}` | 更新 S3 用户名称或 role。 |
| `POST` | `/api/v1/s3-users/{accessKey}/secret` | 轮换 S3 secret key。 |
| `DELETE` | `/api/v1/s3-users/{accessKey}` | 删除 S3 用户。 |

`POST /api/v1/s3-users` 除 `role` 外可选填 `name`。`PUT /api/v1/s3-users/{accessKey}` 可只提交其中一个字段;省略 `name` 表示不改名,传入 `"name": ""` 表示清除名称。名称会去除首尾空白,最长 128 个 Unicode 字符,不能包含控制字符;非空名称必须唯一,区分大小写。列表、创建和更新响应包含 `name`。Access Key 仍是凭据和存储桶所有者的身份标识。

缓存设置在 `cache` 下提供 `eviction_policy`、`lru_high_watermark_percent` 和 `lru_low_watermark_percent`。有效策略为 `lru`、`after_upload` 和 `none`。水位必须满足 `0 <= low < high <= 100`,且只在 `lru` 策略下生效。

完整运行时可用时,`GET /api/v1/settings` 还会返回 `runtime_filecoin_default_copies`,表示当前进程实际使用的值。`config.filecoin.default_copies` 仍表示已保存、下次重启后生效的值。
Expand Down
5 changes: 4 additions & 1 deletion docs/zh/reference/cli-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,10 @@ Admin 命令使用 HTTP Basic auth。用户名来自 `admin.auth.username`;密

```bash
synaps3 admin status
synaps3 admin s3-user create
synaps3 admin s3-user create --name "备份客户端"
synaps3 admin s3-user list
synaps3 admin s3-user update <access-key> --role userplus
synaps3 admin s3-user update <access-key> --name "归档客户端"
synaps3 admin s3-user rotate-secret <access-key>
synaps3 admin settings get
synaps3 admin settings set cache.max_size_gb=200
Expand All @@ -91,6 +92,8 @@ synaps3 admin storage-confirmation release 42 --attempt-id current-attempt-id --

没有受保护的密码文件时,在无回显提示中输入 Admin 密码。不要把密码直接写入 shell history。创建 S3 用户和轮换 secret key 时只显示一次 secret key,请保存到权限为 `0600` 的客户端凭据文件。

S3 用户名称可选且必须唯一。使用 `admin s3-user update <access-key> --name ''` 可清除名称。用户列表会同时显示名称和完整 Access Key。

Admin 全局 flags 必须放在 `admin` 之后、子命令之前:

| Flag | 用途 |
Expand Down
Loading
Loading