s3api: fix static IAM policy enforcement after reload (#8532)
* s3api: honor attached IAM policies over legacy actions * s3api: hydrate IAM policy docs during config reload * s3api: use policy-aware auth when listing buckets * credential: propagate context through filer_etc policy reads * credential: make legacy policy deletes durable * s3api: exercise managed policy runtime loader * s3api: allow static IAM users without session tokens * iam: deny unmatched attached policies under default allow * iam: load embedded policy files from filer store * s3api: require session tokens for IAM presigning * s3api: sync runtime policies into zero-config IAM * credential: respect context in policy file loads * credential: serialize legacy policy deletes * iam: align filer policy store naming * s3api: use authenticated principals for presigning * iam: deep copy policy conditions * s3api: require request creation in policy tests * filer: keep ReadInsideFiler as the context-aware API * iam: harden filer policy store writes * credential: strengthen legacy policy serialization test * credential: forward runtime policy loaders through wrapper * s3api: harden runtime policy merging * iam: require typed already-exists errors
This commit is contained in:
@@ -1553,6 +1553,165 @@ func (iam *IdentityAccessManagement) GetCredentialManager() *credential.Credenti
|
||||
return iam.credentialManager
|
||||
}
|
||||
|
||||
type managedPolicyLoader interface {
|
||||
LoadManagedPolicies(ctx context.Context) ([]*iam_pb.Policy, error)
|
||||
}
|
||||
|
||||
type inlinePolicyLoader interface {
|
||||
LoadInlinePolicies(ctx context.Context) (map[string]map[string]policy_engine.PolicyDocument, error)
|
||||
}
|
||||
|
||||
func inlinePolicyRuntimeName(userName, policyName string) string {
|
||||
return "__inline_policy__/" + userName + "/" + policyName
|
||||
}
|
||||
|
||||
func mergePoliciesIntoConfiguration(config *iam_pb.S3ApiConfiguration, policies []*iam_pb.Policy) {
|
||||
if len(policies) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
existingPolicies := make(map[string]int, len(config.Policies))
|
||||
for idx, policy := range config.Policies {
|
||||
if policy == nil || policy.Name == "" {
|
||||
continue
|
||||
}
|
||||
existingPolicies[policy.Name] = idx
|
||||
}
|
||||
|
||||
for _, policy := range policies {
|
||||
if policy == nil || policy.Name == "" {
|
||||
continue
|
||||
}
|
||||
policyCopy := &iam_pb.Policy{Name: policy.Name, Content: policy.Content}
|
||||
if existingIdx, found := existingPolicies[policy.Name]; found {
|
||||
config.Policies[existingIdx] = policyCopy
|
||||
continue
|
||||
}
|
||||
|
||||
config.Policies = append(config.Policies, policyCopy)
|
||||
existingPolicies[policy.Name] = len(config.Policies) - 1
|
||||
}
|
||||
}
|
||||
|
||||
func appendUniquePolicyName(policyNames []string, policyName string) []string {
|
||||
for _, existingPolicyName := range policyNames {
|
||||
if existingPolicyName == policyName {
|
||||
return policyNames
|
||||
}
|
||||
}
|
||||
return append(policyNames, policyName)
|
||||
}
|
||||
|
||||
func (iam *IdentityAccessManagement) loadManagedPoliciesForRuntime(ctx context.Context) ([]*iam_pb.Policy, error) {
|
||||
store := iam.credentialManager.GetStore()
|
||||
if store == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if loader, ok := store.(managedPolicyLoader); ok {
|
||||
return loader.LoadManagedPolicies(ctx)
|
||||
}
|
||||
|
||||
policies, err := iam.credentialManager.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, fmt.Errorf("failed to marshal policy %q: %w", name, err)
|
||||
}
|
||||
|
||||
managedPolicies = append(managedPolicies, &iam_pb.Policy{
|
||||
Name: name,
|
||||
Content: string(content),
|
||||
})
|
||||
}
|
||||
|
||||
return managedPolicies, nil
|
||||
}
|
||||
|
||||
func (iam *IdentityAccessManagement) hydrateRuntimePolicies(ctx context.Context, config *iam_pb.S3ApiConfiguration) error {
|
||||
if iam.credentialManager == nil || config == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
managedPolicies, err := iam.loadManagedPoliciesForRuntime(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load managed policies for runtime: %w", err)
|
||||
}
|
||||
mergePoliciesIntoConfiguration(config, managedPolicies)
|
||||
|
||||
store := iam.credentialManager.GetStore()
|
||||
if store == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
inlineLoader, ok := store.(inlinePolicyLoader)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
inlinePoliciesByUser, err := inlineLoader.LoadInlinePolicies(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load inline policies for runtime: %w", err)
|
||||
}
|
||||
|
||||
if len(inlinePoliciesByUser) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
identityByName := make(map[string]*iam_pb.Identity, len(config.Identities))
|
||||
for _, identity := range config.Identities {
|
||||
identityByName[identity.Name] = identity
|
||||
}
|
||||
|
||||
inlinePolicies := make([]*iam_pb.Policy, 0)
|
||||
for userName, userPolicies := range inlinePoliciesByUser {
|
||||
identity, found := identityByName[userName]
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
|
||||
for policyName, policyDocument := range userPolicies {
|
||||
content, err := json.Marshal(policyDocument)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal inline policy %q for user %q: %w", policyName, userName, err)
|
||||
}
|
||||
|
||||
runtimePolicyName := inlinePolicyRuntimeName(userName, policyName)
|
||||
inlinePolicies = append(inlinePolicies, &iam_pb.Policy{
|
||||
Name: runtimePolicyName,
|
||||
Content: string(content),
|
||||
})
|
||||
identity.PolicyNames = appendUniquePolicyName(identity.PolicyNames, runtimePolicyName)
|
||||
}
|
||||
}
|
||||
|
||||
mergePoliciesIntoConfiguration(config, inlinePolicies)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (iam *IdentityAccessManagement) syncRuntimePoliciesToIAMManager(ctx context.Context, policies []*iam_pb.Policy) error {
|
||||
if iam == nil || iam.iamIntegration == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
provider, ok := iam.iamIntegration.(IAMManagerProvider)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
manager := provider.GetIAMManager()
|
||||
if manager == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return manager.SyncRuntimePolicies(ctx, policies)
|
||||
}
|
||||
|
||||
// LoadS3ApiConfigurationFromCredentialManager loads configuration using the credential manager
|
||||
func (iam *IdentityAccessManagement) LoadS3ApiConfigurationFromCredentialManager() error {
|
||||
glog.V(1).Infof("Loading S3 API configuration from credential manager")
|
||||
@@ -1566,6 +1725,15 @@ func (iam *IdentityAccessManagement) LoadS3ApiConfigurationFromCredentialManager
|
||||
glog.V(2).Infof("Credential manager returned %d identities and %d accounts",
|
||||
len(s3ApiConfiguration.Identities), len(s3ApiConfiguration.Accounts))
|
||||
|
||||
if err := iam.hydrateRuntimePolicies(context.Background(), s3ApiConfiguration); err != nil {
|
||||
glog.Errorf("Failed to hydrate runtime IAM policies: %v", err)
|
||||
return err
|
||||
}
|
||||
if err := iam.syncRuntimePoliciesToIAMManager(context.Background(), s3ApiConfiguration.Policies); err != nil {
|
||||
glog.Errorf("Failed to sync runtime IAM policies to advanced IAM manager: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := iam.loadS3ApiConfiguration(s3ApiConfiguration); err != nil {
|
||||
glog.Errorf("Failed to load S3 API configuration: %v", err)
|
||||
return err
|
||||
@@ -1726,11 +1894,23 @@ func (iam *IdentityAccessManagement) VerifyActionPermission(r *http.Request, ide
|
||||
hasSessionToken := r.Header.Get("X-SeaweedFS-Session-Token") != "" ||
|
||||
r.Header.Get("X-Amz-Security-Token") != "" ||
|
||||
r.URL.Query().Get("X-Amz-Security-Token") != ""
|
||||
hasAttachedPolicies := len(identity.PolicyNames) > 0
|
||||
|
||||
if (len(identity.Actions) == 0 || hasSessionToken) && iam.iamIntegration != nil {
|
||||
if (len(identity.Actions) == 0 || hasSessionToken || hasAttachedPolicies) && iam.iamIntegration != nil {
|
||||
return iam.authorizeWithIAM(r, identity, action, bucket, object)
|
||||
}
|
||||
|
||||
// Attached IAM policies are authoritative for IAM users. The legacy Actions
|
||||
// field is a lossy projection that cannot represent deny statements,
|
||||
// conditions, or fine-grained action differences such as PutObject vs
|
||||
// DeleteObject.
|
||||
if hasAttachedPolicies {
|
||||
if iam.evaluateIAMPolicies(r, identity, action, bucket, object) {
|
||||
return s3err.ErrNone
|
||||
}
|
||||
return s3err.ErrAccessDenied
|
||||
}
|
||||
|
||||
// Traditional actions-based authorization from static S3 config.
|
||||
if len(identity.Actions) > 0 {
|
||||
if !identity.CanDo(action, bucket, object) {
|
||||
@@ -1739,14 +1919,6 @@ func (iam *IdentityAccessManagement) VerifyActionPermission(r *http.Request, ide
|
||||
return s3err.ErrNone
|
||||
}
|
||||
|
||||
// IAM policy fallback for identities with attached policies but without IAM integration.
|
||||
if len(identity.PolicyNames) > 0 {
|
||||
if iam.evaluateIAMPolicies(r, identity, action, bucket, object) {
|
||||
return s3err.ErrNone
|
||||
}
|
||||
return s3err.ErrAccessDenied
|
||||
}
|
||||
|
||||
return s3err.ErrAccessDenied
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package s3api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -10,18 +11,71 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/credential"
|
||||
"github.com/seaweedfs/seaweedfs/weed/credential/memory"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine"
|
||||
. "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util/wildcard"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
|
||||
jsonpb "google.golang.org/protobuf/encoding/protojson"
|
||||
|
||||
_ "github.com/seaweedfs/seaweedfs/weed/credential/filer_etc"
|
||||
_ "github.com/seaweedfs/seaweedfs/weed/credential/memory"
|
||||
)
|
||||
|
||||
type loadConfigurationDropsPoliciesStore struct {
|
||||
*memory.MemoryStore
|
||||
loadManagedPoliciesCalled bool
|
||||
}
|
||||
|
||||
func (store *loadConfigurationDropsPoliciesStore) LoadConfiguration(ctx context.Context) (*iam_pb.S3ApiConfiguration, error) {
|
||||
config, err := store.MemoryStore.LoadConfiguration(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stripped := *config
|
||||
stripped.Policies = nil
|
||||
return &stripped, nil
|
||||
}
|
||||
|
||||
func (store *loadConfigurationDropsPoliciesStore) LoadManagedPolicies(ctx context.Context) ([]*iam_pb.Policy, error) {
|
||||
store.loadManagedPoliciesCalled = true
|
||||
|
||||
config, err := store.MemoryStore.LoadConfiguration(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
policies := make([]*iam_pb.Policy, 0, len(config.Policies))
|
||||
for _, policy := range config.Policies {
|
||||
policies = append(policies, &iam_pb.Policy{
|
||||
Name: policy.Name,
|
||||
Content: policy.Content,
|
||||
})
|
||||
}
|
||||
|
||||
return policies, nil
|
||||
}
|
||||
|
||||
type inlinePolicyRuntimeStore struct {
|
||||
*memory.MemoryStore
|
||||
inlinePolicies map[string]map[string]policy_engine.PolicyDocument
|
||||
}
|
||||
|
||||
func (store *inlinePolicyRuntimeStore) LoadInlinePolicies(ctx context.Context) (map[string]map[string]policy_engine.PolicyDocument, error) {
|
||||
_ = ctx
|
||||
return store.inlinePolicies, nil
|
||||
}
|
||||
|
||||
func newPolicyAuthRequest(t *testing.T, method string) *http.Request {
|
||||
t.Helper()
|
||||
req, err := http.NewRequest(method, "http://s3.amazonaws.com/test-bucket/test-object", nil)
|
||||
require.NoError(t, err)
|
||||
return req
|
||||
}
|
||||
|
||||
func TestIdentityListFileFormat(t *testing.T) {
|
||||
|
||||
s3ApiConfiguration := &iam_pb.S3ApiConfiguration{}
|
||||
@@ -374,6 +428,25 @@ func TestVerifyActionPermissionPolicyFallback(t *testing.T) {
|
||||
assert.Equal(t, s3err.ErrNone, errCode)
|
||||
})
|
||||
|
||||
t.Run("attached policies override coarse legacy actions", func(t *testing.T) {
|
||||
iam := &IdentityAccessManagement{}
|
||||
err := iam.PutPolicy("putOnly", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutObject","Resource":"arn:aws:s3:::test-bucket/*"}]}`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
identity := &Identity{
|
||||
Name: "policy-user",
|
||||
Account: &AccountAdmin,
|
||||
Actions: []Action{"Write:test-bucket"},
|
||||
PolicyNames: []string{"putOnly"},
|
||||
}
|
||||
|
||||
putErrCode := iam.VerifyActionPermission(buildRequest(t, http.MethodPut), identity, Action(ACTION_WRITE), "test-bucket", "test-object")
|
||||
assert.Equal(t, s3err.ErrNone, putErrCode)
|
||||
|
||||
deleteErrCode := iam.VerifyActionPermission(buildRequest(t, http.MethodDelete), identity, Action(ACTION_WRITE), "test-bucket", "test-object")
|
||||
assert.Equal(t, s3err.ErrAccessDenied, deleteErrCode)
|
||||
})
|
||||
|
||||
t.Run("valid policy updated to invalid denies access", func(t *testing.T) {
|
||||
iam := &IdentityAccessManagement{}
|
||||
err := iam.PutPolicy("myPolicy", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::test-bucket/*"}]}`)
|
||||
@@ -409,6 +482,288 @@ func TestVerifyActionPermissionPolicyFallback(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoadS3ApiConfigurationFromCredentialManagerHydratesManagedPolicies(t *testing.T) {
|
||||
baseStore := &memory.MemoryStore{}
|
||||
assert.NoError(t, baseStore.Initialize(nil, ""))
|
||||
|
||||
store := &loadConfigurationDropsPoliciesStore{MemoryStore: baseStore}
|
||||
cm := &credential.CredentialManager{Store: store}
|
||||
|
||||
config := &iam_pb.S3ApiConfiguration{
|
||||
Identities: []*iam_pb.Identity{
|
||||
{
|
||||
Name: "managed-user",
|
||||
PolicyNames: []string{"managedGet"},
|
||||
Credentials: []*iam_pb.Credential{
|
||||
{AccessKey: "AKIAMANAGED000001", SecretKey: "managed-secret"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Policies: []*iam_pb.Policy{
|
||||
{
|
||||
Name: "managedGet",
|
||||
Content: `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::test-bucket/*"}]}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.NoError(t, cm.SaveConfiguration(context.Background(), config))
|
||||
|
||||
iam := &IdentityAccessManagement{credentialManager: cm}
|
||||
assert.NoError(t, iam.LoadS3ApiConfigurationFromCredentialManager())
|
||||
assert.True(t, store.loadManagedPoliciesCalled)
|
||||
|
||||
identity := iam.lookupByIdentityName("managed-user")
|
||||
if !assert.NotNil(t, identity) {
|
||||
return
|
||||
}
|
||||
|
||||
errCode := iam.VerifyActionPermission(newPolicyAuthRequest(t, http.MethodGet), identity, Action(ACTION_READ), "test-bucket", "test-object")
|
||||
assert.Equal(t, s3err.ErrNone, errCode)
|
||||
}
|
||||
|
||||
func TestLoadS3ApiConfigurationFromCredentialManagerHydratesManagedPoliciesThroughPropagatingStore(t *testing.T) {
|
||||
baseStore := &memory.MemoryStore{}
|
||||
assert.NoError(t, baseStore.Initialize(nil, ""))
|
||||
|
||||
upstream := &loadConfigurationDropsPoliciesStore{MemoryStore: baseStore}
|
||||
wrappedStore := credential.NewPropagatingCredentialStore(upstream, nil, nil)
|
||||
cm := &credential.CredentialManager{Store: wrappedStore}
|
||||
|
||||
config := &iam_pb.S3ApiConfiguration{
|
||||
Identities: []*iam_pb.Identity{
|
||||
{
|
||||
Name: "managed-user",
|
||||
PolicyNames: []string{"managedGet"},
|
||||
Credentials: []*iam_pb.Credential{
|
||||
{AccessKey: "AKIAMANAGED000010", SecretKey: "managed-secret"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Policies: []*iam_pb.Policy{
|
||||
{
|
||||
Name: "managedGet",
|
||||
Content: `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::test-bucket/*"}]}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.NoError(t, cm.SaveConfiguration(context.Background(), config))
|
||||
|
||||
iam := &IdentityAccessManagement{credentialManager: cm}
|
||||
assert.NoError(t, iam.LoadS3ApiConfigurationFromCredentialManager())
|
||||
assert.True(t, upstream.loadManagedPoliciesCalled)
|
||||
|
||||
identity := iam.lookupByIdentityName("managed-user")
|
||||
if !assert.NotNil(t, identity) {
|
||||
return
|
||||
}
|
||||
|
||||
errCode := iam.VerifyActionPermission(newPolicyAuthRequest(t, http.MethodGet), identity, Action(ACTION_READ), "test-bucket", "test-object")
|
||||
assert.Equal(t, s3err.ErrNone, errCode)
|
||||
}
|
||||
|
||||
func TestLoadS3ApiConfigurationFromCredentialManagerSyncsPoliciesToIAMManager(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
baseStore := &memory.MemoryStore{}
|
||||
assert.NoError(t, baseStore.Initialize(nil, ""))
|
||||
|
||||
cm := &credential.CredentialManager{Store: baseStore}
|
||||
config := &iam_pb.S3ApiConfiguration{
|
||||
Identities: []*iam_pb.Identity{
|
||||
{
|
||||
Name: "managed-user",
|
||||
PolicyNames: []string{"managedPut"},
|
||||
Credentials: []*iam_pb.Credential{
|
||||
{AccessKey: "AKIAMANAGED000002", SecretKey: "managed-secret"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Policies: []*iam_pb.Policy{
|
||||
{
|
||||
Name: "managedPut",
|
||||
Content: `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:PutObject","s3:ListBucket"],"Resource":["arn:aws:s3:::cli-allowed-bucket","arn:aws:s3:::cli-allowed-bucket/*"]}]}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.NoError(t, cm.SaveConfiguration(ctx, config))
|
||||
|
||||
iamManager, err := loadIAMManagerFromConfig("", func() string { return "localhost:8888" }, func() string {
|
||||
return "fallback-key-for-zero-config"
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
iamManager.SetUserStore(cm)
|
||||
|
||||
iam := &IdentityAccessManagement{credentialManager: cm}
|
||||
iam.SetIAMIntegration(NewS3IAMIntegration(iamManager, ""))
|
||||
|
||||
assert.NoError(t, iam.LoadS3ApiConfigurationFromCredentialManager())
|
||||
|
||||
identity := iam.lookupByIdentityName("managed-user")
|
||||
if !assert.NotNil(t, identity) {
|
||||
return
|
||||
}
|
||||
|
||||
allowedErrCode := iam.VerifyActionPermission(newPolicyAuthRequest(t, http.MethodPut), identity, Action(ACTION_WRITE), "cli-allowed-bucket", "test-object")
|
||||
assert.Equal(t, s3err.ErrNone, allowedErrCode)
|
||||
|
||||
forbiddenErrCode := iam.VerifyActionPermission(newPolicyAuthRequest(t, http.MethodPut), identity, Action(ACTION_WRITE), "cli-forbidden-bucket", "test-object")
|
||||
assert.Equal(t, s3err.ErrAccessDenied, forbiddenErrCode)
|
||||
}
|
||||
|
||||
func TestLoadS3ApiConfigurationFromCredentialManagerHydratesInlinePolicies(t *testing.T) {
|
||||
baseStore := &memory.MemoryStore{}
|
||||
assert.NoError(t, baseStore.Initialize(nil, ""))
|
||||
|
||||
inlinePolicy := policy_engine.PolicyDocument{
|
||||
Version: policy_engine.PolicyVersion2012_10_17,
|
||||
Statement: []policy_engine.PolicyStatement{
|
||||
{
|
||||
Effect: policy_engine.PolicyEffectAllow,
|
||||
Action: policy_engine.NewStringOrStringSlice("s3:PutObject"),
|
||||
Resource: policy_engine.NewStringOrStringSlice("arn:aws:s3:::test-bucket/*"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
store := &inlinePolicyRuntimeStore{
|
||||
MemoryStore: baseStore,
|
||||
inlinePolicies: map[string]map[string]policy_engine.PolicyDocument{
|
||||
"inline-user": {
|
||||
"PutOnly": inlinePolicy,
|
||||
},
|
||||
},
|
||||
}
|
||||
cm := &credential.CredentialManager{Store: store}
|
||||
|
||||
config := &iam_pb.S3ApiConfiguration{
|
||||
Identities: []*iam_pb.Identity{
|
||||
{
|
||||
Name: "inline-user",
|
||||
Actions: []string{"Write:test-bucket"},
|
||||
Credentials: []*iam_pb.Credential{
|
||||
{AccessKey: "AKIAINLINE0000001", SecretKey: "inline-secret"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.NoError(t, cm.SaveConfiguration(context.Background(), config))
|
||||
|
||||
iam := &IdentityAccessManagement{credentialManager: cm}
|
||||
assert.NoError(t, iam.LoadS3ApiConfigurationFromCredentialManager())
|
||||
|
||||
identity := iam.lookupByIdentityName("inline-user")
|
||||
if !assert.NotNil(t, identity) {
|
||||
return
|
||||
}
|
||||
assert.Contains(t, identity.PolicyNames, inlinePolicyRuntimeName("inline-user", "PutOnly"))
|
||||
|
||||
putErrCode := iam.VerifyActionPermission(newPolicyAuthRequest(t, http.MethodPut), identity, Action(ACTION_WRITE), "test-bucket", "test-object")
|
||||
assert.Equal(t, s3err.ErrNone, putErrCode)
|
||||
|
||||
deleteErrCode := iam.VerifyActionPermission(newPolicyAuthRequest(t, http.MethodDelete), identity, Action(ACTION_WRITE), "test-bucket", "test-object")
|
||||
assert.Equal(t, s3err.ErrAccessDenied, deleteErrCode)
|
||||
}
|
||||
|
||||
func TestLoadS3ApiConfigurationFromCredentialManagerHydratesInlinePoliciesThroughPropagatingStore(t *testing.T) {
|
||||
baseStore := &memory.MemoryStore{}
|
||||
assert.NoError(t, baseStore.Initialize(nil, ""))
|
||||
|
||||
inlinePolicy := policy_engine.PolicyDocument{
|
||||
Version: policy_engine.PolicyVersion2012_10_17,
|
||||
Statement: []policy_engine.PolicyStatement{
|
||||
{
|
||||
Effect: policy_engine.PolicyEffectAllow,
|
||||
Action: policy_engine.NewStringOrStringSlice("s3:PutObject"),
|
||||
Resource: policy_engine.NewStringOrStringSlice("arn:aws:s3:::test-bucket/*"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
upstream := &inlinePolicyRuntimeStore{
|
||||
MemoryStore: baseStore,
|
||||
inlinePolicies: map[string]map[string]policy_engine.PolicyDocument{
|
||||
"inline-user": {
|
||||
"PutOnly": inlinePolicy,
|
||||
},
|
||||
},
|
||||
}
|
||||
wrappedStore := credential.NewPropagatingCredentialStore(upstream, nil, nil)
|
||||
cm := &credential.CredentialManager{Store: wrappedStore}
|
||||
|
||||
config := &iam_pb.S3ApiConfiguration{
|
||||
Identities: []*iam_pb.Identity{
|
||||
{
|
||||
Name: "inline-user",
|
||||
Actions: []string{"Write:test-bucket"},
|
||||
Credentials: []*iam_pb.Credential{
|
||||
{AccessKey: "AKIAINLINE0000010", SecretKey: "inline-secret"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.NoError(t, cm.SaveConfiguration(context.Background(), config))
|
||||
|
||||
iam := &IdentityAccessManagement{credentialManager: cm}
|
||||
assert.NoError(t, iam.LoadS3ApiConfigurationFromCredentialManager())
|
||||
|
||||
identity := iam.lookupByIdentityName("inline-user")
|
||||
if !assert.NotNil(t, identity) {
|
||||
return
|
||||
}
|
||||
assert.Contains(t, identity.PolicyNames, inlinePolicyRuntimeName("inline-user", "PutOnly"))
|
||||
|
||||
putErrCode := iam.VerifyActionPermission(newPolicyAuthRequest(t, http.MethodPut), identity, Action(ACTION_WRITE), "test-bucket", "test-object")
|
||||
assert.Equal(t, s3err.ErrNone, putErrCode)
|
||||
|
||||
deleteErrCode := iam.VerifyActionPermission(newPolicyAuthRequest(t, http.MethodDelete), identity, Action(ACTION_WRITE), "test-bucket", "test-object")
|
||||
assert.Equal(t, s3err.ErrAccessDenied, deleteErrCode)
|
||||
}
|
||||
|
||||
func TestLoadConfigurationDropsPoliciesStoreDoesNotMutateSourceConfig(t *testing.T) {
|
||||
baseStore := &memory.MemoryStore{}
|
||||
require.NoError(t, baseStore.Initialize(nil, ""))
|
||||
|
||||
config := &iam_pb.S3ApiConfiguration{
|
||||
Policies: []*iam_pb.Policy{
|
||||
{Name: "managedGet", Content: `{"Version":"2012-10-17","Statement":[]}`},
|
||||
},
|
||||
}
|
||||
require.NoError(t, baseStore.SaveConfiguration(context.Background(), config))
|
||||
|
||||
store := &loadConfigurationDropsPoliciesStore{MemoryStore: baseStore}
|
||||
|
||||
stripped, err := store.LoadConfiguration(context.Background())
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, stripped.Policies)
|
||||
|
||||
source, err := baseStore.LoadConfiguration(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, source.Policies, 1)
|
||||
assert.Equal(t, "managedGet", source.Policies[0].Name)
|
||||
}
|
||||
|
||||
func TestMergePoliciesIntoConfigurationSkipsNilPolicies(t *testing.T) {
|
||||
config := &iam_pb.S3ApiConfiguration{
|
||||
Policies: []*iam_pb.Policy{
|
||||
nil,
|
||||
{Name: "existing", Content: "old"},
|
||||
},
|
||||
}
|
||||
|
||||
mergePoliciesIntoConfiguration(config, []*iam_pb.Policy{
|
||||
nil,
|
||||
{Name: "", Content: "ignored"},
|
||||
{Name: "existing", Content: "updated"},
|
||||
{Name: "new", Content: "created"},
|
||||
})
|
||||
|
||||
require.Len(t, config.Policies, 3)
|
||||
assert.Nil(t, config.Policies[0])
|
||||
assert.Equal(t, "existing", config.Policies[1].Name)
|
||||
assert.Equal(t, "updated", config.Policies[1].Content)
|
||||
assert.Equal(t, "new", config.Policies[2].Name)
|
||||
assert.Equal(t, "created", config.Policies[2].Content)
|
||||
}
|
||||
|
||||
type LoadS3ApiConfigurationTestCase struct {
|
||||
pbAccount *iam_pb.Account
|
||||
pbIdent *iam_pb.Identity
|
||||
|
||||
@@ -248,7 +248,7 @@ func (s3iam *S3IAMIntegration) AuthorizeAction(ctx context.Context, identity *IA
|
||||
return s3err.ErrNone // Fallback to existing authorization
|
||||
}
|
||||
|
||||
if identity.SessionToken == "" {
|
||||
if identity == nil || identity.Principal == "" {
|
||||
return s3err.ErrAccessDenied
|
||||
}
|
||||
|
||||
@@ -292,9 +292,12 @@ func (s3iam *S3IAMIntegration) AuthorizeAction(ctx context.Context, identity *IA
|
||||
|
||||
// Create action request
|
||||
actionRequest := &integration.ActionRequest{
|
||||
Principal: identity.Principal,
|
||||
Action: specificAction,
|
||||
Resource: resourceArn,
|
||||
Principal: identity.Principal,
|
||||
Action: specificAction,
|
||||
Resource: resourceArn,
|
||||
// Static SigV4 IAM users do not carry a session token. IAMManager
|
||||
// evaluates their attached policies directly and only validates STS/OIDC
|
||||
// session state when a token is actually present.
|
||||
SessionToken: identity.SessionToken,
|
||||
RequestContext: requestContext,
|
||||
PolicyNames: identity.PolicyNames,
|
||||
|
||||
@@ -13,16 +13,15 @@ import (
|
||||
"github.com/seaweedfs/seaweedfs/weed/iam/sts"
|
||||
"github.com/seaweedfs/seaweedfs/weed/iam/utils"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestS3IAMMiddleware tests the basic S3 IAM middleware functionality
|
||||
func TestS3IAMMiddleware(t *testing.T) {
|
||||
// Create IAM manager
|
||||
iamManager := integration.NewIAMManager()
|
||||
func newTestS3IAMManagerWithDefaultEffect(t *testing.T, defaultEffect string) *integration.IAMManager {
|
||||
t.Helper()
|
||||
|
||||
// Initialize with test configuration
|
||||
iamManager := integration.NewIAMManager()
|
||||
config := &integration.IAMConfig{
|
||||
STS: &sts.STSConfig{
|
||||
TokenDuration: sts.FlexibleDuration{Duration: time.Hour},
|
||||
@@ -31,7 +30,7 @@ func TestS3IAMMiddleware(t *testing.T) {
|
||||
SigningKey: []byte("test-signing-key-32-characters-long"),
|
||||
},
|
||||
Policy: &policy.PolicyEngineConfig{
|
||||
DefaultEffect: "Deny",
|
||||
DefaultEffect: defaultEffect,
|
||||
StoreType: "memory",
|
||||
},
|
||||
Roles: &integration.RoleStoreConfig{
|
||||
@@ -40,10 +39,22 @@ func TestS3IAMMiddleware(t *testing.T) {
|
||||
}
|
||||
|
||||
err := iamManager.Initialize(config, func() string {
|
||||
return "localhost:8888" // Mock filer address for testing
|
||||
return "localhost:8888"
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return iamManager
|
||||
}
|
||||
|
||||
func newTestS3IAMManager(t *testing.T) *integration.IAMManager {
|
||||
t.Helper()
|
||||
return newTestS3IAMManagerWithDefaultEffect(t, "Deny")
|
||||
}
|
||||
|
||||
// TestS3IAMMiddleware tests the basic S3 IAM middleware functionality
|
||||
func TestS3IAMMiddleware(t *testing.T) {
|
||||
iamManager := newTestS3IAMManager(t)
|
||||
|
||||
// Create S3 IAM integration
|
||||
s3IAMIntegration := NewS3IAMIntegration(iamManager, "localhost:8888")
|
||||
|
||||
@@ -52,6 +63,74 @@ func TestS3IAMMiddleware(t *testing.T) {
|
||||
assert.True(t, s3IAMIntegration.enabled)
|
||||
}
|
||||
|
||||
func TestS3IAMMiddlewareStaticV4ManagedPolicies(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
iamManager := newTestS3IAMManager(t)
|
||||
|
||||
allowPolicy := &policy.PolicyDocument{
|
||||
Version: "2012-10-17",
|
||||
Statement: []policy.Statement{
|
||||
{
|
||||
Effect: "Allow",
|
||||
Action: policy.StringList{"s3:PutObject", "s3:ListBucket"},
|
||||
Resource: policy.StringList{"arn:aws:s3:::cli-allowed-bucket", "arn:aws:s3:::cli-allowed-bucket/*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
require.NoError(t, iamManager.CreatePolicy(ctx, "localhost:8888", "cli-bucket-access-policy", allowPolicy))
|
||||
|
||||
s3IAMIntegration := NewS3IAMIntegration(iamManager, "localhost:8888")
|
||||
identity := &IAMIdentity{
|
||||
Name: "cli-test-user",
|
||||
Principal: "arn:aws:iam::000000000000:user/cli-test-user",
|
||||
PolicyNames: []string{"cli-bucket-access-policy"},
|
||||
}
|
||||
|
||||
putReq := httptest.NewRequest(http.MethodPut, "http://example.com/cli-allowed-bucket/test-file.txt", http.NoBody)
|
||||
putErrCode := s3IAMIntegration.AuthorizeAction(ctx, identity, s3_constants.ACTION_WRITE, "cli-allowed-bucket", "test-file.txt", putReq)
|
||||
assert.Equal(t, s3err.ErrNone, putErrCode)
|
||||
|
||||
listReq := httptest.NewRequest(http.MethodGet, "http://example.com/cli-allowed-bucket/", http.NoBody)
|
||||
listErrCode := s3IAMIntegration.AuthorizeAction(ctx, identity, s3_constants.ACTION_LIST, "cli-allowed-bucket", "", listReq)
|
||||
assert.Equal(t, s3err.ErrNone, listErrCode)
|
||||
}
|
||||
|
||||
func TestS3IAMMiddlewareAttachedPoliciesRestrictDefaultAllow(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
iamManager := newTestS3IAMManagerWithDefaultEffect(t, "Allow")
|
||||
|
||||
allowPolicy := &policy.PolicyDocument{
|
||||
Version: "2012-10-17",
|
||||
Statement: []policy.Statement{
|
||||
{
|
||||
Effect: "Allow",
|
||||
Action: policy.StringList{"s3:PutObject", "s3:ListBucket"},
|
||||
Resource: policy.StringList{"arn:aws:s3:::cli-allowed-bucket", "arn:aws:s3:::cli-allowed-bucket/*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
require.NoError(t, iamManager.CreatePolicy(ctx, "localhost:8888", "cli-bucket-access-policy", allowPolicy))
|
||||
|
||||
s3IAMIntegration := NewS3IAMIntegration(iamManager, "localhost:8888")
|
||||
identity := &IAMIdentity{
|
||||
Name: "cli-test-user",
|
||||
Principal: "arn:aws:iam::000000000000:user/cli-test-user",
|
||||
PolicyNames: []string{"cli-bucket-access-policy"},
|
||||
}
|
||||
|
||||
allowedReq := httptest.NewRequest(http.MethodPut, "http://example.com/cli-allowed-bucket/test-file.txt", http.NoBody)
|
||||
allowedErrCode := s3IAMIntegration.AuthorizeAction(ctx, identity, s3_constants.ACTION_WRITE, "cli-allowed-bucket", "test-file.txt", allowedReq)
|
||||
assert.Equal(t, s3err.ErrNone, allowedErrCode)
|
||||
|
||||
forbiddenReq := httptest.NewRequest(http.MethodPut, "http://example.com/cli-forbidden-bucket/forbidden-file.txt", http.NoBody)
|
||||
forbiddenErrCode := s3IAMIntegration.AuthorizeAction(ctx, identity, s3_constants.ACTION_WRITE, "cli-forbidden-bucket", "forbidden-file.txt", forbiddenReq)
|
||||
assert.Equal(t, s3err.ErrAccessDenied, forbiddenErrCode)
|
||||
|
||||
forbiddenListReq := httptest.NewRequest(http.MethodGet, "http://example.com/cli-forbidden-bucket/", http.NoBody)
|
||||
forbiddenListErrCode := s3IAMIntegration.AuthorizeAction(ctx, identity, s3_constants.ACTION_LIST, "cli-forbidden-bucket", "", forbiddenListReq)
|
||||
assert.Equal(t, s3err.ErrAccessDenied, forbiddenListErrCode)
|
||||
}
|
||||
|
||||
// TestS3IAMMiddlewareJWTAuth tests JWT authentication
|
||||
func TestS3IAMMiddlewareJWTAuth(t *testing.T) {
|
||||
// Skip for now since it requires full setup
|
||||
|
||||
@@ -101,21 +101,10 @@ func (pm *S3PresignedURLManager) GeneratePresignedURLWithIAM(ctx context.Context
|
||||
if pm.s3iam == nil || !pm.s3iam.enabled {
|
||||
return nil, fmt.Errorf("IAM integration not enabled")
|
||||
}
|
||||
|
||||
// Validate session token and get identity
|
||||
// Use a proper ARN format for the principal
|
||||
principalArn := fmt.Sprintf("arn:aws:sts::assumed-role/PresignedUser/presigned-session")
|
||||
iamIdentity := &IAMIdentity{
|
||||
SessionToken: req.SessionToken,
|
||||
Principal: principalArn,
|
||||
Name: "presigned-user",
|
||||
Account: &AccountAdmin,
|
||||
if req == nil || strings.TrimSpace(req.SessionToken) == "" {
|
||||
return nil, fmt.Errorf("IAM authorization failed: session token is required")
|
||||
}
|
||||
|
||||
// Determine S3 action from method
|
||||
action := determineS3ActionFromMethodAndPath(req.Method, req.Bucket, req.ObjectKey)
|
||||
|
||||
// Check IAM permissions before generating URL
|
||||
authRequest := &http.Request{
|
||||
Method: req.Method,
|
||||
URL: &url.URL{Path: "/" + req.Bucket + "/" + req.ObjectKey},
|
||||
@@ -124,7 +113,16 @@ func (pm *S3PresignedURLManager) GeneratePresignedURLWithIAM(ctx context.Context
|
||||
authRequest.Header.Set("Authorization", "Bearer "+req.SessionToken)
|
||||
authRequest = authRequest.WithContext(ctx)
|
||||
|
||||
errCode := pm.s3iam.AuthorizeAction(ctx, iamIdentity, action, req.Bucket, req.ObjectKey, authRequest)
|
||||
iamIdentity, errCode := pm.s3iam.AuthenticateJWT(ctx, authRequest)
|
||||
if errCode != s3err.ErrNone {
|
||||
return nil, fmt.Errorf("IAM authorization failed: invalid session token")
|
||||
}
|
||||
|
||||
// Determine S3 action from method
|
||||
action := determineS3ActionFromMethodAndPath(req.Method, req.Bucket, req.ObjectKey)
|
||||
|
||||
// Check IAM permissions before generating URL
|
||||
errCode = pm.s3iam.AuthorizeAction(ctx, iamIdentity, action, req.Bucket, req.ObjectKey, authRequest)
|
||||
if errCode != s3err.ErrNone {
|
||||
return nil, fmt.Errorf("IAM authorization failed: user does not have permission for action %s on resource %s/%s", action, req.Bucket, req.ObjectKey)
|
||||
}
|
||||
|
||||
@@ -220,6 +220,35 @@ func TestPresignedURLGeneration(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPresignedURLGenerationUsesAuthenticatedPrincipal(t *testing.T) {
|
||||
iamManager := setupTestIAMManagerForPresigned(t)
|
||||
s3iam := NewS3IAMIntegration(iamManager, "localhost:8888")
|
||||
s3iam.enabled = true
|
||||
presignedManager := NewS3PresignedURLManager(s3iam)
|
||||
|
||||
ctx := context.Background()
|
||||
setupTestRolesForPresigned(ctx, iamManager)
|
||||
|
||||
validJWTToken := createTestJWTPresigned(t, "https://test-issuer.com", "test-user-123", "test-signing-key")
|
||||
|
||||
response, err := iamManager.AssumeRoleWithWebIdentity(ctx, &sts.AssumeRoleWithWebIdentityRequest{
|
||||
RoleArn: "arn:aws:iam::role/S3ReadOnlyRole",
|
||||
WebIdentityToken: validJWTToken,
|
||||
RoleSessionName: "presigned-read-only-session",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = presignedManager.GeneratePresignedURLWithIAM(ctx, &PresignedURLRequest{
|
||||
Method: "PUT",
|
||||
Bucket: "test-bucket",
|
||||
ObjectKey: "new-file.txt",
|
||||
Expiration: time.Hour,
|
||||
SessionToken: response.Credentials.SessionToken,
|
||||
}, "http://localhost:8333")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "IAM authorization failed")
|
||||
}
|
||||
|
||||
// TestPresignedURLExpiration tests URL expiration validation
|
||||
func TestPresignedURLExpiration(t *testing.T) {
|
||||
tests := []struct {
|
||||
|
||||
@@ -88,20 +88,7 @@ func (s3a *S3ApiServer) ListBucketsHandler(w http.ResponseWriter, r *http.Reques
|
||||
|
||||
// Skip permission check if user is already the owner (optimization)
|
||||
if !isOwner {
|
||||
hasPermission := false
|
||||
// Check permissions for each bucket
|
||||
// For JWT-authenticated users, use IAM authorization
|
||||
sessionToken := r.Header.Get("X-SeaweedFS-Session-Token")
|
||||
if s3a.iam.iamIntegration != nil && sessionToken != "" {
|
||||
// Use IAM authorization for JWT users
|
||||
errCode := s3a.iam.authorizeWithIAM(r, identity, s3_constants.ACTION_LIST, entry.Name, "")
|
||||
hasPermission = (errCode == s3err.ErrNone)
|
||||
} else {
|
||||
// Use legacy authorization for non-JWT users
|
||||
hasPermission = identity.CanDo(s3_constants.ACTION_LIST, entry.Name, "")
|
||||
}
|
||||
|
||||
if !hasPermission {
|
||||
if errCode := s3a.iam.VerifyActionPermission(r, identity, s3_constants.ACTION_LIST, entry.Name, ""); errCode != s3err.ErrNone {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1043,3 +1043,43 @@ func TestListBucketsIssue7796(t *testing.T) {
|
||||
"geoserver should NOT see buckets they neither own nor have permission for")
|
||||
})
|
||||
}
|
||||
|
||||
func TestListBucketsIssue8516PolicyBasedVisibility(t *testing.T) {
|
||||
iam := &IdentityAccessManagement{}
|
||||
require.NoError(t, iam.PutPolicy("listOnly", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:ListBucket","Resource":"arn:aws:s3:::policy-bucket"}]}`))
|
||||
|
||||
identity := &Identity{
|
||||
Name: "policy-user",
|
||||
Account: &AccountAdmin,
|
||||
PolicyNames: []string{"listOnly"},
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "http://s3.amazonaws.com/", nil)
|
||||
buckets := []*filer_pb.Entry{
|
||||
{
|
||||
Name: "policy-bucket",
|
||||
IsDirectory: true,
|
||||
Extended: map[string][]byte{s3_constants.AmzIdentityId: []byte("admin")},
|
||||
Attributes: &filer_pb.FuseAttributes{Crtime: time.Now().Unix()},
|
||||
},
|
||||
{
|
||||
Name: "other-bucket",
|
||||
IsDirectory: true,
|
||||
Extended: map[string][]byte{s3_constants.AmzIdentityId: []byte("admin")},
|
||||
Attributes: &filer_pb.FuseAttributes{Crtime: time.Now().Unix()},
|
||||
},
|
||||
}
|
||||
|
||||
var visibleBuckets []string
|
||||
for _, entry := range buckets {
|
||||
isOwner := isBucketOwnedByIdentity(entry, identity)
|
||||
if !isOwner {
|
||||
if errCode := iam.VerifyActionPermission(req, identity, s3_constants.ACTION_LIST, entry.Name, ""); errCode != s3err.ErrNone {
|
||||
continue
|
||||
}
|
||||
}
|
||||
visibleBuckets = append(visibleBuckets, entry.Name)
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"policy-bucket"}, visibleBuckets)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package s3api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -34,7 +35,7 @@ func NewCircuitBreaker(option *S3ApiServerOption) *CircuitBreaker {
|
||||
|
||||
// Use WithOneOfGrpcFilerClients to support multiple filers with failover
|
||||
err := pb.WithOneOfGrpcFilerClients(false, option.Filers, option.GrpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
|
||||
content, err := filer.ReadInsideFiler(client, s3_constants.CircuitBreakerConfigDir, s3_constants.CircuitBreakerConfigFile)
|
||||
content, err := filer.ReadInsideFiler(context.Background(), client, s3_constants.CircuitBreakerConfigDir, s3_constants.CircuitBreakerConfigFile)
|
||||
if errors.Is(err, filer_pb.ErrNotFound) {
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user