package shell import ( "context" "encoding/json" "flag" "fmt" "io" "strings" "github.com/seaweedfs/seaweedfs/weed/iam" "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) func init() { Commands = append(Commands, &commandS3UserProvision{}) } type commandS3UserProvision struct { } func (c *commandS3UserProvision) Name() string { return "s3.user.provision" } func (c *commandS3UserProvision) Help() string { return `create a user with a bucket policy in one step s3.user.provision -name -bucket -role readwrite s3.user.provision -name -bucket -role readonly Convenience wrapper that performs these steps: 1. Creates an IAM policy for the bucket and role 2. Creates the user with auto-generated credentials 3. Attaches the policy to the user Roles: readonly - s3:GetObject, s3:ListBucket readwrite - s3:GetObject, s3:PutObject, s3:DeleteObject, s3:ListBucket admin - s3:* (full access to the bucket) ` } func (c *commandS3UserProvision) HasTag(CommandTag) bool { return false } var rolePolicies = map[string][]string{ "readonly": {"s3:GetObject"}, "readwrite": {"s3:GetObject", "s3:PutObject", "s3:DeleteObject"}, "admin": {"s3:*"}, } func (c *commandS3UserProvision) Do(args []string, commandEnv *CommandEnv, writer io.Writer) error { f := flag.NewFlagSet(c.Name(), flag.ContinueOnError) name := f.String("name", "", "user name") bucket := f.String("bucket", "", "bucket name") role := f.String("role", "", "role: readonly, readwrite, or admin") if err := f.Parse(args); err != nil { return err } if *name == "" { return fmt.Errorf("-name is required") } if *bucket == "" { return fmt.Errorf("-bucket is required") } if strings.ContainsAny(*bucket, "*?") { return fmt.Errorf("-bucket must be a literal bucket name, not a wildcard pattern") } if *role == "" { return fmt.Errorf("-role is required (readonly, readwrite, admin)") } actions, ok := rolePolicies[*role] if !ok { return fmt.Errorf("unknown role %q: must be readonly, readwrite, or admin", *role) } policyName := fmt.Sprintf("%s-%s-%s", *bucket, *name, *role) // Build the policy document bucketActions := []string{"s3:ListBucket"} if *role == "admin" { bucketActions = []string{"s3:*"} } policyDoc := map[string]interface{}{ "Version": "2012-10-17", "Statement": []map[string]interface{}{ { "Effect": "Allow", "Action": actions, "Resource": []string{fmt.Sprintf("arn:aws:s3:::%s/*", *bucket)}, }, { "Effect": "Allow", "Action": bucketActions, "Resource": []string{fmt.Sprintf("arn:aws:s3:::%s", *bucket)}, }, }, } policyJSON, err := json.Marshal(policyDoc) if err != nil { return fmt.Errorf("marshal policy: %v", err) } var ak, sk string var userCreated bool err = commandEnv.withIamClient(func(ctx context.Context, client iam_pb.SeaweedIdentityAccessManagementClient) error { // Step 0: Check if user already exists var existingIdentity *iam_pb.Identity if resp, getErr := client.GetUser(ctx, &iam_pb.GetUserRequest{Username: *name}); getErr == nil && resp.Identity != nil { existingIdentity = resp.Identity fmt.Fprintf(writer, "User %q already exists, adding policy\n", *name) } else if getErr != nil && status.Code(getErr) != codes.NotFound { return fmt.Errorf("check user existence: %w", getErr) } // Step 1: Create policy _, err := client.PutPolicy(ctx, &iam_pb.PutPolicyRequest{ Name: policyName, Content: string(policyJSON), }) if err != nil { return fmt.Errorf("create policy: %v", err) } fmt.Fprintf(writer, "Created policy %q\n", policyName) // rollbackPolicy removes the policy we just created. Used when a later // step fails, to avoid leaving the policy orphaned. rollbackPolicy := func() { if _, delErr := client.DeletePolicy(ctx, &iam_pb.DeletePolicyRequest{Name: policyName}); delErr != nil { fmt.Fprintf(writer, "Warning: failed to rollback policy %q: %v\n", policyName, delErr) } } if existingIdentity != nil { // User exists: attach the new policy if not already present for _, pn := range existingIdentity.PolicyNames { if pn == policyName { fmt.Fprintf(writer, "Policy %q already attached to user %q\n", policyName, *name) return nil } } existingIdentity.PolicyNames = append(existingIdentity.PolicyNames, policyName) _, err = client.UpdateUser(ctx, &iam_pb.UpdateUserRequest{Username: *name, Identity: existingIdentity}) if err != nil { rollbackPolicy() return fmt.Errorf("attach policy to existing user: %w", err) } fmt.Fprintf(writer, "Attached policy %q to existing user %q\n", policyName, *name) } else { // Step 2: Create new user with credentials ak, err = iam.GenerateRandomString(iam.AccessKeyIdLength, iam.CharsetUpper) if err != nil { rollbackPolicy() return fmt.Errorf("generate access key: %v", err) } sk, err = iam.GenerateSecretAccessKey() if err != nil { rollbackPolicy() return fmt.Errorf("generate secret key: %v", err) } identity := &iam_pb.Identity{ Name: *name, Credentials: []*iam_pb.Credential{ { AccessKey: ak, SecretKey: sk, Status: iam.AccessKeyStatusActive, }, }, PolicyNames: []string{policyName}, } _, err = client.CreateUser(ctx, &iam_pb.CreateUserRequest{Identity: identity}) if err != nil { rollbackPolicy() return fmt.Errorf("create user: %w", err) } userCreated = true fmt.Fprintf(writer, "Created user %q with policy %q attached\n", *name, policyName) } return nil }) if err != nil { return err } if userCreated { fmt.Fprintln(writer) fmt.Fprintf(writer, "Access Key: %s\n", ak) fmt.Fprintf(writer, "Secret Key: %s\n", sk) fmt.Fprintln(writer) fmt.Fprintln(writer, "Save these credentials - the secret key cannot be retrieved later.") } return nil }