mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
Add workflow-oriented S3 shell commands
This commit is contained in:
@@ -31,8 +31,13 @@ func (c *commandS3Configure) Name() string {
|
||||
func (c *commandS3Configure) Help() string {
|
||||
return `configure and apply s3 options for each bucket
|
||||
|
||||
# see the current configuration file content
|
||||
s3.configure
|
||||
# for workflow-oriented user management, prefer:
|
||||
s3.user.create -name=username -generate_credentials
|
||||
s3.user.show -name=username
|
||||
s3.policy.attach -user=username -name=mypolicy
|
||||
|
||||
# see the current configuration file content
|
||||
s3.configure
|
||||
|
||||
# create a new identity with account information
|
||||
s3.configure -user=username -actions=Read,Write,List,Tagging -buckets=bucket-name -policies=policy1,policy2 -access_key=key -secret_key=secret -account_id=id -account_display_name=name -account_email=email@example.com -apply
|
||||
|
||||
@@ -29,10 +29,18 @@ func (c *commandS3Policy) Name() string {
|
||||
func (c *commandS3Policy) Help() string {
|
||||
return `manage s3 policies
|
||||
|
||||
# create or update a policy
|
||||
s3.policy -put -name=mypolicy -file=policy.json
|
||||
# preferred exact commands
|
||||
s3.policy.create -name=mypolicy -file=policy.json
|
||||
s3.policy.show -name=mypolicy
|
||||
s3.policy.list
|
||||
s3.policy.delete -name=mypolicy
|
||||
s3.policy.attach -user=alice -name=mypolicy
|
||||
s3.policy.detach -user=alice -name=mypolicy
|
||||
|
||||
# list all policies
|
||||
# create or update a policy
|
||||
s3.policy -put -name=mypolicy -file=policy.json
|
||||
|
||||
# list all policies
|
||||
s3.policy -list
|
||||
|
||||
# get a policy
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/credential"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Commands = append(Commands,
|
||||
&s3ShellCommand{
|
||||
name: "s3.policy.create",
|
||||
help: `create or update a managed S3 policy
|
||||
|
||||
s3.policy.create -name photos-rw -file policy.json`,
|
||||
do: runS3PolicyCreateCommand,
|
||||
},
|
||||
&s3ShellCommand{
|
||||
name: "s3.policy.show",
|
||||
help: `show one managed S3 policy
|
||||
|
||||
s3.policy.show -name photos-rw`,
|
||||
do: runS3PolicyShowCommand,
|
||||
},
|
||||
&s3ShellCommand{
|
||||
name: "s3.policy.list",
|
||||
help: `list managed S3 policies`,
|
||||
do: runS3PolicyListCommand,
|
||||
},
|
||||
&s3ShellCommand{
|
||||
name: "s3.policy.delete",
|
||||
help: `delete a managed S3 policy
|
||||
|
||||
s3.policy.delete -name photos-rw`,
|
||||
do: runS3PolicyDeleteCommand,
|
||||
},
|
||||
&s3ShellCommand{
|
||||
name: "s3.policy.attach",
|
||||
help: `attach a managed S3 policy to a filer-backed user
|
||||
|
||||
s3.policy.attach -user alice -name photos-rw`,
|
||||
do: runS3PolicyAttachCommand,
|
||||
},
|
||||
&s3ShellCommand{
|
||||
name: "s3.policy.detach",
|
||||
help: `detach a managed S3 policy from a filer-backed user
|
||||
|
||||
s3.policy.detach -user alice -name photos-rw`,
|
||||
do: runS3PolicyDetachCommand,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func runS3PolicyCreateCommand(args []string, commandEnv *CommandEnv, writer io.Writer) error {
|
||||
fs := flag.NewFlagSet("s3.policy.create", flag.ContinueOnError)
|
||||
fs.SetOutput(writer)
|
||||
name := fs.String("name", "", "policy name")
|
||||
file := fs.String("file", "", "policy file (json)")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if err == flag.ErrHelp {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if *name == "" {
|
||||
return fmt.Errorf("-name is required")
|
||||
}
|
||||
if *file == "" {
|
||||
return fmt.Errorf("-file is required")
|
||||
}
|
||||
|
||||
return withS3ShellStore(commandEnv, func(ctx context.Context, store s3ShellStore) error {
|
||||
return runS3PolicyCreate(ctx, store, *name, *file, writer)
|
||||
})
|
||||
}
|
||||
|
||||
func runS3PolicyShowCommand(args []string, commandEnv *CommandEnv, writer io.Writer) error {
|
||||
fs := flag.NewFlagSet("s3.policy.show", flag.ContinueOnError)
|
||||
fs.SetOutput(writer)
|
||||
name := fs.String("name", "", "policy name")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if err == flag.ErrHelp {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if *name == "" {
|
||||
return fmt.Errorf("-name is required")
|
||||
}
|
||||
|
||||
return withS3ShellStore(commandEnv, func(ctx context.Context, store s3ShellStore) error {
|
||||
return runS3PolicyShow(ctx, store, *name, writer)
|
||||
})
|
||||
}
|
||||
|
||||
func runS3PolicyListCommand(args []string, commandEnv *CommandEnv, writer io.Writer) error {
|
||||
fs := flag.NewFlagSet("s3.policy.list", flag.ContinueOnError)
|
||||
fs.SetOutput(writer)
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if err == flag.ErrHelp {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return withS3ShellStore(commandEnv, func(ctx context.Context, store s3ShellStore) error {
|
||||
return runS3PolicyList(ctx, store, writer)
|
||||
})
|
||||
}
|
||||
|
||||
func runS3PolicyDeleteCommand(args []string, commandEnv *CommandEnv, writer io.Writer) error {
|
||||
fs := flag.NewFlagSet("s3.policy.delete", flag.ContinueOnError)
|
||||
fs.SetOutput(writer)
|
||||
name := fs.String("name", "", "policy name")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if err == flag.ErrHelp {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if *name == "" {
|
||||
return fmt.Errorf("-name is required")
|
||||
}
|
||||
|
||||
return withS3ShellStore(commandEnv, func(ctx context.Context, store s3ShellStore) error {
|
||||
return runS3PolicyDelete(ctx, store, *name, writer)
|
||||
})
|
||||
}
|
||||
|
||||
func runS3PolicyAttachCommand(args []string, commandEnv *CommandEnv, writer io.Writer) error {
|
||||
return runS3PolicyAttachDetachCommand("s3.policy.attach", true, args, commandEnv, writer)
|
||||
}
|
||||
|
||||
func runS3PolicyDetachCommand(args []string, commandEnv *CommandEnv, writer io.Writer) error {
|
||||
return runS3PolicyAttachDetachCommand("s3.policy.detach", false, args, commandEnv, writer)
|
||||
}
|
||||
|
||||
func runS3PolicyAttachDetachCommand(commandName string, attach bool, args []string, commandEnv *CommandEnv, writer io.Writer) error {
|
||||
fs := flag.NewFlagSet(commandName, flag.ContinueOnError)
|
||||
fs.SetOutput(writer)
|
||||
user := fs.String("user", "", "user name")
|
||||
name := fs.String("name", "", "policy name")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if err == flag.ErrHelp {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if *user == "" {
|
||||
return fmt.Errorf("-user is required")
|
||||
}
|
||||
if *name == "" {
|
||||
return fmt.Errorf("-name is required")
|
||||
}
|
||||
|
||||
return withS3ShellStore(commandEnv, func(ctx context.Context, store s3ShellStore) error {
|
||||
if attach {
|
||||
return runS3PolicyAttach(ctx, store, *user, *name, writer)
|
||||
}
|
||||
return runS3PolicyDetach(ctx, store, *user, *name, writer)
|
||||
})
|
||||
}
|
||||
|
||||
func runS3PolicyCreate(ctx context.Context, store s3ShellStore, name, file string, writer io.Writer) error {
|
||||
document, err := loadPolicyDocumentFromFile(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := store.CreatePolicy(ctx, name, *document); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(writer, "Saved policy %q.\n", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runS3PolicyShow(ctx context.Context, store s3ShellStore, name string, writer io.Writer) error {
|
||||
document, err := store.GetPolicy(ctx, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if document == nil {
|
||||
return fmt.Errorf("policy %q not found", name)
|
||||
}
|
||||
formatted, err := marshalPolicyDocument(document)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(writer, formatted)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runS3PolicyList(ctx context.Context, store s3ShellStore, writer io.Writer) error {
|
||||
policies, err := store.GetPolicies(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
names := make([]string, 0, len(policies))
|
||||
for name := range policies {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, name := range names {
|
||||
fmt.Fprintln(writer, name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runS3PolicyDelete(ctx context.Context, store s3ShellStore, name string, writer io.Writer) error {
|
||||
if err := store.DeletePolicy(ctx, name); err != nil {
|
||||
if s3ShellIsNotFound(err) {
|
||||
return fmt.Errorf("policy %q not found", name)
|
||||
}
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(writer, "Deleted policy %q.\n", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runS3PolicyAttach(ctx context.Context, store s3ShellStore, username, policyName string, writer io.Writer) error {
|
||||
identity, err := getS3User(ctx, store, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureMutableIdentity(identity, "attach policies to"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := store.AttachUserPolicy(ctx, username, policyName); err != nil {
|
||||
if errors.Is(err, credential.ErrPolicyNotFound) {
|
||||
return fmt.Errorf("policy %q not found", policyName)
|
||||
}
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(writer, "Attached policy %q to user %q.\n", policyName, username)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runS3PolicyDetach(ctx context.Context, store s3ShellStore, username, policyName string, writer io.Writer) error {
|
||||
identity, err := getS3User(ctx, store, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureMutableIdentity(identity, "detach policies from"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := store.DetachUserPolicy(ctx, username, policyName); err != nil {
|
||||
if errors.Is(err, credential.ErrPolicyNotAttached) {
|
||||
return fmt.Errorf("policy %q is not attached to user %q", policyName, username)
|
||||
}
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(writer, "Detached policy %q from user %q.\n", policyName, username)
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadPolicyDocumentFromFile(file string) (*policy_engine.PolicyDocument, error) {
|
||||
data, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read policy file: %v", err)
|
||||
}
|
||||
var document policy_engine.PolicyDocument
|
||||
if err := json.Unmarshal(data, &document); err != nil {
|
||||
return nil, fmt.Errorf("invalid policy json: %v", err)
|
||||
}
|
||||
return &document, nil
|
||||
}
|
||||
|
||||
func marshalPolicyDocument(document *policy_engine.PolicyDocument) (string, error) {
|
||||
data, err := json.MarshalIndent(document, "", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(data)), nil
|
||||
}
|
||||
@@ -0,0 +1,709 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/credential"
|
||||
iamgrpc "github.com/seaweedfs/seaweedfs/weed/credential/grpc"
|
||||
weediam "github.com/seaweedfs/seaweedfs/weed/iam"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
var errS3AccessKeyInUse = errors.New("access key already in use")
|
||||
|
||||
type s3ShellCommand struct {
|
||||
name string
|
||||
help string
|
||||
do func([]string, *CommandEnv, io.Writer) error
|
||||
}
|
||||
|
||||
func (c *s3ShellCommand) Name() string { return c.name }
|
||||
|
||||
func (c *s3ShellCommand) Help() string { return c.help }
|
||||
|
||||
func (c *s3ShellCommand) Do(args []string, commandEnv *CommandEnv, writer io.Writer) error {
|
||||
return c.do(args, commandEnv, writer)
|
||||
}
|
||||
|
||||
func (c *s3ShellCommand) HasTag(CommandTag) bool { return false }
|
||||
|
||||
type s3ShellStore interface {
|
||||
CreateUser(ctx context.Context, identity *iam_pb.Identity) error
|
||||
GetUser(ctx context.Context, username string) (*iam_pb.Identity, error)
|
||||
UpdateUser(ctx context.Context, username string, identity *iam_pb.Identity) error
|
||||
DeleteUser(ctx context.Context, username string) error
|
||||
ListUsers(ctx context.Context) ([]string, error)
|
||||
GetUserByAccessKey(ctx context.Context, accessKey string) (*iam_pb.Identity, error)
|
||||
CreateAccessKey(ctx context.Context, username string, credential *iam_pb.Credential) error
|
||||
DeleteAccessKey(ctx context.Context, username string, accessKey string) error
|
||||
GetPolicy(ctx context.Context, name string) (*policy_engine.PolicyDocument, error)
|
||||
GetPolicies(ctx context.Context) (map[string]policy_engine.PolicyDocument, error)
|
||||
CreatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error
|
||||
DeletePolicy(ctx context.Context, name string) error
|
||||
AttachUserPolicy(ctx context.Context, username string, policyName string) error
|
||||
DetachUserPolicy(ctx context.Context, username string, policyName string) error
|
||||
ListAttachedUserPolicies(ctx context.Context, username string) ([]string, error)
|
||||
}
|
||||
|
||||
type s3UserCreateOptions struct {
|
||||
name string
|
||||
accessKey string
|
||||
secretKey string
|
||||
generateCredentials bool
|
||||
accountID string
|
||||
displayName string
|
||||
email string
|
||||
}
|
||||
|
||||
type s3AccessKeyCreateOptions struct {
|
||||
username string
|
||||
accessKey string
|
||||
secretKey string
|
||||
generateCredentials bool
|
||||
}
|
||||
|
||||
func init() {
|
||||
Commands = append(Commands,
|
||||
&s3ShellCommand{
|
||||
name: "s3.user.list",
|
||||
help: `list S3 users with source and status`,
|
||||
do: runS3UserListCommand,
|
||||
},
|
||||
&s3ShellCommand{
|
||||
name: "s3.user.show",
|
||||
help: `show one S3 user's details
|
||||
|
||||
s3.user.show -name alice`,
|
||||
do: runS3UserShowCommand,
|
||||
},
|
||||
&s3ShellCommand{
|
||||
name: "s3.user.create",
|
||||
help: `create a filer-backed S3 user
|
||||
|
||||
s3.user.create -name alice -generate_credentials
|
||||
s3.user.create -name alice -access_key AKIA... -secret_key secret
|
||||
s3.user.create -name alice -email alice@example.com -display_name "Alice"`,
|
||||
do: runS3UserCreateCommand,
|
||||
},
|
||||
&s3ShellCommand{
|
||||
name: "s3.user.delete",
|
||||
help: `delete a filer-backed S3 user
|
||||
|
||||
s3.user.delete -name alice`,
|
||||
do: runS3UserDeleteCommand,
|
||||
},
|
||||
&s3ShellCommand{
|
||||
name: "s3.user.enable",
|
||||
help: `enable a filer-backed S3 user
|
||||
|
||||
s3.user.enable -name alice`,
|
||||
do: runS3UserEnableCommand,
|
||||
},
|
||||
&s3ShellCommand{
|
||||
name: "s3.user.disable",
|
||||
help: `disable a filer-backed S3 user
|
||||
|
||||
s3.user.disable -name alice`,
|
||||
do: runS3UserDisableCommand,
|
||||
},
|
||||
&s3ShellCommand{
|
||||
name: "s3.user.accesskey.list",
|
||||
help: `list access keys for a filer-backed S3 user
|
||||
|
||||
s3.user.accesskey.list -user alice`,
|
||||
do: runS3UserAccessKeyListCommand,
|
||||
},
|
||||
&s3ShellCommand{
|
||||
name: "s3.user.accesskey.create",
|
||||
help: `create an access key for a filer-backed S3 user
|
||||
|
||||
s3.user.accesskey.create -user alice -generate_credentials
|
||||
s3.user.accesskey.create -user alice -access_key AKIA... -secret_key secret`,
|
||||
do: runS3UserAccessKeyCreateCommand,
|
||||
},
|
||||
&s3ShellCommand{
|
||||
name: "s3.user.accesskey.delete",
|
||||
help: `delete an access key for a filer-backed S3 user
|
||||
|
||||
s3.user.accesskey.delete -user alice -access_key AKIA...`,
|
||||
do: runS3UserAccessKeyDeleteCommand,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func runS3UserListCommand(args []string, commandEnv *CommandEnv, writer io.Writer) error {
|
||||
fs := flag.NewFlagSet("s3.user.list", flag.ContinueOnError)
|
||||
fs.SetOutput(writer)
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if err == flag.ErrHelp {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return withS3ShellStore(commandEnv, func(ctx context.Context, store s3ShellStore) error {
|
||||
return runS3UserList(ctx, store, writer)
|
||||
})
|
||||
}
|
||||
|
||||
func runS3UserShowCommand(args []string, commandEnv *CommandEnv, writer io.Writer) error {
|
||||
fs := flag.NewFlagSet("s3.user.show", flag.ContinueOnError)
|
||||
fs.SetOutput(writer)
|
||||
name := fs.String("name", "", "user name")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if err == flag.ErrHelp {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if *name == "" {
|
||||
return fmt.Errorf("-name is required")
|
||||
}
|
||||
|
||||
return withS3ShellStore(commandEnv, func(ctx context.Context, store s3ShellStore) error {
|
||||
return runS3UserShow(ctx, store, *name, writer)
|
||||
})
|
||||
}
|
||||
|
||||
func runS3UserCreateCommand(args []string, commandEnv *CommandEnv, writer io.Writer) error {
|
||||
fs := flag.NewFlagSet("s3.user.create", flag.ContinueOnError)
|
||||
fs.SetOutput(writer)
|
||||
opts := s3UserCreateOptions{}
|
||||
fs.StringVar(&opts.name, "name", "", "user name")
|
||||
fs.StringVar(&opts.accessKey, "access_key", "", "access key to create")
|
||||
fs.StringVar(&opts.secretKey, "secret_key", "", "secret key to create")
|
||||
fs.BoolVar(&opts.generateCredentials, "generate_credentials", false, "generate an initial access key pair")
|
||||
fs.StringVar(&opts.accountID, "account_id", "", "account id")
|
||||
fs.StringVar(&opts.displayName, "display_name", "", "account display name")
|
||||
fs.StringVar(&opts.email, "email", "", "account email")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if err == flag.ErrHelp {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if opts.name == "" {
|
||||
return fmt.Errorf("-name is required")
|
||||
}
|
||||
|
||||
return withS3ShellStore(commandEnv, func(ctx context.Context, store s3ShellStore) error {
|
||||
return runS3UserCreate(ctx, store, opts, writer)
|
||||
})
|
||||
}
|
||||
|
||||
func runS3UserDeleteCommand(args []string, commandEnv *CommandEnv, writer io.Writer) error {
|
||||
fs := flag.NewFlagSet("s3.user.delete", flag.ContinueOnError)
|
||||
fs.SetOutput(writer)
|
||||
name := fs.String("name", "", "user name")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if err == flag.ErrHelp {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if *name == "" {
|
||||
return fmt.Errorf("-name is required")
|
||||
}
|
||||
|
||||
return withS3ShellStore(commandEnv, func(ctx context.Context, store s3ShellStore) error {
|
||||
return runS3UserDelete(ctx, store, *name, writer)
|
||||
})
|
||||
}
|
||||
|
||||
func runS3UserEnableCommand(args []string, commandEnv *CommandEnv, writer io.Writer) error {
|
||||
return runS3UserStatusCommand("s3.user.enable", false, args, commandEnv, writer)
|
||||
}
|
||||
|
||||
func runS3UserDisableCommand(args []string, commandEnv *CommandEnv, writer io.Writer) error {
|
||||
return runS3UserStatusCommand("s3.user.disable", true, args, commandEnv, writer)
|
||||
}
|
||||
|
||||
func runS3UserStatusCommand(commandName string, disabled bool, args []string, commandEnv *CommandEnv, writer io.Writer) error {
|
||||
fs := flag.NewFlagSet(commandName, flag.ContinueOnError)
|
||||
fs.SetOutput(writer)
|
||||
name := fs.String("name", "", "user name")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if err == flag.ErrHelp {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if *name == "" {
|
||||
return fmt.Errorf("-name is required")
|
||||
}
|
||||
|
||||
return withS3ShellStore(commandEnv, func(ctx context.Context, store s3ShellStore) error {
|
||||
return runS3UserSetDisabled(ctx, store, *name, disabled, writer)
|
||||
})
|
||||
}
|
||||
|
||||
func runS3UserAccessKeyListCommand(args []string, commandEnv *CommandEnv, writer io.Writer) error {
|
||||
fs := flag.NewFlagSet("s3.user.accesskey.list", flag.ContinueOnError)
|
||||
fs.SetOutput(writer)
|
||||
user := fs.String("user", "", "user name")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if err == flag.ErrHelp {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if *user == "" {
|
||||
return fmt.Errorf("-user is required")
|
||||
}
|
||||
|
||||
return withS3ShellStore(commandEnv, func(ctx context.Context, store s3ShellStore) error {
|
||||
return runS3UserAccessKeyList(ctx, store, *user, writer)
|
||||
})
|
||||
}
|
||||
|
||||
func runS3UserAccessKeyCreateCommand(args []string, commandEnv *CommandEnv, writer io.Writer) error {
|
||||
fs := flag.NewFlagSet("s3.user.accesskey.create", flag.ContinueOnError)
|
||||
fs.SetOutput(writer)
|
||||
opts := s3AccessKeyCreateOptions{}
|
||||
fs.StringVar(&opts.username, "user", "", "user name")
|
||||
fs.StringVar(&opts.accessKey, "access_key", "", "access key to create")
|
||||
fs.StringVar(&opts.secretKey, "secret_key", "", "secret key to create")
|
||||
fs.BoolVar(&opts.generateCredentials, "generate_credentials", false, "generate an access key pair")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if err == flag.ErrHelp {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if opts.username == "" {
|
||||
return fmt.Errorf("-user is required")
|
||||
}
|
||||
|
||||
return withS3ShellStore(commandEnv, func(ctx context.Context, store s3ShellStore) error {
|
||||
return runS3UserAccessKeyCreate(ctx, store, opts, writer)
|
||||
})
|
||||
}
|
||||
|
||||
func runS3UserAccessKeyDeleteCommand(args []string, commandEnv *CommandEnv, writer io.Writer) error {
|
||||
fs := flag.NewFlagSet("s3.user.accesskey.delete", flag.ContinueOnError)
|
||||
fs.SetOutput(writer)
|
||||
user := fs.String("user", "", "user name")
|
||||
accessKey := fs.String("access_key", "", "access key to delete")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if err == flag.ErrHelp {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if *user == "" {
|
||||
return fmt.Errorf("-user is required")
|
||||
}
|
||||
if *accessKey == "" {
|
||||
return fmt.Errorf("-access_key is required")
|
||||
}
|
||||
|
||||
return withS3ShellStore(commandEnv, func(ctx context.Context, store s3ShellStore) error {
|
||||
return runS3UserAccessKeyDelete(ctx, store, *user, *accessKey, writer)
|
||||
})
|
||||
}
|
||||
|
||||
func runS3UserList(ctx context.Context, store s3ShellStore, writer io.Writer) error {
|
||||
usernames, err := store.ListUsers(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sort.Strings(usernames)
|
||||
|
||||
fmt.Fprintln(writer, "NAME\tSOURCE\tSTATUS\tACCESS KEYS\tPOLICIES")
|
||||
for _, username := range usernames {
|
||||
identity, err := store.GetUser(ctx, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(writer, "%s\t%s\t%s\t%d\t%d\n",
|
||||
identity.Name,
|
||||
s3IdentitySource(identity),
|
||||
s3IdentityStatus(identity),
|
||||
len(identity.Credentials),
|
||||
len(identity.PolicyNames),
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runS3UserShow(ctx context.Context, store s3ShellStore, username string, writer io.Writer) error {
|
||||
identity, err := getS3User(ctx, store, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeS3IdentityDetails(writer, identity)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runS3UserCreate(ctx context.Context, store s3ShellStore, opts s3UserCreateOptions, writer io.Writer) error {
|
||||
existing, err := store.GetUser(ctx, opts.name)
|
||||
switch {
|
||||
case err == nil && existing != nil:
|
||||
if existing.IsStatic {
|
||||
return fmt.Errorf("user %q already exists in -s3.config; edit the static config for bootstrap users", opts.name)
|
||||
}
|
||||
return fmt.Errorf("user %q already exists", opts.name)
|
||||
case err != nil && !s3ShellIsNotFound(err):
|
||||
return err
|
||||
}
|
||||
|
||||
identity := &iam_pb.Identity{Name: opts.name}
|
||||
if opts.accountID != "" || opts.displayName != "" || opts.email != "" {
|
||||
identity.Account = &iam_pb.Account{
|
||||
Id: opts.accountID,
|
||||
DisplayName: opts.displayName,
|
||||
EmailAddress: opts.email,
|
||||
}
|
||||
}
|
||||
|
||||
createdCredential, err := s3BuildCredentialForCreate(ctx, store, opts.accessKey, opts.secretKey, opts.generateCredentials || opts.accessKey != "" || opts.secretKey != "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if createdCredential != nil {
|
||||
identity.Credentials = []*iam_pb.Credential{createdCredential}
|
||||
}
|
||||
|
||||
if err := store.CreateUser(ctx, identity); err != nil {
|
||||
if s3ShellIsAlreadyExists(err) {
|
||||
return fmt.Errorf("user %q already exists", opts.name)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(writer, "Created user %q.\n", opts.name)
|
||||
if createdCredential != nil {
|
||||
writeCreatedCredential(writer, createdCredential)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runS3UserDelete(ctx context.Context, store s3ShellStore, username string, writer io.Writer) error {
|
||||
identity, err := getS3User(ctx, store, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureMutableIdentity(identity, "delete"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := store.DeleteUser(ctx, username); err != nil {
|
||||
if s3ShellIsNotFound(err) {
|
||||
return fmt.Errorf("user %q not found", username)
|
||||
}
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(writer, "Deleted user %q.\n", username)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runS3UserSetDisabled(ctx context.Context, store s3ShellStore, username string, disabled bool, writer io.Writer) error {
|
||||
identity, err := getS3User(ctx, store, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureMutableIdentity(identity, ternary(disabled, "disable", "enable")); err != nil {
|
||||
return err
|
||||
}
|
||||
if identity.Disabled == disabled {
|
||||
fmt.Fprintf(writer, "User %q is already %s.\n", username, s3IdentityStatus(identity))
|
||||
return nil
|
||||
}
|
||||
|
||||
updated := proto.Clone(identity).(*iam_pb.Identity)
|
||||
updated.Disabled = disabled
|
||||
if err := store.UpdateUser(ctx, username, updated); err != nil {
|
||||
return err
|
||||
}
|
||||
if disabled {
|
||||
fmt.Fprintf(writer, "Disabled user %q.\n", username)
|
||||
} else {
|
||||
fmt.Fprintf(writer, "Enabled user %q.\n", username)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runS3UserAccessKeyList(ctx context.Context, store s3ShellStore, username string, writer io.Writer) error {
|
||||
identity, err := getS3User(ctx, store, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(writer, "USER\tACCESS KEY\tSTATUS\n")
|
||||
for _, credential := range sortedCredentials(identity.Credentials) {
|
||||
fmt.Fprintf(writer, "%s\t%s\t%s\n", username, credential.AccessKey, s3CredentialStatus(credential))
|
||||
}
|
||||
if len(identity.Credentials) == 0 {
|
||||
fmt.Fprintf(writer, "%s\t%s\t%s\n", username, "-", "-")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runS3UserAccessKeyCreate(ctx context.Context, store s3ShellStore, opts s3AccessKeyCreateOptions, writer io.Writer) error {
|
||||
identity, err := getS3User(ctx, store, opts.username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureMutableIdentity(identity, "create access keys for"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
createdCredential, err := s3BuildCredentialForCreate(ctx, store, opts.accessKey, opts.secretKey, opts.generateCredentials || opts.accessKey != "" || opts.secretKey != "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if createdCredential == nil {
|
||||
return fmt.Errorf("set -generate_credentials or provide -access_key and/or -secret_key")
|
||||
}
|
||||
|
||||
if err := store.CreateAccessKey(ctx, opts.username, createdCredential); err != nil {
|
||||
if s3ShellIsAlreadyExists(err) {
|
||||
return fmt.Errorf("access key %q is already in use", createdCredential.AccessKey)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(writer, "Created access key for user %q.\n", opts.username)
|
||||
writeCreatedCredential(writer, createdCredential)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runS3UserAccessKeyDelete(ctx context.Context, store s3ShellStore, username string, accessKey string, writer io.Writer) error {
|
||||
identity, err := getS3User(ctx, store, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureMutableIdentity(identity, "delete access keys for"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := store.DeleteAccessKey(ctx, username, accessKey); err != nil {
|
||||
if s3ShellIsNotFound(err) {
|
||||
return fmt.Errorf("access key %q not found for user %q", accessKey, username)
|
||||
}
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(writer, "Deleted access key %q for user %q.\n", accessKey, username)
|
||||
return nil
|
||||
}
|
||||
|
||||
func withS3ShellStore(commandEnv *CommandEnv, fn func(ctx context.Context, store s3ShellStore) error) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
store := &iamgrpc.IamGrpcStore{}
|
||||
store.SetFilerAddressFunc(func() pb.ServerAddress {
|
||||
return commandEnv.option.FilerAddress
|
||||
}, commandEnv.option.GrpcDialOption)
|
||||
|
||||
return fn(ctx, store)
|
||||
}
|
||||
|
||||
func ensureMutableIdentity(identity *iam_pb.Identity, action string) error {
|
||||
if identity != nil && identity.IsStatic {
|
||||
return fmt.Errorf("cannot %s user %q because it comes from -s3.config; edit the static config for bootstrap users", action, identity.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getS3User(ctx context.Context, store s3ShellStore, username string) (*iam_pb.Identity, error) {
|
||||
identity, err := store.GetUser(ctx, username)
|
||||
if err != nil {
|
||||
if s3ShellIsNotFound(err) {
|
||||
return nil, fmt.Errorf("user %q not found", username)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return identity, nil
|
||||
}
|
||||
|
||||
func s3BuildCredentialForCreate(ctx context.Context, store s3ShellStore, accessKey, secretKey string, shouldCreate bool) (*iam_pb.Credential, error) {
|
||||
if !shouldCreate {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if accessKey == "" {
|
||||
var err error
|
||||
accessKey, err = generateUniqueAccessKey(ctx, store)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if err := ensureAccessKeyAvailable(ctx, store, accessKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if secretKey == "" {
|
||||
var err error
|
||||
secretKey, err = weediam.GenerateSecretAccessKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generate secret key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &iam_pb.Credential{
|
||||
AccessKey: accessKey,
|
||||
SecretKey: secretKey,
|
||||
Status: weediam.AccessKeyStatusActive,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ensureAccessKeyAvailable(ctx context.Context, store s3ShellStore, accessKey string) error {
|
||||
_, err := store.GetUserByAccessKey(ctx, accessKey)
|
||||
if err == nil {
|
||||
return fmt.Errorf("%w: %s", errS3AccessKeyInUse, accessKey)
|
||||
}
|
||||
if s3ShellIsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func generateUniqueAccessKey(ctx context.Context, store s3ShellStore) (string, error) {
|
||||
for range 16 {
|
||||
accessKey, err := generateAccessKey()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := ensureAccessKeyAvailable(ctx, store, accessKey); err == nil {
|
||||
return accessKey, nil
|
||||
} else if !errors.Is(err, errS3AccessKeyInUse) {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("failed to generate a unique access key")
|
||||
}
|
||||
|
||||
func generateAccessKey() (string, error) {
|
||||
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
var raw [20]byte
|
||||
if _, err := rand.Read(raw[:]); err != nil {
|
||||
return "", fmt.Errorf("generate access key: %w", err)
|
||||
}
|
||||
for i := range raw {
|
||||
raw[i] = charset[int(raw[i])%len(charset)]
|
||||
}
|
||||
return string(raw[:]), nil
|
||||
}
|
||||
|
||||
func s3IdentitySource(identity *iam_pb.Identity) string {
|
||||
if identity != nil && identity.IsStatic {
|
||||
return "static"
|
||||
}
|
||||
return "dynamic"
|
||||
}
|
||||
|
||||
func s3IdentityStatus(identity *iam_pb.Identity) string {
|
||||
if identity != nil && identity.Disabled {
|
||||
return "disabled"
|
||||
}
|
||||
return "enabled"
|
||||
}
|
||||
|
||||
func s3CredentialStatus(credential *iam_pb.Credential) string {
|
||||
if credential == nil || credential.Status == "" {
|
||||
return weediam.AccessKeyStatusActive
|
||||
}
|
||||
return credential.Status
|
||||
}
|
||||
|
||||
func sortedCredentials(credentials []*iam_pb.Credential) []*iam_pb.Credential {
|
||||
out := append([]*iam_pb.Credential(nil), credentials...)
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
return out[i].AccessKey < out[j].AccessKey
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func writeCreatedCredential(writer io.Writer, credential *iam_pb.Credential) {
|
||||
fmt.Fprintf(writer, "Access Key: %s\n", credential.AccessKey)
|
||||
fmt.Fprintf(writer, "Secret Key: %s\n", credential.SecretKey)
|
||||
}
|
||||
|
||||
func writeS3IdentityDetails(writer io.Writer, identity *iam_pb.Identity) {
|
||||
fmt.Fprintf(writer, "Name: %s\n", identity.Name)
|
||||
fmt.Fprintf(writer, "Source: %s\n", s3IdentitySource(identity))
|
||||
fmt.Fprintf(writer, "Status: %s\n", s3IdentityStatus(identity))
|
||||
if identity.Account != nil {
|
||||
if identity.Account.Id != "" {
|
||||
fmt.Fprintf(writer, "Account ID: %s\n", identity.Account.Id)
|
||||
}
|
||||
if identity.Account.DisplayName != "" {
|
||||
fmt.Fprintf(writer, "Display Name: %s\n", identity.Account.DisplayName)
|
||||
}
|
||||
if identity.Account.EmailAddress != "" {
|
||||
fmt.Fprintf(writer, "Email: %s\n", identity.Account.EmailAddress)
|
||||
}
|
||||
}
|
||||
|
||||
if len(identity.Credentials) == 0 {
|
||||
fmt.Fprintln(writer, "Access Keys: none")
|
||||
} else {
|
||||
fmt.Fprintln(writer, "Access Keys:")
|
||||
for _, credential := range sortedCredentials(identity.Credentials) {
|
||||
fmt.Fprintf(writer, " %s (%s)\n", credential.AccessKey, s3CredentialStatus(credential))
|
||||
}
|
||||
}
|
||||
|
||||
if len(identity.PolicyNames) == 0 {
|
||||
fmt.Fprintln(writer, "Policies: none")
|
||||
} else {
|
||||
policies := append([]string(nil), identity.PolicyNames...)
|
||||
sort.Strings(policies)
|
||||
fmt.Fprintf(writer, "Policies: %s\n", strings.Join(policies, ", "))
|
||||
}
|
||||
|
||||
if len(identity.Actions) == 0 {
|
||||
fmt.Fprintln(writer, "Actions: none")
|
||||
} else {
|
||||
actions := append([]string(nil), identity.Actions...)
|
||||
sort.Strings(actions)
|
||||
fmt.Fprintf(writer, "Actions: %s\n", strings.Join(actions, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
func s3ShellIsNotFound(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if errors.Is(err, credential.ErrUserNotFound) ||
|
||||
errors.Is(err, credential.ErrAccessKeyNotFound) ||
|
||||
errors.Is(err, credential.ErrPolicyNotFound) {
|
||||
return true
|
||||
}
|
||||
if st, ok := status.FromError(err); ok && st.Code() == codes.NotFound {
|
||||
return true
|
||||
}
|
||||
return strings.Contains(strings.ToLower(err.Error()), "not found")
|
||||
}
|
||||
|
||||
func s3ShellIsAlreadyExists(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if errors.Is(err, credential.ErrUserAlreadyExists) {
|
||||
return true
|
||||
}
|
||||
if st, ok := status.FromError(err); ok && st.Code() == codes.AlreadyExists {
|
||||
return true
|
||||
}
|
||||
return strings.Contains(strings.ToLower(err.Error()), "already exists")
|
||||
}
|
||||
|
||||
func ternary[T any](cond bool, onTrue, onFalse T) T {
|
||||
if cond {
|
||||
return onTrue
|
||||
}
|
||||
return onFalse
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/credential/memory"
|
||||
weediam "github.com/seaweedfs/seaweedfs/weed/iam"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine"
|
||||
)
|
||||
|
||||
func newTestS3ShellStore(t *testing.T) *memory.MemoryStore {
|
||||
t.Helper()
|
||||
|
||||
store := &memory.MemoryStore{}
|
||||
if err := store.Initialize(nil, ""); err != nil {
|
||||
t.Fatalf("initialize memory store: %v", err)
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
func TestRunS3UserCreateGeneratesCredentials(t *testing.T) {
|
||||
store := newTestS3ShellStore(t)
|
||||
|
||||
var out bytes.Buffer
|
||||
err := runS3UserCreate(context.Background(), store, s3UserCreateOptions{
|
||||
name: "alice",
|
||||
generateCredentials: true,
|
||||
email: "alice@example.com",
|
||||
}, &out)
|
||||
if err != nil {
|
||||
t.Fatalf("runS3UserCreate: %v", err)
|
||||
}
|
||||
|
||||
user, err := store.GetUser(context.Background(), "alice")
|
||||
if err != nil {
|
||||
t.Fatalf("GetUser: %v", err)
|
||||
}
|
||||
if len(user.Credentials) != 1 {
|
||||
t.Fatalf("expected 1 credential, got %d", len(user.Credentials))
|
||||
}
|
||||
if got := user.Credentials[0].Status; got != weediam.AccessKeyStatusActive {
|
||||
t.Fatalf("expected %q status, got %q", weediam.AccessKeyStatusActive, got)
|
||||
}
|
||||
if user.Account == nil || user.Account.EmailAddress != "alice@example.com" {
|
||||
t.Fatalf("expected account email to be set, got %+v", user.Account)
|
||||
}
|
||||
|
||||
output := out.String()
|
||||
if !strings.Contains(output, `Created user "alice".`) {
|
||||
t.Fatalf("expected created message, got %q", output)
|
||||
}
|
||||
if !strings.Contains(output, "Access Key:") || !strings.Contains(output, "Secret Key:") {
|
||||
t.Fatalf("expected credential output, got %q", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunS3UserSetDisabledRejectsStaticUser(t *testing.T) {
|
||||
store := newTestS3ShellStore(t)
|
||||
if err := store.CreateUser(context.Background(), &iam_pb.Identity{
|
||||
Name: "bootstrap-admin",
|
||||
IsStatic: true,
|
||||
}); err != nil {
|
||||
t.Fatalf("CreateUser: %v", err)
|
||||
}
|
||||
|
||||
err := runS3UserSetDisabled(context.Background(), store, "bootstrap-admin", true, &bytes.Buffer{})
|
||||
if err == nil {
|
||||
t.Fatal("expected static user mutation to fail")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "-s3.config") {
|
||||
t.Fatalf("expected static user guidance, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunS3UserListIncludesSourceAndStatus(t *testing.T) {
|
||||
store := newTestS3ShellStore(t)
|
||||
if err := store.CreateUser(context.Background(), &iam_pb.Identity{
|
||||
Name: "alice",
|
||||
Credentials: []*iam_pb.Credential{
|
||||
{AccessKey: "ALICEKEY", SecretKey: "secret", Status: weediam.AccessKeyStatusActive},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("CreateUser alice: %v", err)
|
||||
}
|
||||
if err := store.CreateUser(context.Background(), &iam_pb.Identity{
|
||||
Name: "bootstrap-admin",
|
||||
IsStatic: true,
|
||||
Disabled: true,
|
||||
}); err != nil {
|
||||
t.Fatalf("CreateUser bootstrap-admin: %v", err)
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
if err := runS3UserList(context.Background(), store, &out); err != nil {
|
||||
t.Fatalf("runS3UserList: %v", err)
|
||||
}
|
||||
|
||||
output := out.String()
|
||||
if !strings.Contains(output, "NAME\tSOURCE\tSTATUS\tACCESS KEYS\tPOLICIES") {
|
||||
t.Fatalf("expected header, got %q", output)
|
||||
}
|
||||
if !strings.Contains(output, "alice\tdynamic\tenabled\t1\t0") {
|
||||
t.Fatalf("expected dynamic user row, got %q", output)
|
||||
}
|
||||
if !strings.Contains(output, "bootstrap-admin\tstatic\tdisabled\t0\t0") {
|
||||
t.Fatalf("expected static user row, got %q", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunS3UserAccessKeyCreateGeneratesCredential(t *testing.T) {
|
||||
store := newTestS3ShellStore(t)
|
||||
if err := store.CreateUser(context.Background(), &iam_pb.Identity{Name: "alice"}); err != nil {
|
||||
t.Fatalf("CreateUser: %v", err)
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
err := runS3UserAccessKeyCreate(context.Background(), store, s3AccessKeyCreateOptions{
|
||||
username: "alice",
|
||||
generateCredentials: true,
|
||||
}, &out)
|
||||
if err != nil {
|
||||
t.Fatalf("runS3UserAccessKeyCreate: %v", err)
|
||||
}
|
||||
|
||||
user, err := store.GetUser(context.Background(), "alice")
|
||||
if err != nil {
|
||||
t.Fatalf("GetUser: %v", err)
|
||||
}
|
||||
if len(user.Credentials) != 1 {
|
||||
t.Fatalf("expected 1 credential, got %d", len(user.Credentials))
|
||||
}
|
||||
if !strings.Contains(out.String(), `Created access key for user "alice".`) {
|
||||
t.Fatalf("expected create message, got %q", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunS3PolicyAttachUpdatesUserPolicies(t *testing.T) {
|
||||
store := newTestS3ShellStore(t)
|
||||
if err := store.CreateUser(context.Background(), &iam_pb.Identity{Name: "alice"}); err != nil {
|
||||
t.Fatalf("CreateUser: %v", err)
|
||||
}
|
||||
if err := store.CreatePolicy(context.Background(), "photos-rw", policy_engine.PolicyDocument{
|
||||
Version: "2012-10-17",
|
||||
}); err != nil {
|
||||
t.Fatalf("CreatePolicy: %v", err)
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
if err := runS3PolicyAttach(context.Background(), store, "alice", "photos-rw", &out); err != nil {
|
||||
t.Fatalf("runS3PolicyAttach: %v", err)
|
||||
}
|
||||
|
||||
user, err := store.GetUser(context.Background(), "alice")
|
||||
if err != nil {
|
||||
t.Fatalf("GetUser: %v", err)
|
||||
}
|
||||
if len(user.PolicyNames) != 1 || user.PolicyNames[0] != "photos-rw" {
|
||||
t.Fatalf("expected attached policy, got %v", user.PolicyNames)
|
||||
}
|
||||
if !strings.Contains(out.String(), `Attached policy "photos-rw" to user "alice".`) {
|
||||
t.Fatalf("expected attach message, got %q", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunS3PolicyDetachRejectsStaticUser(t *testing.T) {
|
||||
store := newTestS3ShellStore(t)
|
||||
if err := store.CreateUser(context.Background(), &iam_pb.Identity{
|
||||
Name: "bootstrap-admin",
|
||||
IsStatic: true,
|
||||
PolicyNames: []string{"photos-rw"},
|
||||
}); err != nil {
|
||||
t.Fatalf("CreateUser: %v", err)
|
||||
}
|
||||
if err := store.CreatePolicy(context.Background(), "photos-rw", policy_engine.PolicyDocument{
|
||||
Version: "2012-10-17",
|
||||
}); err != nil {
|
||||
t.Fatalf("CreatePolicy: %v", err)
|
||||
}
|
||||
|
||||
err := runS3PolicyDetach(context.Background(), store, "bootstrap-admin", "photos-rw", &bytes.Buffer{})
|
||||
if err == nil {
|
||||
t.Fatal("expected static user mutation to fail")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "-s3.config") {
|
||||
t.Fatalf("expected static user guidance, got %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user