Files
seaweedfs/weed/credential/propagating_store.go
T
Chris LuandClaude Opus 5.5 d727c6970f plugin worker: stop forwarding the stream's admin token to job RPCs (#12391)
* plugin worker: keep the stream's admin token out of job contexts

The bearer token minted when the worker opens its control stream was
attached to connCtx, which every detection and execution context derives
from. Job RPCs therefore forwarded that connect-time token, and when a
job added its own fresh token (s3_lifecycle's LifecycleDelete) the S3
server validated the stale one first and rejected the call once it
expired.

Attach the token only to the WorkerStream call, as the Rust worker does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* credential: replace an inherited admin token instead of appending one

WithS3InternalAdminAuth appended its fresh Bearer token, so a context
that already carried an authorization value sent both, and the S3
gateway's checkAdminAuth validates only the first. Set the header on a
copy of the outgoing metadata so the fresh token is the only one sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* plugin worker: read the captured job metadata after the stream ends

Both result channels could be ready at once, letting select fail a
successful run. The handler reports before JobCompleted reaches admin,
so wait for the stream to finish and then read the captured metadata.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* plugin worker: test that dropping the connection cancels running jobs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 05:26:12 -08:00

607 lines
22 KiB
Go

package credential
import (
"context"
"encoding/json"
"sync"
"time"
"github.com/seaweedfs/seaweedfs/weed/cluster"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/pb/s3_pb"
"github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine"
"github.com/seaweedfs/seaweedfs/weed/security"
"github.com/seaweedfs/seaweedfs/weed/util"
"github.com/seaweedfs/seaweedfs/weed/wdclient"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
var _ CredentialStore = &PropagatingCredentialStore{}
var _ PolicyManager = &PropagatingCredentialStore{}
type propagatingManagedPolicyLoader interface {
LoadManagedPolicies(ctx context.Context) ([]*iam_pb.Policy, error)
}
type propagatingInlinePolicyLoader interface {
LoadInlinePolicies(ctx context.Context) (map[string]map[string]policy_engine.PolicyDocument, error)
}
type PropagatingCredentialStore struct {
CredentialStore
masterClient *wdclient.MasterClient
grpcDialOption grpc.DialOption
}
func NewPropagatingCredentialStore(upstream CredentialStore, masterClient *wdclient.MasterClient, grpcDialOption grpc.DialOption) *PropagatingCredentialStore {
return &PropagatingCredentialStore{
CredentialStore: upstream,
masterClient: masterClient,
grpcDialOption: grpcDialOption,
}
}
func (s *PropagatingCredentialStore) SetFilerAddressFunc(getFiler func() pb.ServerAddress, grpcDialOption grpc.DialOption) {
if setter, ok := s.CredentialStore.(FilerAddressSetter); ok {
setter.SetFilerAddressFunc(getFiler, grpcDialOption)
}
}
// WithS3InternalAdminAuth sets a fresh Bearer token signed with
// jwt.filer_signing.key on the outgoing context, replacing any it already
// carries, so the S3 gateway's internal gRPC handlers (IAM cache, lifecycle)
// accept the call. With no key configured it is a no-op, matching the S3
// handler's checkAdminAuth. Returns the token's lifetime (0 = no expiry) so
// callers can cap any downstream timeout below it.
func WithS3InternalAdminAuth(ctx context.Context) (context.Context, time.Duration) {
signingKey := util.GetViper().GetString("jwt.filer_signing.key")
if signingKey == "" {
return ctx, 0
}
expiresAfterSec := util.GetViper().GetInt("jwt.filer_signing.expires_after_seconds")
token := security.GenJwtForFilerAdmin(security.SigningKey(signingKey), expiresAfterSec)
if token == "" {
return ctx, 0
}
md, _ := metadata.FromOutgoingContext(ctx)
md = md.Copy()
md.Set("authorization", security.BearerPrefix+string(token))
return metadata.NewOutgoingContext(ctx, md), time.Duration(expiresAfterSec) * time.Second
}
func (s *PropagatingCredentialStore) propagateChange(ctx context.Context, fn func(context.Context, s3_pb.SeaweedS3IamCacheClient) error) {
if s.masterClient == nil {
return
}
// List S3 servers
var s3Servers []string
err := s.masterClient.WithClient(ctx, false, func(client master_pb.SeaweedClient) error {
glog.V(4).Infof("IAM: listing S3 servers (FilerGroup: '%s')", s.masterClient.FilerGroup)
resp, err := client.ListClusterNodes(ctx, &master_pb.ListClusterNodesRequest{
ClientType: cluster.S3Type,
FilerGroup: s.masterClient.FilerGroup,
})
if err != nil {
glog.Warningf("failed to list S3 servers: %v", err)
return err
}
for _, node := range resp.ClusterNodes {
s3Servers = append(s3Servers, node.Address)
}
return nil
})
if err != nil {
glog.Warningf("failed to list s3 servers via master client: %v", err)
return
}
glog.V(1).Infof("IAM: propagating change to %d S3 servers: %v", len(s3Servers), s3Servers)
// Mint the admin token after master discovery so master retries can't burn
// through the token lifetime before the peer fan-out begins. Cap the
// propagation deadline below the token's expiry so slower peers don't see
// an expired token.
authedCtx, tokenTTL := WithS3InternalAdminAuth(ctx)
propagateTimeout := 10 * time.Second
if tokenTTL > 0 && tokenTTL < propagateTimeout {
propagateTimeout = tokenTTL
}
propagateCtx, cancel := context.WithTimeout(authedCtx, propagateTimeout)
defer cancel()
var wg sync.WaitGroup
for _, server := range s3Servers {
wg.Add(1)
go func(server string) {
defer wg.Done()
err := pb.WithGrpcClient(context.Background(), false, 0, func(conn *grpc.ClientConn) error {
glog.V(4).Infof("IAM: successfully connected to S3 server %s for propagation", server)
client := s3_pb.NewSeaweedS3IamCacheClient(conn)
return fn(propagateCtx, client)
}, server, false, s.grpcDialOption)
if err != nil {
glog.Warningf("failed to propagate change to s3 server %s: %v", server, err)
}
}(server)
}
wg.Wait()
}
func (s *PropagatingCredentialStore) AttachUserPolicy(ctx context.Context, username string, policyName string) error {
glog.V(4).Infof("IAM: PropagatingCredentialStore.AttachUserPolicy %s -> %s", username, policyName)
if err := s.CredentialStore.AttachUserPolicy(ctx, username, policyName); err != nil {
return err
}
// Fetch updated identity to propagate
identity, err := s.CredentialStore.GetUser(ctx, username)
if err != nil {
glog.Warningf("failed to get user %s after attaching policy: %v", username, err)
return nil
}
s.propagateChange(ctx, func(tx context.Context, client s3_pb.SeaweedS3IamCacheClient) error {
_, err := client.PutIdentity(tx, &iam_pb.PutIdentityRequest{Identity: identity})
return err
})
return nil
}
func (s *PropagatingCredentialStore) DetachUserPolicy(ctx context.Context, username string, policyName string) error {
glog.V(4).Infof("IAM: PropagatingCredentialStore.DetachUserPolicy %s -> %s", username, policyName)
if err := s.CredentialStore.DetachUserPolicy(ctx, username, policyName); err != nil {
return err
}
// Fetch updated identity to propagate
identity, err := s.CredentialStore.GetUser(ctx, username)
if err != nil {
glog.Warningf("failed to get user %s after detaching policy: %v", username, err)
return nil
}
s.propagateChange(ctx, func(tx context.Context, client s3_pb.SeaweedS3IamCacheClient) error {
_, err := client.PutIdentity(tx, &iam_pb.PutIdentityRequest{Identity: identity})
return err
})
return nil
}
func (s *PropagatingCredentialStore) ListAttachedUserPolicies(ctx context.Context, username string) ([]string, error) {
return s.CredentialStore.ListAttachedUserPolicies(ctx, username)
}
// SaveConfiguration overrides the embedded CredentialStore.SaveConfiguration
// so bulk identity / group updates also push to running S3 caches. The IAM
// API flow ends each handler with SaveConfiguration after recomputing
// identity.Actions (the legacy authorization field), so reusing PutIdentity
// here is what keeps inline-policy changes visible to S3 servers without
// requiring a restart. We diff against the prior store state so deletions
// also fan out (RemoveIdentity / RemoveGroup); without the diff a postgres
// user who got pruned by SaveConfiguration would linger in the S3 cache.
func (s *PropagatingCredentialStore) SaveConfiguration(ctx context.Context, config *iam_pb.S3ApiConfiguration) error {
priorUsers, priorErr := s.CredentialStore.ListUsers(ctx)
if priorErr != nil {
glog.V(1).Infof("failed to list users before SaveConfiguration; skipping deletion propagation: %v", priorErr)
priorUsers = nil
}
priorGroups, gPriorErr := s.CredentialStore.ListGroups(ctx)
if gPriorErr != nil {
glog.V(1).Infof("failed to list groups before SaveConfiguration; skipping deletion propagation: %v", gPriorErr)
priorGroups = nil
}
if err := s.CredentialStore.SaveConfiguration(ctx, config); err != nil {
return err
}
keptUsers := make(map[string]struct{}, len(config.Identities))
for _, ident := range config.Identities {
if ident != nil {
keptUsers[ident.Name] = struct{}{}
}
}
keptGroups := make(map[string]struct{}, len(config.Groups))
for _, g := range config.Groups {
if g != nil {
keptGroups[g.Name] = struct{}{}
}
}
s.propagateChange(ctx, func(tx context.Context, client s3_pb.SeaweedS3IamCacheClient) error {
for _, ident := range config.Identities {
if ident == nil {
continue
}
if _, err := client.PutIdentity(tx, &iam_pb.PutIdentityRequest{Identity: ident}); err != nil {
return err
}
}
for _, g := range config.Groups {
if g == nil {
continue
}
if _, err := client.PutGroup(tx, &iam_pb.PutGroupRequest{Group: g}); err != nil {
return err
}
}
for _, name := range priorUsers {
if _, kept := keptUsers[name]; kept {
continue
}
if _, err := client.RemoveIdentity(tx, &iam_pb.RemoveIdentityRequest{Username: name}); err != nil {
return err
}
}
for _, name := range priorGroups {
if _, kept := keptGroups[name]; kept {
continue
}
if _, err := client.RemoveGroup(tx, &iam_pb.RemoveGroupRequest{GroupName: name}); err != nil {
return err
}
}
return nil
})
return nil
}
func (s *PropagatingCredentialStore) CreateUser(ctx context.Context, identity *iam_pb.Identity) error {
glog.V(4).Infof("IAM: PropagatingCredentialStore.CreateUser %s", identity.Name)
if err := s.CredentialStore.CreateUser(ctx, identity); err != nil {
return err
}
s.propagateChange(ctx, func(tx context.Context, client s3_pb.SeaweedS3IamCacheClient) error {
_, err := client.PutIdentity(tx, &iam_pb.PutIdentityRequest{Identity: identity})
return err
})
return nil
}
func (s *PropagatingCredentialStore) UpdateUser(ctx context.Context, username string, identity *iam_pb.Identity) error {
glog.V(4).Infof("IAM: PropagatingCredentialStore.UpdateUser %s", username)
if err := s.CredentialStore.UpdateUser(ctx, username, identity); err != nil {
return err
}
s.propagateChange(ctx, func(tx context.Context, client s3_pb.SeaweedS3IamCacheClient) error {
if _, err := client.PutIdentity(tx, &iam_pb.PutIdentityRequest{Identity: identity}); err != nil {
return err
}
if username != identity.Name {
if _, err := client.RemoveIdentity(tx, &iam_pb.RemoveIdentityRequest{Username: username}); err != nil {
return err
}
}
return nil
})
return nil
}
func (s *PropagatingCredentialStore) DeleteUser(ctx context.Context, username string) error {
glog.V(4).Infof("IAM: PropagatingCredentialStore.DeleteUser %s", username)
if err := s.CredentialStore.DeleteUser(ctx, username); err != nil {
return err
}
s.propagateChange(ctx, func(tx context.Context, client s3_pb.SeaweedS3IamCacheClient) error {
_, err := client.RemoveIdentity(tx, &iam_pb.RemoveIdentityRequest{Username: username})
return err
})
return nil
}
func (s *PropagatingCredentialStore) CreateAccessKey(ctx context.Context, username string, credential *iam_pb.Credential) error {
if err := s.CredentialStore.CreateAccessKey(ctx, username, credential); err != nil {
return err
}
// Fetch updated identity to propagate
identity, err := s.CredentialStore.GetUser(ctx, username)
if err != nil {
glog.Warningf("failed to get user %s after creating access key: %v", username, err)
return nil
}
s.propagateChange(ctx, func(tx context.Context, client s3_pb.SeaweedS3IamCacheClient) error {
_, err := client.PutIdentity(tx, &iam_pb.PutIdentityRequest{Identity: identity})
return err
})
return nil
}
func (s *PropagatingCredentialStore) DeleteAccessKey(ctx context.Context, username string, accessKey string) error {
if err := s.CredentialStore.DeleteAccessKey(ctx, username, accessKey); err != nil {
return err
}
// Fetch updated identity to propagate
identity, err := s.CredentialStore.GetUser(ctx, username)
if err != nil {
glog.Warningf("failed to get user %s after deleting access key: %v", username, err)
return nil
}
s.propagateChange(ctx, func(tx context.Context, client s3_pb.SeaweedS3IamCacheClient) error {
_, err := client.PutIdentity(tx, &iam_pb.PutIdentityRequest{Identity: identity})
return err
})
return nil
}
func (s *PropagatingCredentialStore) PutPolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
glog.V(4).Infof("IAM: PropagatingCredentialStore.PutPolicy %s", name)
if err := s.CredentialStore.PutPolicy(ctx, name, document); err != nil {
return err
}
s.propagateChange(ctx, func(tx context.Context, client s3_pb.SeaweedS3IamCacheClient) error {
content, err := json.Marshal(document)
if err != nil {
return err
}
_, err = client.PutPolicy(tx, &iam_pb.PutPolicyRequest{Name: name, Content: string(content)})
return err
})
return nil
}
func (s *PropagatingCredentialStore) DeletePolicy(ctx context.Context, name string) error {
glog.V(4).Infof("IAM: PropagatingCredentialStore.DeletePolicy %s", name)
if err := s.CredentialStore.DeletePolicy(ctx, name); err != nil {
return err
}
s.propagateChange(ctx, func(tx context.Context, client s3_pb.SeaweedS3IamCacheClient) error {
_, err := client.DeletePolicy(tx, &iam_pb.DeletePolicyRequest{Name: name})
return err
})
return nil
}
func (s *PropagatingCredentialStore) ListPolicyNames(ctx context.Context) ([]string, error) {
return s.CredentialStore.ListPolicyNames(ctx)
}
func (s *PropagatingCredentialStore) LoadManagedPolicies(ctx context.Context) ([]*iam_pb.Policy, error) {
if loader, ok := s.CredentialStore.(propagatingManagedPolicyLoader); ok {
return loader.LoadManagedPolicies(ctx)
}
policies, err := s.CredentialStore.GetPolicies(ctx)
if err != nil {
return nil, err
}
managedPolicies := make([]*iam_pb.Policy, 0, len(policies))
for name, policyDocument := range policies {
content, err := json.Marshal(policyDocument)
if err != nil {
return nil, err
}
managedPolicies = append(managedPolicies, &iam_pb.Policy{
Name: name,
Content: string(content),
})
}
return managedPolicies, nil
}
func (s *PropagatingCredentialStore) LoadInlinePolicies(ctx context.Context) (map[string]map[string]policy_engine.PolicyDocument, error) {
if loader, ok := s.CredentialStore.(propagatingInlinePolicyLoader); ok {
return loader.LoadInlinePolicies(ctx)
}
return nil, nil
}
func (s *PropagatingCredentialStore) PutUserInlinePolicy(ctx context.Context, userName, policyName string, document policy_engine.PolicyDocument) error {
if store, ok := s.CredentialStore.(InlinePolicyStore); ok {
return store.PutUserInlinePolicy(ctx, userName, policyName, document)
}
return nil
}
func (s *PropagatingCredentialStore) GetUserInlinePolicy(ctx context.Context, userName, policyName string) (*policy_engine.PolicyDocument, error) {
if store, ok := s.CredentialStore.(InlinePolicyStore); ok {
return store.GetUserInlinePolicy(ctx, userName, policyName)
}
return nil, nil
}
func (s *PropagatingCredentialStore) DeleteUserInlinePolicy(ctx context.Context, userName, policyName string) error {
if store, ok := s.CredentialStore.(InlinePolicyStore); ok {
return store.DeleteUserInlinePolicy(ctx, userName, policyName)
}
return nil
}
func (s *PropagatingCredentialStore) ListUserInlinePolicies(ctx context.Context, userName string) ([]string, error) {
if store, ok := s.CredentialStore.(InlinePolicyStore); ok {
return store.ListUserInlinePolicies(ctx, userName)
}
return nil, nil
}
func (s *PropagatingCredentialStore) PutGroupInlinePolicy(ctx context.Context, groupName, policyName string, document policy_engine.PolicyDocument) error {
if store, ok := s.CredentialStore.(GroupInlinePolicyStore); ok {
return store.PutGroupInlinePolicy(ctx, groupName, policyName, document)
}
return nil
}
func (s *PropagatingCredentialStore) GetGroupInlinePolicy(ctx context.Context, groupName, policyName string) (*policy_engine.PolicyDocument, error) {
if store, ok := s.CredentialStore.(GroupInlinePolicyStore); ok {
return store.GetGroupInlinePolicy(ctx, groupName, policyName)
}
return nil, nil
}
func (s *PropagatingCredentialStore) DeleteGroupInlinePolicy(ctx context.Context, groupName, policyName string) error {
if store, ok := s.CredentialStore.(GroupInlinePolicyStore); ok {
return store.DeleteGroupInlinePolicy(ctx, groupName, policyName)
}
return nil
}
func (s *PropagatingCredentialStore) ListGroupInlinePolicies(ctx context.Context, groupName string) ([]string, error) {
if store, ok := s.CredentialStore.(GroupInlinePolicyStore); ok {
return store.ListGroupInlinePolicies(ctx, groupName)
}
return nil, nil
}
func (s *PropagatingCredentialStore) LoadGroupInlinePolicies(ctx context.Context) (map[string]map[string]policy_engine.PolicyDocument, error) {
if loader, ok := s.CredentialStore.(GroupInlinePoliciesLoader); ok {
return loader.LoadGroupInlinePolicies(ctx)
}
return nil, nil
}
func (s *PropagatingCredentialStore) CreatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
if pm, ok := s.CredentialStore.(PolicyManager); ok {
if err := pm.CreatePolicy(ctx, name, document); err != nil {
return err
}
} else {
if err := s.CredentialStore.PutPolicy(ctx, name, document); err != nil {
return err
}
}
s.propagateChange(ctx, func(tx context.Context, client s3_pb.SeaweedS3IamCacheClient) error {
content, err := json.Marshal(document)
if err != nil {
return err
}
_, err = client.PutPolicy(tx, &iam_pb.PutPolicyRequest{Name: name, Content: string(content)})
return err
})
return nil
}
func (s *PropagatingCredentialStore) UpdatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
if pm, ok := s.CredentialStore.(PolicyManager); ok {
if err := pm.UpdatePolicy(ctx, name, document); err != nil {
return err
}
} else {
if err := s.CredentialStore.PutPolicy(ctx, name, document); err != nil {
return err
}
}
s.propagateChange(ctx, func(tx context.Context, client s3_pb.SeaweedS3IamCacheClient) error {
content, err := json.Marshal(document)
if err != nil {
return err
}
_, err = client.PutPolicy(tx, &iam_pb.PutPolicyRequest{Name: name, Content: string(content)})
return err
})
return nil
}
func (s *PropagatingCredentialStore) CreateServiceAccount(ctx context.Context, sa *iam_pb.ServiceAccount) error {
glog.V(4).Infof("IAM: PropagatingCredentialStore.CreateServiceAccount %s (parent: %s)", sa.Id, sa.ParentUser)
if err := s.CredentialStore.CreateServiceAccount(ctx, sa); err != nil {
return err
}
// Fetch parent identity to propagate
identity, err := s.CredentialStore.GetUser(ctx, sa.ParentUser)
if err != nil {
glog.Warningf("failed to get parent user %s after creating service account: %v", sa.ParentUser, err)
return nil
}
s.propagateChange(ctx, func(tx context.Context, client s3_pb.SeaweedS3IamCacheClient) error {
_, err := client.PutIdentity(tx, &iam_pb.PutIdentityRequest{Identity: identity})
return err
})
return nil
}
func (s *PropagatingCredentialStore) UpdateServiceAccount(ctx context.Context, id string, sa *iam_pb.ServiceAccount) error {
if err := s.CredentialStore.UpdateServiceAccount(ctx, id, sa); err != nil {
return err
}
// Fetch parent identity to propagate
identity, err := s.CredentialStore.GetUser(ctx, sa.ParentUser)
if err != nil {
glog.Warningf("failed to get parent user %s after updating service account: %v", sa.ParentUser, err)
return nil
}
s.propagateChange(ctx, func(tx context.Context, client s3_pb.SeaweedS3IamCacheClient) error {
_, err := client.PutIdentity(tx, &iam_pb.PutIdentityRequest{Identity: identity})
return err
})
return nil
}
func (s *PropagatingCredentialStore) DeleteServiceAccount(ctx context.Context, id string) error {
// Retrieve SA first to get ParentUser
sa, err := s.CredentialStore.GetServiceAccount(ctx, id)
if err != nil {
// If accessing non-existent SA, just proceed to delete (idempotency)
// But we can't propagate to parent...
if err := s.CredentialStore.DeleteServiceAccount(ctx, id); err != nil {
return err
}
return nil
}
if err := s.CredentialStore.DeleteServiceAccount(ctx, id); err != nil {
return err
}
// Fetch parent identity to propagate
identity, err := s.CredentialStore.GetUser(ctx, sa.ParentUser)
if err != nil {
glog.Warningf("failed to get parent user %s after deleting service account: %v", sa.ParentUser, err)
return nil
}
s.propagateChange(ctx, func(tx context.Context, client s3_pb.SeaweedS3IamCacheClient) error {
_, err := client.PutIdentity(tx, &iam_pb.PutIdentityRequest{Identity: identity})
return err
})
return nil
}
func (s *PropagatingCredentialStore) CreateGroup(ctx context.Context, group *iam_pb.Group) error {
if group != nil {
glog.V(4).Infof("IAM: PropagatingCredentialStore.CreateGroup %s", group.Name)
}
if err := s.CredentialStore.CreateGroup(ctx, group); err != nil {
return err
}
s.propagateChange(ctx, func(tx context.Context, client s3_pb.SeaweedS3IamCacheClient) error {
_, err := client.PutGroup(tx, &iam_pb.PutGroupRequest{Group: group})
return err
})
return nil
}
func (s *PropagatingCredentialStore) GetGroup(ctx context.Context, groupName string) (*iam_pb.Group, error) {
return s.CredentialStore.GetGroup(ctx, groupName)
}
func (s *PropagatingCredentialStore) DeleteGroup(ctx context.Context, groupName string) error {
glog.V(4).Infof("IAM: PropagatingCredentialStore.DeleteGroup %s", groupName)
if err := s.CredentialStore.DeleteGroup(ctx, groupName); err != nil {
return err
}
s.propagateChange(ctx, func(tx context.Context, client s3_pb.SeaweedS3IamCacheClient) error {
_, err := client.RemoveGroup(tx, &iam_pb.RemoveGroupRequest{GroupName: groupName})
return err
})
return nil
}
func (s *PropagatingCredentialStore) ListGroups(ctx context.Context) ([]string, error) {
return s.CredentialStore.ListGroups(ctx)
}
func (s *PropagatingCredentialStore) UpdateGroup(ctx context.Context, group *iam_pb.Group) error {
if group != nil {
glog.V(4).Infof("IAM: PropagatingCredentialStore.UpdateGroup %s", group.Name)
}
if err := s.CredentialStore.UpdateGroup(ctx, group); err != nil {
return err
}
s.propagateChange(ctx, func(tx context.Context, client s3_pb.SeaweedS3IamCacheClient) error {
_, err := client.PutGroup(tx, &iam_pb.PutGroupRequest{Group: group})
return err
})
return nil
}