Files
seaweedfs/test/s3/policy/shell_accesskey_test.go
Chris Lu 10e7f0f2bc fix(shell): s3.user.provision handles existing users by attaching policy (#9040)
* fix(shell): s3.user.provision handles existing users by attaching policy

Instead of erroring when the user already exists, the command now
creates the policy and attaches it to the existing user via UpdateUser.
Credentials are only generated and displayed for newly created users.

* fix(shell): skip duplicate policy attachment in s3.user.provision

Check if the policy is already attached before appending and calling
UpdateUser, making repeated runs idempotent.

* fix(shell): generate service account ID in s3.serviceaccount.create

The command built a ServiceAccount proto without setting Id, which was
rejected by credential.ValidateServiceAccountId on any real store. Now
generates sa:<parent>:<uuid> matching the format used by the admin UI.

* test(s3): integration tests for s3.* shell commands

Adds TestShell* integration tests covering ~40 previously untested
shell commands: user, accesskey, group, serviceaccount, anonymous,
bucket, policy.attach/detach, config.show, and iam.export/import.

Switches the test cluster's credential store from memory to filer_etc
because the memory store silently drops groups and service accounts
in LoadConfiguration/SaveConfiguration.

* fix(shell): rollback policy on key generation failure in s3.user.provision

If iam.GenerateRandomString or iam.GenerateSecretAccessKey fails after
the policy was persisted, the policy would be left orphaned. Extracts
the rollback logic into a local closure and invokes it on all failure
paths after policy creation for consistency.

* address PR review feedback for s3 shell tests and serviceaccount

- s3.serviceaccount.create: use 16 bytes of randomness (hex-encoded) for
  the service account UUID instead of 4 bytes to eliminate collision risk
- s3.serviceaccount.create: print the actual ID and drop the outdated
  "server-assigned" note (the ID is now client-generated)
- tests: guard createdAK in accesskey rotate/delete subtests so sibling
  failures don't run invalid CLI calls
- tests: requireContains/requireNotContains use t.Fatalf to fail fast
- tests: Provision subtest asserts the "Attached policy" message on the
  second provision call for an existing user
- tests: update extractServiceAccountID comment example to match the
  sa:<parent>:<uuid> format
- tests: drop redundant saID empty-check (extractServiceAccountID fatals)

* test(s3): use t.Fatalf for precondition check in serviceaccount test
2026-04-11 22:30:51 -07:00

76 lines
3.0 KiB
Go

package policy
import (
"fmt"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/stretchr/testify/require"
)
// TestShellAccessKeyLifecycle exercises s3.accesskey.* commands end-to-end.
func TestShellAccessKeyLifecycle(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
cluster, err := startMiniCluster(t)
require.NoError(t, err)
defer cluster.Stop()
const weedCmd = "weed"
master := string(pb.NewServerAddress("127.0.0.1", cluster.masterPort, cluster.masterGrpcPort))
filer := string(pb.NewServerAddress("127.0.0.1", cluster.filerPort, cluster.filerGrpcPort))
userName := uniqueName("akuser")
// Create user with explicit key so we know the initial value.
initialAK := "INITIALAK1234567890X"
initialSK := "initialsecret1234567890abcdefghijklmnop"
execShell(t, weedCmd, master, filer,
fmt.Sprintf("s3.user.create -name %s -access_key %s -secret_key %s", userName, initialAK, initialSK))
defer execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.user.delete -name %s", userName))
t.Run("ListInitialKey", func(t *testing.T) {
out := execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.accesskey.list -user %s", userName))
requireContains(t, out, initialAK, "accesskey.list initial")
})
var createdAK string
t.Run("CreateAdditionalKey", func(t *testing.T) {
out := execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.accesskey.create -user %s", userName))
requireContains(t, out, "Access Key:", "accesskey.create output")
requireContains(t, out, "Secret Key:", "accesskey.create output")
createdAK = extractFieldAfter(out, "Access Key:")
if createdAK == "" {
t.Fatalf("failed to extract access key from create output:\n%s", out)
}
out = execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.accesskey.list -user %s", userName))
requireContains(t, out, initialAK, "list contains original")
requireContains(t, out, createdAK, "list contains new key")
})
t.Run("RotateKey", func(t *testing.T) {
if createdAK == "" {
t.Fatal("createdAK is empty; CreateAdditionalKey must run successfully first")
}
out := execShell(t, weedCmd, master, filer,
fmt.Sprintf("s3.accesskey.rotate -user %s -access_key %s", userName, initialAK))
requireContains(t, out, initialAK, "rotate shows old key")
requireContains(t, out, "deleted", "rotate marks old key deleted")
out = execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.accesskey.list -user %s", userName))
requireNotContains(t, out, initialAK, "old key removed")
requireContains(t, out, createdAK, "other key still present")
})
t.Run("DeleteKey", func(t *testing.T) {
if createdAK == "" {
t.Fatal("createdAK is empty; CreateAdditionalKey must run successfully first")
}
execShell(t, weedCmd, master, filer,
fmt.Sprintf("s3.accesskey.delete -user %s -access_key %s", userName, createdAK))
out := execShell(t, weedCmd, master, filer, fmt.Sprintf("s3.accesskey.list -user %s", userName))
requireNotContains(t, out, createdAK, "deleted key removed from list")
})
}