Files
seaweedFS/weed/shell/command_s3_configure.go
Chris Lu 6bf088cec9 IAM Policy Management via gRPC (#8109)
* Add IAM gRPC service definition

- Add GetConfiguration/PutConfiguration for config management
- Add CreateUser/GetUser/UpdateUser/DeleteUser/ListUsers for user management
- Add CreateAccessKey/DeleteAccessKey/GetUserByAccessKey for access key management
- Methods mirror existing IAM HTTP API functionality

* Add IAM gRPC handlers on filer server

- Implement IamGrpcServer with CredentialManager integration
- Handle configuration get/put operations
- Handle user CRUD operations
- Handle access key create/delete operations
- All methods delegate to CredentialManager for actual storage

* Wire IAM gRPC service to filer server

- Add CredentialManager field to FilerOption and FilerServer
- Import credential store implementations in filer command
- Initialize CredentialManager from credential.toml if available
- Register IAM gRPC service on filer gRPC server
- Enable credential management via gRPC alongside existing filer services

* Regenerate IAM protobuf with gRPC service methods

* iam_pb: add Policy Management to protobuf definitions

* credential: implement PolicyManager in credential stores

* filer: implement IAM Policy Management RPCs

* shell: add s3.policy command

* test: add integration test for s3.policy

* test: fix compilation errors in policy_test

* pb

* fmt

* test

* weed shell: add -policies flag to s3.configure

This allows linking/unlinking IAM policies to/from identities
directly from the s3.configure command.

* test: verify s3.configure policy linking and fix port allocation

- Added test case for linking policies to users via s3.configure
- Implemented findAvailablePortPair to ensure HTTP and gRPC ports
  are both available, avoiding conflicts with randomized port assignments.
- Updated assertion to match jsonpb output (policyNames)

* credential: add StoreTypeGrpc constant

* credential: add IAM gRPC store boilerplate

* credential: implement identity methods in gRPC store

* credential: implement policy methods in gRPC store

* admin: use gRPC credential store for AdminServer

This ensures that all IAM and policy changes made through the Admin UI
are persisted via the Filer's IAM gRPC service instead of direct file manipulation.

* shell: s3.configure use granular IAM gRPC APIs instead of full config patching

* shell: s3.configure use granular IAM gRPC APIs

* shell: replace deprecated ioutil with os in s3.policy

* filer: use gRPC FailedPrecondition for unconfigured credential manager

* test: improve s3.policy integration tests and fix error checks

* ci: add s3 policy shell integration tests to github workflow

* filer: fix LoadCredentialConfiguration error handling

* credential/grpc: propagate unmarshal errors in GetPolicies

* filer/grpc: improve error handling and validation

* shell: use gRPC status codes in s3.configure

* credential: document PutPolicy as create-or-replace

* credential/postgres: reuse CreatePolicy in PutPolicy to deduplicate logic

* shell: add timeout context and strictly enforce flags in s3.policy

* iam: standardize policy content field naming in gRPC and proto

* shell: extract slice helper functions in s3.configure

* filer: map credential store errors to gRPC status codes

* filer: add input validation for UpdateUser and CreateAccessKey

* iam: improve validation in policy and config handlers

* filer: ensure IAM service registration by defaulting credential manager

* credential: add GetStoreName method to manager

* test: verify policy deletion in integration test
2026-01-25 13:39:30 -08:00

282 lines
8.1 KiB
Go

package shell
import (
"bytes"
"context"
"flag"
"fmt"
"io"
"strings"
"github.com/seaweedfs/seaweedfs/weed/filer"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func init() {
Commands = append(Commands, &commandS3Configure{})
}
type commandS3Configure struct {
}
func (c *commandS3Configure) Name() string {
return "s3.configure"
}
func (c *commandS3Configure) Help() string {
return `configure and apply s3 options for each bucket
# 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
`
}
func (c *commandS3Configure) HasTag(CommandTag) bool {
return false
}
func (c *commandS3Configure) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
s3ConfigureCommand := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
actions := s3ConfigureCommand.String("actions", "", "comma separated actions names: Read,Write,List,Tagging,Admin")
user := s3ConfigureCommand.String("user", "", "user name")
buckets := s3ConfigureCommand.String("buckets", "", "bucket name")
accessKey := s3ConfigureCommand.String("access_key", "", "specify the access key")
secretKey := s3ConfigureCommand.String("secret_key", "", "specify the secret key")
accountId := s3ConfigureCommand.String("account_id", "", "specify the account id")
accountDisplayName := s3ConfigureCommand.String("account_display_name", "", "specify the account display name")
accountEmail := s3ConfigureCommand.String("account_email", "", "specify the account email address")
policies := s3ConfigureCommand.String("policies", "", "comma separated policy names")
isDelete := s3ConfigureCommand.Bool("delete", false, "delete users, actions, access keys or policies")
apply := s3ConfigureCommand.Bool("apply", false, "update and apply s3 configuration")
if err = s3ConfigureCommand.Parse(args); err != nil {
return nil
}
// Case 1: List configuration (no user specified)
if *user == "" {
return c.listConfiguration(commandEnv, writer)
}
// Case 2: Modify specific user
var identity *iam_pb.Identity
var isNewUser bool
err = pb.WithGrpcClient(false, 0, func(conn *grpc.ClientConn) error {
client := iam_pb.NewSeaweedIdentityAccessManagementClient(conn)
// Try to get existing user
resp, getErr := client.GetUser(context.Background(), &iam_pb.GetUserRequest{
Username: *user,
})
if getErr == nil {
identity = resp.Identity
if identity == nil {
// Should not happen if err is nil, but handle defensively
isNewUser = true
identity = &iam_pb.Identity{Name: *user}
}
} else {
st, ok := status.FromError(getErr)
if ok && st.Code() == codes.NotFound {
isNewUser = true
identity = &iam_pb.Identity{
Name: *user,
Credentials: []*iam_pb.Credential{},
Actions: []string{},
PolicyNames: []string{},
}
} else {
return fmt.Errorf("failed to get user %s: %v", *user, getErr)
}
}
// Apply changes to identity object
if err := c.applyChanges(identity, isNewUser, actions, buckets, accessKey, secretKey, policies, isDelete, accountId, accountDisplayName, accountEmail); err != nil {
return err
}
// Print changes (Simulation)
var buf bytes.Buffer
filer.ProtoToText(&buf, identity)
fmt.Fprint(writer, buf.String())
fmt.Fprintln(writer)
if !*apply {
infoAboutSimulationMode(writer, *apply, "-apply")
return nil
}
// Apply changes
if *isDelete && *actions == "" && *accessKey == "" && *buckets == "" && *policies == "" {
// Delete User
_, err := client.DeleteUser(context.Background(), &iam_pb.DeleteUserRequest{Username: *user})
return err
} else {
// Create or Update User
if isNewUser {
_, err := client.CreateUser(context.Background(), &iam_pb.CreateUserRequest{Identity: identity})
return err
} else {
_, err := client.UpdateUser(context.Background(), &iam_pb.UpdateUserRequest{Username: *user, Identity: identity})
return err
}
}
}, commandEnv.option.FilerAddress.ToGrpcAddress(), false, commandEnv.option.GrpcDialOption)
return err
}
func (c *commandS3Configure) listConfiguration(commandEnv *CommandEnv, writer io.Writer) error {
return pb.WithGrpcClient(false, 0, func(conn *grpc.ClientConn) error {
client := iam_pb.NewSeaweedIdentityAccessManagementClient(conn)
resp, err := client.GetConfiguration(context.Background(), &iam_pb.GetConfigurationRequest{})
if err != nil {
return err
}
var buf bytes.Buffer
filer.ProtoToText(&buf, resp.Configuration)
fmt.Fprint(writer, buf.String())
fmt.Fprintln(writer)
return nil
}, commandEnv.option.FilerAddress.ToGrpcAddress(), false, commandEnv.option.GrpcDialOption)
}
func (c *commandS3Configure) applyChanges(identity *iam_pb.Identity, isNewUser bool, actions, buckets, accessKey, secretKey, policies *string, isDelete *bool, accountId, accountDisplayName, accountEmail *string) error {
// Helper to update account info
if *accountId != "" || *accountDisplayName != "" || *accountEmail != "" {
if identity.Account == nil {
identity.Account = &iam_pb.Account{}
}
if *accountId != "" {
identity.Account.Id = *accountId
}
if *accountDisplayName != "" {
identity.Account.DisplayName = *accountDisplayName
}
if *accountEmail != "" {
identity.Account.EmailAddress = *accountEmail
}
}
// Prepare lists
var cmdActions []string
if *actions != "" {
for _, action := range strings.Split(*actions, ",") {
if *buckets == "" {
cmdActions = append(cmdActions, action)
} else {
for _, bucket := range strings.Split(*buckets, ",") {
cmdActions = append(cmdActions, fmt.Sprintf("%s:%s", action, bucket))
}
}
}
}
var cmdPolicies []string
if *policies != "" {
for _, policy := range strings.Split(*policies, ",") {
if policy != "" {
cmdPolicies = append(cmdPolicies, policy)
}
}
}
if *isDelete {
// DELETE LOGIC
// Remove Actions
if len(cmdActions) > 0 {
identity.Actions = removeFromSlice(identity.Actions, cmdActions)
}
// Remove Credentials
if *accessKey != "" {
var keepCredentials []*iam_pb.Credential
for _, cred := range identity.Credentials {
if cred.AccessKey != *accessKey {
keepCredentials = append(keepCredentials, cred)
}
}
identity.Credentials = keepCredentials
}
// Remove Policies
if len(cmdPolicies) > 0 {
identity.PolicyNames = removeFromSlice(identity.PolicyNames, cmdPolicies)
}
} else {
// ADD/UPDATE LOGIC
// Add Actions
identity.Actions = addUniqueToSlice(identity.Actions, cmdActions)
// Add/Update Credentials
if *accessKey != "" && identity.Name != "anonymous" {
found := false
for _, cred := range identity.Credentials {
if cred.AccessKey == *accessKey {
found = true
if *secretKey != "" {
cred.SecretKey = *secretKey
}
break
}
}
if !found {
if *secretKey == "" {
return fmt.Errorf("secret_key is required when adding a new access_key")
}
identity.Credentials = append(identity.Credentials, &iam_pb.Credential{
AccessKey: *accessKey,
SecretKey: *secretKey,
})
}
}
// Add Policies
identity.PolicyNames = addUniqueToSlice(identity.PolicyNames, cmdPolicies)
}
return nil
}
// Helper to remove items from a slice
func removeFromSlice(current []string, toRemove []string) []string {
removeSet := make(map[string]struct{}, len(toRemove))
for _, item := range toRemove {
removeSet[item] = struct{}{}
}
var result []string
for _, item := range current {
if _, found := removeSet[item]; !found {
result = append(result, item)
}
}
return result
}
// Helper to add unique items to a slice
func addUniqueToSlice(current []string, toAdd []string) []string {
existingSet := make(map[string]struct{}, len(current))
for _, item := range current {
existingSet[item] = struct{}{}
}
for _, item := range toAdd {
if _, found := existingSet[item]; !found {
current = append(current, item)
}
}
return current
}