mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-05-18 07:41:31 +00:00
- Use text/tabwriter in runS3UserList and runS3UserAccessKeyList for properly aligned column output - Remove fragile strings.Contains "not found" and "already exists" fallbacks in s3ShellIsNotFound and s3ShellIsAlreadyExists; rely on typed errors and gRPC status codes only - Update test assertions to match tabwriter-formatted output
713 lines
21 KiB
Go
713 lines
21 KiB
Go
package shell
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strings"
|
|
"text/tabwriter"
|
|
"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)
|
|
|
|
tw := tabwriter.NewWriter(writer, 0, 0, 2, ' ', 0)
|
|
fmt.Fprintln(tw, "NAME\tSOURCE\tSTATUS\tACCESS KEYS\tPOLICIES")
|
|
for _, username := range usernames {
|
|
identity, err := store.GetUser(ctx, username)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(tw, "%s\t%s\t%s\t%d\t%d\n",
|
|
identity.Name,
|
|
s3IdentitySource(identity),
|
|
s3IdentityStatus(identity),
|
|
len(identity.Credentials),
|
|
len(identity.PolicyNames),
|
|
)
|
|
}
|
|
return tw.Flush()
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
tw := tabwriter.NewWriter(writer, 0, 0, 2, ' ', 0)
|
|
fmt.Fprintln(tw, "USER\tACCESS KEY\tSTATUS")
|
|
for _, credential := range sortedCredentials(identity.Credentials) {
|
|
fmt.Fprintf(tw, "%s\t%s\t%s\n", username, credential.AccessKey, s3CredentialStatus(credential))
|
|
}
|
|
if len(identity.Credentials) == 0 {
|
|
fmt.Fprintf(tw, "%s\t%s\t%s\n", username, "-", "-")
|
|
}
|
|
return tw.Flush()
|
|
}
|
|
|
|
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 false
|
|
}
|
|
|
|
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 false
|
|
}
|
|
|
|
func ternary[T any](cond bool, onTrue, onFalse T) T {
|
|
if cond {
|
|
return onTrue
|
|
}
|
|
return onFalse
|
|
}
|