diff --git a/weed/shell/command_s3_group_add_user.go b/weed/shell/command_s3_group_add_user.go new file mode 100644 index 000000000..3fa09fecd --- /dev/null +++ b/weed/shell/command_s3_group_add_user.go @@ -0,0 +1,95 @@ +package shell + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "io" + "time" + + "github.com/seaweedfs/seaweedfs/weed/pb" + "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" + "google.golang.org/grpc" +) + +func init() { + Commands = append(Commands, &commandS3GroupAddUser{}) +} + +type commandS3GroupAddUser struct { +} + +func (c *commandS3GroupAddUser) Name() string { + return "s3.group.add-user" +} + +func (c *commandS3GroupAddUser) Help() string { + return `add a user to an S3 IAM group + + s3.group.add-user -group -user +` +} + +func (c *commandS3GroupAddUser) HasTag(CommandTag) bool { + return false +} + +func (c *commandS3GroupAddUser) Do(args []string, commandEnv *CommandEnv, writer io.Writer) error { + f := flag.NewFlagSet(c.Name(), flag.ContinueOnError) + group := f.String("group", "", "group name") + user := f.String("user", "", "user name") + if err := f.Parse(args); err != nil { + return err + } + if *group == "" { + return fmt.Errorf("-group is required") + } + if *user == "" { + return fmt.Errorf("-user is required") + } + + return pb.WithGrpcClient(false, 0, func(conn *grpc.ClientConn) error { + client := iam_pb.NewSeaweedIdentityAccessManagementClient(conn) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + resp, err := client.GetConfiguration(ctx, &iam_pb.GetConfigurationRequest{}) + if err != nil { + return err + } + cfg := resp.GetConfiguration() + if cfg == nil { + return fmt.Errorf("no IAM configuration found") + } + + // Verify user exists + userFound := false + for _, id := range cfg.Identities { + if id.Name == *user { + userFound = true + break + } + } + if !userFound { + return fmt.Errorf("user %s not found", *user) + } + + for _, g := range cfg.Groups { + if g.Name == *group { + // Check if already a member + for _, m := range g.Members { + if m == *user { + return fmt.Errorf("user %s is already a member of group %s", *user, *group) + } + } + g.Members = append(g.Members, *user) + if _, err := client.PutConfiguration(ctx, &iam_pb.PutConfigurationRequest{Configuration: cfg}); err != nil { + return err + } + return json.NewEncoder(writer).Encode(map[string]string{"group": *group, "user": *user}) + } + } + return fmt.Errorf("group %s not found", *group) + }, commandEnv.option.FilerAddress.ToGrpcAddress(), false, commandEnv.option.GrpcDialOption) +} diff --git a/weed/shell/command_s3_group_create.go b/weed/shell/command_s3_group_create.go new file mode 100644 index 000000000..c6b4f3616 --- /dev/null +++ b/weed/shell/command_s3_group_create.go @@ -0,0 +1,79 @@ +package shell + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "io" + "time" + + "github.com/seaweedfs/seaweedfs/weed/pb" + "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" + "google.golang.org/grpc" +) + +func init() { + Commands = append(Commands, &commandS3GroupCreate{}) +} + +type commandS3GroupCreate struct { +} + +func (c *commandS3GroupCreate) Name() string { + return "s3.group.create" +} + +func (c *commandS3GroupCreate) Help() string { + return `create an S3 IAM group + + s3.group.create -name + + Creates a new empty group. Add users with s3.group.add-user and + attach policies with s3.policy.attach or the IAM API. +` +} + +func (c *commandS3GroupCreate) HasTag(CommandTag) bool { + return false +} + +func (c *commandS3GroupCreate) Do(args []string, commandEnv *CommandEnv, writer io.Writer) error { + f := flag.NewFlagSet(c.Name(), flag.ContinueOnError) + name := f.String("name", "", "group name") + if err := f.Parse(args); err != nil { + return err + } + if *name == "" { + return fmt.Errorf("-name is required") + } + + return pb.WithGrpcClient(false, 0, func(conn *grpc.ClientConn) error { + client := iam_pb.NewSeaweedIdentityAccessManagementClient(conn) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + resp, err := client.GetConfiguration(ctx, &iam_pb.GetConfigurationRequest{}) + if err != nil { + return err + } + cfg := resp.GetConfiguration() + if cfg == nil { + return fmt.Errorf("no IAM configuration found") + } + + for _, g := range cfg.Groups { + if g.Name == *name { + return fmt.Errorf("group %s already exists", *name) + } + } + + cfg.Groups = append(cfg.Groups, &iam_pb.Group{Name: *name}) + + if _, err := client.PutConfiguration(ctx, &iam_pb.PutConfigurationRequest{Configuration: cfg}); err != nil { + return err + } + + return json.NewEncoder(writer).Encode(map[string]string{"group": *name}) + }, commandEnv.option.FilerAddress.ToGrpcAddress(), false, commandEnv.option.GrpcDialOption) +} diff --git a/weed/shell/command_s3_group_delete.go b/weed/shell/command_s3_group_delete.go new file mode 100644 index 000000000..6ae88bffe --- /dev/null +++ b/weed/shell/command_s3_group_delete.go @@ -0,0 +1,81 @@ +package shell + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "io" + "time" + + "github.com/seaweedfs/seaweedfs/weed/pb" + "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" + "google.golang.org/grpc" +) + +func init() { + Commands = append(Commands, &commandS3GroupDelete{}) +} + +type commandS3GroupDelete struct { +} + +func (c *commandS3GroupDelete) Name() string { + return "s3.group.delete" +} + +func (c *commandS3GroupDelete) Help() string { + return `delete an S3 IAM group + + s3.group.delete -name + + The group must have no members and no attached policies. +` +} + +func (c *commandS3GroupDelete) HasTag(CommandTag) bool { + return false +} + +func (c *commandS3GroupDelete) Do(args []string, commandEnv *CommandEnv, writer io.Writer) error { + f := flag.NewFlagSet(c.Name(), flag.ContinueOnError) + name := f.String("name", "", "group name") + if err := f.Parse(args); err != nil { + return err + } + if *name == "" { + return fmt.Errorf("-name is required") + } + + return pb.WithGrpcClient(false, 0, func(conn *grpc.ClientConn) error { + client := iam_pb.NewSeaweedIdentityAccessManagementClient(conn) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + resp, err := client.GetConfiguration(ctx, &iam_pb.GetConfigurationRequest{}) + if err != nil { + return err + } + cfg := resp.GetConfiguration() + if cfg == nil { + return fmt.Errorf("no IAM configuration found") + } + + for i, g := range cfg.Groups { + if g.Name == *name { + if len(g.Members) > 0 { + return fmt.Errorf("cannot delete group %s: has %d member(s)", *name, len(g.Members)) + } + if len(g.PolicyNames) > 0 { + return fmt.Errorf("cannot delete group %s: has %d attached policy(ies)", *name, len(g.PolicyNames)) + } + cfg.Groups = append(cfg.Groups[:i], cfg.Groups[i+1:]...) + if _, err := client.PutConfiguration(ctx, &iam_pb.PutConfigurationRequest{Configuration: cfg}); err != nil { + return err + } + return json.NewEncoder(writer).Encode(map[string]string{"deleted": *name}) + } + } + return fmt.Errorf("group %s not found", *name) + }, commandEnv.option.FilerAddress.ToGrpcAddress(), false, commandEnv.option.GrpcDialOption) +} diff --git a/weed/shell/command_s3_group_list.go b/weed/shell/command_s3_group_list.go new file mode 100644 index 000000000..c4e28af80 --- /dev/null +++ b/weed/shell/command_s3_group_list.go @@ -0,0 +1,78 @@ +package shell + +import ( + "context" + "encoding/json" + "io" + "time" + + "github.com/seaweedfs/seaweedfs/weed/pb" + "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" + "google.golang.org/grpc" +) + +func init() { + Commands = append(Commands, &commandS3GroupList{}) +} + +type commandS3GroupList struct { +} + +func (c *commandS3GroupList) Name() string { + return "s3.group.list" +} + +func (c *commandS3GroupList) Help() string { + return `list S3 IAM groups + + s3.group.list + + Output: JSON array of groups with members and policies. +` +} + +func (c *commandS3GroupList) HasTag(CommandTag) bool { + return false +} + +type s3GroupListEntry struct { + Name string `json:"name"` + Status string `json:"status"` + Members int `json:"members"` + Policies []string `json:"policies"` +} + +func (c *commandS3GroupList) Do(args []string, commandEnv *CommandEnv, writer io.Writer) error { + return pb.WithGrpcClient(false, 0, func(conn *grpc.ClientConn) error { + client := iam_pb.NewSeaweedIdentityAccessManagementClient(conn) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + resp, err := client.GetConfiguration(ctx, &iam_pb.GetConfigurationRequest{}) + if err != nil { + return err + } + + var result []s3GroupListEntry + for _, g := range resp.Configuration.GetGroups() { + status := "enabled" + if g.Disabled { + status = "disabled" + } + policies := g.PolicyNames + if policies == nil { + policies = []string{} + } + result = append(result, s3GroupListEntry{ + Name: g.Name, + Status: status, + Members: len(g.Members), + Policies: policies, + }) + } + if result == nil { + result = []s3GroupListEntry{} + } + return json.NewEncoder(writer).Encode(result) + }, commandEnv.option.FilerAddress.ToGrpcAddress(), false, commandEnv.option.GrpcDialOption) +} diff --git a/weed/shell/command_s3_group_remove_user.go b/weed/shell/command_s3_group_remove_user.go new file mode 100644 index 000000000..80c89d47f --- /dev/null +++ b/weed/shell/command_s3_group_remove_user.go @@ -0,0 +1,82 @@ +package shell + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "io" + "time" + + "github.com/seaweedfs/seaweedfs/weed/pb" + "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" + "google.golang.org/grpc" +) + +func init() { + Commands = append(Commands, &commandS3GroupRemoveUser{}) +} + +type commandS3GroupRemoveUser struct { +} + +func (c *commandS3GroupRemoveUser) Name() string { + return "s3.group.remove-user" +} + +func (c *commandS3GroupRemoveUser) Help() string { + return `remove a user from an S3 IAM group + + s3.group.remove-user -group -user +` +} + +func (c *commandS3GroupRemoveUser) HasTag(CommandTag) bool { + return false +} + +func (c *commandS3GroupRemoveUser) Do(args []string, commandEnv *CommandEnv, writer io.Writer) error { + f := flag.NewFlagSet(c.Name(), flag.ContinueOnError) + group := f.String("group", "", "group name") + user := f.String("user", "", "user name") + if err := f.Parse(args); err != nil { + return err + } + if *group == "" { + return fmt.Errorf("-group is required") + } + if *user == "" { + return fmt.Errorf("-user is required") + } + + return pb.WithGrpcClient(false, 0, func(conn *grpc.ClientConn) error { + client := iam_pb.NewSeaweedIdentityAccessManagementClient(conn) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + resp, err := client.GetConfiguration(ctx, &iam_pb.GetConfigurationRequest{}) + if err != nil { + return err + } + cfg := resp.GetConfiguration() + if cfg == nil { + return fmt.Errorf("no IAM configuration found") + } + + for _, g := range cfg.Groups { + if g.Name == *group { + for i, m := range g.Members { + if m == *user { + g.Members = append(g.Members[:i], g.Members[i+1:]...) + if _, err := client.PutConfiguration(ctx, &iam_pb.PutConfigurationRequest{Configuration: cfg}); err != nil { + return err + } + return json.NewEncoder(writer).Encode(map[string]string{"group": *group, "removed": *user}) + } + } + return fmt.Errorf("user %s is not a member of group %s", *user, *group) + } + } + return fmt.Errorf("group %s not found", *group) + }, commandEnv.option.FilerAddress.ToGrpcAddress(), false, commandEnv.option.GrpcDialOption) +} diff --git a/weed/shell/command_s3_group_show.go b/weed/shell/command_s3_group_show.go new file mode 100644 index 000000000..4514cd8c0 --- /dev/null +++ b/weed/shell/command_s3_group_show.go @@ -0,0 +1,91 @@ +package shell + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "io" + "time" + + "github.com/seaweedfs/seaweedfs/weed/pb" + "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" + "google.golang.org/grpc" +) + +func init() { + Commands = append(Commands, &commandS3GroupShow{}) +} + +type commandS3GroupShow struct { +} + +func (c *commandS3GroupShow) Name() string { + return "s3.group.show" +} + +func (c *commandS3GroupShow) Help() string { + return `show details of an S3 IAM group + + s3.group.show -name + + Output: JSON with group name, status, members, and attached policies. +` +} + +func (c *commandS3GroupShow) HasTag(CommandTag) bool { + return false +} + +type s3GroupShowResult struct { + Name string `json:"name"` + Status string `json:"status"` + Members []string `json:"members"` + Policies []string `json:"policies"` +} + +func (c *commandS3GroupShow) Do(args []string, commandEnv *CommandEnv, writer io.Writer) error { + f := flag.NewFlagSet(c.Name(), flag.ContinueOnError) + name := f.String("name", "", "group name") + if err := f.Parse(args); err != nil { + return err + } + if *name == "" { + return fmt.Errorf("-name is required") + } + + return pb.WithGrpcClient(false, 0, func(conn *grpc.ClientConn) error { + client := iam_pb.NewSeaweedIdentityAccessManagementClient(conn) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + resp, err := client.GetConfiguration(ctx, &iam_pb.GetConfigurationRequest{}) + if err != nil { + return err + } + + for _, g := range resp.Configuration.GetGroups() { + if g.Name == *name { + status := "enabled" + if g.Disabled { + status = "disabled" + } + members := g.Members + if members == nil { + members = []string{} + } + policies := g.PolicyNames + if policies == nil { + policies = []string{} + } + return json.NewEncoder(writer).Encode(s3GroupShowResult{ + Name: g.Name, + Status: status, + Members: members, + Policies: policies, + }) + } + } + return fmt.Errorf("group %s not found", *name) + }, commandEnv.option.FilerAddress.ToGrpcAddress(), false, commandEnv.option.GrpcDialOption) +}