diff --git a/test/s3/policy/policy_test.go b/test/s3/policy/policy_test.go index 509834bb8..ef2eca670 100644 --- a/test/s3/policy/policy_test.go +++ b/test/s3/policy/policy_test.go @@ -2,6 +2,7 @@ package policy import ( "context" + "errors" "fmt" "net" "net/http" @@ -201,6 +202,85 @@ func TestS3IAMAttachDetachUserPolicy(t *testing.T) { } } +func TestS3IAMListPoliciesAndGetPolicy(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + cluster, err := startMiniCluster(t) + require.NoError(t, err) + defer cluster.Stop() + + time.Sleep(500 * time.Millisecond) + + policyName := uniqueName("managed-policy") + policyArn := fmt.Sprintf("arn:aws:iam:::policy/%s", policyName) + policyContent := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:ListAllMyBuckets","Resource":"*"}]}` + + iamClient := newIAMClient(t, cluster.s3Endpoint) + _, err = iamClient.CreatePolicy(&iam.CreatePolicyInput{ + PolicyName: aws.String(policyName), + PolicyDocument: aws.String(policyContent), + }) + require.NoError(t, err) + + listOut, err := iamClient.ListPolicies(&iam.ListPoliciesInput{}) + require.NoError(t, err) + require.True(t, managedPolicyContains(listOut.Policies, policyName)) + + getOut, err := iamClient.GetPolicy(&iam.GetPolicyInput{PolicyArn: aws.String(policyArn)}) + require.NoError(t, err) + require.NotNil(t, getOut.Policy) + require.NotNil(t, getOut.Policy.PolicyName) + require.Equal(t, policyName, *getOut.Policy.PolicyName) + + missingArn := fmt.Sprintf("arn:aws:iam:::policy/%s", uniqueName("missing")) + _, err = iamClient.GetPolicy(&iam.GetPolicyInput{PolicyArn: aws.String(missingArn)}) + require.Error(t, err) + var awsErr awserr.Error + require.True(t, errors.As(err, &awsErr)) + require.Equal(t, iam.ErrCodeNoSuchEntityException, awsErr.Code()) +} + +func TestS3IAMDeletePolicyInUse(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + cluster, err := startMiniCluster(t) + require.NoError(t, err) + defer cluster.Stop() + + time.Sleep(500 * time.Millisecond) + + policyName := uniqueName("managed-delete-policy") + policyArn := fmt.Sprintf("arn:aws:iam:::policy/%s", policyName) + policyContent := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:*","Resource":"*"}]}` + + iamClient := newIAMClient(t, cluster.s3Endpoint) + _, err = iamClient.CreatePolicy(&iam.CreatePolicyInput{ + PolicyName: aws.String(policyName), + PolicyDocument: aws.String(policyContent), + }) + require.NoError(t, err) + + userName := uniqueName("iam-user-delete-policy") + _, err = iamClient.CreateUser(&iam.CreateUserInput{UserName: aws.String(userName)}) + require.NoError(t, err) + + _, err = iamClient.AttachUserPolicy(&iam.AttachUserPolicyInput{ + UserName: aws.String(userName), + PolicyArn: aws.String(policyArn), + }) + require.NoError(t, err) + + _, err = iamClient.DeletePolicy(&iam.DeletePolicyInput{PolicyArn: aws.String(policyArn)}) + require.Error(t, err) + var awsErr awserr.Error + require.True(t, errors.As(err, &awsErr)) + require.Equal(t, iam.ErrCodeDeleteConflictException, awsErr.Code()) +} + func execShell(t *testing.T, weedCmd, master, filer, shellCmd string) string { // weed shell -master=... -filer=... args := []string{"shell", "-master=" + master, "-filer=" + filer} @@ -249,6 +329,15 @@ func attachedPolicyContains(policies []*iam.AttachedPolicy, policyName string) b return false } +func managedPolicyContains(policies []*iam.Policy, policyName string) bool { + for _, policy := range policies { + if policy.PolicyName != nil && *policy.PolicyName == policyName { + return true + } + } + return false +} + func uniqueName(prefix string) string { return fmt.Sprintf("%s-%s", prefix, strconv.FormatInt(time.Now().UnixNano(), 36)) } diff --git a/weed/credential/credential_manager.go b/weed/credential/credential_manager.go index 591f52ff8..e78cbf4c9 100644 --- a/weed/credential/credential_manager.go +++ b/weed/credential/credential_manager.go @@ -144,6 +144,11 @@ func (cm *CredentialManager) GetPolicy(ctx context.Context, name string) (*polic return cm.Store.GetPolicy(ctx, name) } +// ListPolicyNames returns the names of all policies +func (cm *CredentialManager) ListPolicyNames(ctx context.Context) ([]string, error) { + return cm.Store.ListPolicyNames(ctx) +} + // CreatePolicy creates a new policy (if supported by the store) func (cm *CredentialManager) CreatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error { // Check if the store implements PolicyManager interface with CreatePolicy diff --git a/weed/credential/credential_store.go b/weed/credential/credential_store.go index 1b174c634..0458677b9 100644 --- a/weed/credential/credential_store.go +++ b/weed/credential/credential_store.go @@ -71,6 +71,8 @@ type CredentialStore interface { // Policy Management GetPolicies(ctx context.Context) (map[string]policy_engine.PolicyDocument, error) + // ListPolicyNames returns the names of all policies + ListPolicyNames(ctx context.Context) ([]string, error) // PutPolicy creates or replaces a policy document. PutPolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error DeletePolicy(ctx context.Context, name string) error diff --git a/weed/credential/filer_etc/filer_etc_policy.go b/weed/credential/filer_etc/filer_etc_policy.go index 895154e91..bff70fc0b 100644 --- a/weed/credential/filer_etc/filer_etc_policy.go +++ b/weed/credential/filer_etc/filer_etc_policy.go @@ -235,3 +235,45 @@ func (store *FilerEtcStore) GetPolicy(ctx context.Context, name string) (*policy return nil, nil // Policy not found } + +// ListPolicyNames returns all managed policy names stored in the filer. +func (store *FilerEtcStore) ListPolicyNames(ctx context.Context) ([]string, error) { + names := make([]string, 0) + + store.mu.RLock() + configured := store.filerAddressFunc != nil + store.mu.RUnlock() + + if !configured { + return names, nil + } + + err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error { + dir := filer.IamConfigDirectory + "/" + IamPoliciesDirectory + entries, err := listEntries(ctx, client, dir) + if err != nil { + if err == filer_pb.ErrNotFound { + return nil + } + return err + } + + for _, entry := range entries { + if entry.IsDirectory { + continue + } + name := entry.Name + if strings.HasSuffix(name, ".json") { + name = name[:len(name)-5] + } + names = append(names, name) + } + + return nil + }) + + if err != nil { + return nil, err + } + return names, nil +} diff --git a/weed/credential/grpc/grpc_policy.go b/weed/credential/grpc/grpc_policy.go index ed02ecf22..52ab9e773 100644 --- a/weed/credential/grpc/grpc_policy.go +++ b/weed/credential/grpc/grpc_policy.go @@ -79,6 +79,22 @@ func (store *IamGrpcStore) CreatePolicy(ctx context.Context, name string, docume return store.PutPolicy(ctx, name, document) } +// ListPolicyNames retrieves names of all IAM policies via gRPC. +func (store *IamGrpcStore) ListPolicyNames(ctx context.Context) ([]string, error) { + var names []string + err := store.withIamClient(func(client iam_pb.SeaweedIdentityAccessManagementClient) error { + resp, err := client.ListPolicies(ctx, &iam_pb.ListPoliciesRequest{}) + if err != nil { + return err + } + for _, policy := range resp.Policies { + names = append(names, policy.Name) + } + return nil + }) + return names, err +} + // UpdatePolicy updates an existing policy (delegates to PutPolicy) func (store *IamGrpcStore) UpdatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error { return store.PutPolicy(ctx, name, document) diff --git a/weed/credential/memory/memory_policy.go b/weed/credential/memory/memory_policy.go index b5e7fdb7a..a86cb0af6 100644 --- a/weed/credential/memory/memory_policy.go +++ b/weed/credential/memory/memory_policy.go @@ -25,6 +25,23 @@ func (store *MemoryStore) GetPolicies(ctx context.Context) (map[string]policy_en return policies, nil } +// ListPolicyNames returns all stored policy names. +func (store *MemoryStore) ListPolicyNames(ctx context.Context) ([]string, error) { + store.mu.RLock() + defer store.mu.RUnlock() + + if !store.initialized { + return nil, fmt.Errorf("store not initialized") + } + + names := make([]string, 0, len(store.policies)) + for name := range store.policies { + names = append(names, name) + } + + return names, nil +} + // GetPolicy retrieves a specific IAM policy by name from memory func (store *MemoryStore) GetPolicy(ctx context.Context, name string) (*policy_engine.PolicyDocument, error) { store.mu.RLock() diff --git a/weed/credential/postgres/postgres_policy.go b/weed/credential/postgres/postgres_policy.go index e3a831ae4..68fb6e044 100644 --- a/weed/credential/postgres/postgres_policy.go +++ b/weed/credential/postgres/postgres_policy.go @@ -41,6 +41,30 @@ func (store *PostgresStore) GetPolicies(ctx context.Context) (map[string]policy_ return policies, nil } +// ListPolicyNames returns all managed policy names from PostgreSQL. +func (store *PostgresStore) ListPolicyNames(ctx context.Context) ([]string, error) { + if !store.configured { + return nil, fmt.Errorf("store not configured") + } + + var names []string + rows, err := store.db.QueryContext(ctx, "SELECT name FROM policies") + if err != nil { + return nil, fmt.Errorf("failed to query policy names: %w", err) + } + defer rows.Close() + + for rows.Next() { + var name string + if err := rows.Scan(&name); err != nil { + return nil, fmt.Errorf("failed to scan policy name: %w", err) + } + names = append(names, name) + } + + return names, nil +} + // CreatePolicy creates a new IAM policy in PostgreSQL func (store *PostgresStore) CreatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error { if !store.configured { diff --git a/weed/credential/propagating_store.go b/weed/credential/propagating_store.go index bd6bce1c4..26b3446c7 100644 --- a/weed/credential/propagating_store.go +++ b/weed/credential/propagating_store.go @@ -236,6 +236,10 @@ func (s *PropagatingCredentialStore) DeletePolicy(ctx context.Context, name stri return nil } +func (s *PropagatingCredentialStore) ListPolicyNames(ctx context.Context) ([]string, error) { + return s.CredentialStore.ListPolicyNames(ctx) +} + 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 { diff --git a/weed/iam/responses.go b/weed/iam/responses.go index 2c72a5720..0afd9f6da 100644 --- a/weed/iam/responses.go +++ b/weed/iam/responses.go @@ -55,6 +55,32 @@ type CreatePolicyResponse struct { } `xml:"CreatePolicyResult"` } +// DeletePolicyResponse is the response for DeletePolicy action. +type DeletePolicyResponse struct { + CommonResponse + XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ DeletePolicyResponse"` +} + +// ListPoliciesResponse is the response for ListPolicies action. +type ListPoliciesResponse struct { + CommonResponse + XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ ListPoliciesResponse"` + ListPoliciesResult struct { + Policies []*iam.Policy `xml:"Policies>member"` + IsTruncated bool `xml:"IsTruncated"` + Marker string `xml:"Marker,omitempty"` + } `xml:"ListPoliciesResult"` +} + +// GetPolicyResponse is the response for GetPolicy action. +type GetPolicyResponse struct { + CommonResponse + XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ GetPolicyResponse"` + GetPolicyResult struct { + Policy iam.Policy `xml:"Policy"` + } `xml:"GetPolicyResult"` +} + // CreateUserResponse is the response for CreateUser action. type CreateUserResponse struct { CommonResponse diff --git a/weed/s3api/s3api_embedded_iam.go b/weed/s3api/s3api_embedded_iam.go index da88b7b0b..886f9d339 100644 --- a/weed/s3api/s3api_embedded_iam.go +++ b/weed/s3api/s3api_embedded_iam.go @@ -10,6 +10,7 @@ import ( "fmt" "net/http" "net/url" + "sort" "strconv" "strings" "sync" @@ -71,6 +72,9 @@ type ( iamListAccessKeysResponse = iamlib.ListAccessKeysResponse iamDeleteAccessKeyResponse = iamlib.DeleteAccessKeyResponse iamCreatePolicyResponse = iamlib.CreatePolicyResponse + iamDeletePolicyResponse = iamlib.DeletePolicyResponse + iamListPoliciesResponse = iamlib.ListPoliciesResponse + iamGetPolicyResponse = iamlib.GetPolicyResponse iamCreateUserResponse = iamlib.CreateUserResponse iamDeleteUserResponse = iamlib.DeleteUserResponse iamGetUserResponse = iamlib.GetUserResponse @@ -172,6 +176,8 @@ func (e *EmbeddedIamApi) writeIamErrorResponse(w http.ResponseWriter, r *http.Re s3err.WriteXMLResponse(w, r, http.StatusInternalServerError, internalErrorResponse) case "NotImplemented": s3err.WriteXMLResponse(w, r, http.StatusNotImplemented, errorResp) + case iam.ErrCodeDeleteConflictException: + s3err.WriteXMLResponse(w, r, http.StatusConflict, errorResp) default: s3err.WriteXMLResponse(w, r, http.StatusInternalServerError, internalErrorResponse) } @@ -378,23 +384,196 @@ func (e *EmbeddedIamApi) GetPolicyDocument(policy *string) (policy_engine.Policy } // CreatePolicy validates and creates a new IAM managed policy. -// NOTE: Currently this only validates the policy document and returns policy metadata. -// The policy is not persisted to a managed policy store. To apply permissions to a user, -// use PutUserPolicy which stores the policy inline on the user's identity. -// TODO: Implement managed policy storage for full AWS IAM compatibility (ListPolicies, GetPolicy). -func (e *EmbeddedIamApi) CreatePolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (iamCreatePolicyResponse, *iamError) { +func (e *EmbeddedIamApi) CreatePolicy(ctx context.Context, values url.Values) (iamCreatePolicyResponse, *iamError) { var resp iamCreatePolicyResponse policyName := values.Get("PolicyName") policyDocumentString := values.Get("PolicyDocument") - _, err := e.GetPolicyDocument(&policyDocumentString) + if policyName == "" { + return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("PolicyName is required")} + } + if policyDocumentString == "" { + return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("PolicyDocument is required")} + } + if e.credentialManager == nil { + return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("credential manager not configured")} + } + policyDocument, err := e.GetPolicyDocument(&policyDocumentString) if err != nil { return resp, &iamError{Code: iam.ErrCodeMalformedPolicyDocumentException, Error: err} } - policyId := iamHash(&policyDocumentString) - arn := fmt.Sprintf("arn:aws:iam:::policy/%s", policyName) + existing, err := e.credentialManager.GetPolicy(ctx, policyName) + if err != nil { + return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err} + } + if existing != nil { + return resp, &iamError{Code: iam.ErrCodeEntityAlreadyExistsException, Error: fmt.Errorf("policy %s already exists", policyName)} + } + if err := e.credentialManager.CreatePolicy(ctx, policyName, policyDocument); err != nil { + return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err} + } + + policyId := iamHash(&policyName) + arn := iamPolicyArn(policyName) resp.CreatePolicyResult.Policy.PolicyName = &policyName resp.CreatePolicyResult.Policy.Arn = &arn resp.CreatePolicyResult.Policy.PolicyId = &policyId + path := "/" + defaultVersionId := "v1" + isAttachable := true + resp.CreatePolicyResult.Policy.Path = &path + resp.CreatePolicyResult.Policy.DefaultVersionId = &defaultVersionId + resp.CreatePolicyResult.Policy.IsAttachable = &isAttachable + return resp, nil +} + +// DeletePolicy deletes a managed policy by ARN. +func (e *EmbeddedIamApi) DeletePolicy(ctx context.Context, values url.Values) (iamDeletePolicyResponse, *iamError) { + var resp iamDeletePolicyResponse + policyArn := values.Get("PolicyArn") + policyName, err := iamPolicyNameFromArn(policyArn) + if err != nil { + return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: err} + } + if e.credentialManager == nil { + return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("credential manager not configured")} + } + policy, err := e.credentialManager.GetPolicy(ctx, policyName) + if err != nil { + return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err} + } + if policy == nil { + return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)} + } + users, err := e.credentialManager.ListUsers(ctx) + if err != nil { + return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err} + } + for _, user := range users { + attachedPolicies, err := e.credentialManager.ListAttachedUserPolicies(ctx, user) + if err != nil { + return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err} + } + for _, attached := range attachedPolicies { + if attached == policyName { + return resp, &iamError{ + Code: iam.ErrCodeDeleteConflictException, + Error: fmt.Errorf("policy %s is attached to user %s", policyName, user), + } + } + } + } + if err := e.credentialManager.DeletePolicy(ctx, policyName); err != nil { + return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err} + } + return resp, nil +} + +// ListPolicies lists managed policies. +func (e *EmbeddedIamApi) ListPolicies(ctx context.Context, values url.Values) (iamListPoliciesResponse, *iamError) { + var resp iamListPoliciesResponse + pathPrefix := values.Get("PathPrefix") + if pathPrefix == "" { + pathPrefix = "/" + } + maxItems := 0 + if maxItemsStr := values.Get("MaxItems"); maxItemsStr != "" { + parsedMaxItems, err := strconv.Atoi(maxItemsStr) + if err != nil || parsedMaxItems <= 0 { + return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("MaxItems must be a positive integer")} + } + maxItems = parsedMaxItems + } + marker := values.Get("Marker") + + if e.credentialManager == nil { + return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("credential manager not configured")} + } + + if pathPrefix != "/" { + return resp, &iamError{Code: "NotImplemented", Error: fmt.Errorf("PathPrefix filtering is not supported yet")} + } + + policyNames, err := e.credentialManager.ListPolicyNames(ctx) + if err != nil { + return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err} + } + sort.Strings(policyNames) + + if marker != "" { + i := sort.SearchStrings(policyNames, marker) + if i < len(policyNames) && policyNames[i] == marker { + policyNames = policyNames[i+1:] + } else if i < len(policyNames) { + policyNames = policyNames[i:] + } else { + policyNames = nil + } + } + + // Policy paths are not tracked in the current configuration, so PathPrefix filtering is not supported yet. + for _, name := range policyNames { + policyNameCopy := name + policyArnCopy := iamPolicyArn(name) + policyId := iamHash(&policyNameCopy) + path := "/" + defaultVersionId := "v1" + isAttachable := true + resp.ListPoliciesResult.Policies = append(resp.ListPoliciesResult.Policies, &iam.Policy{ + PolicyName: &policyNameCopy, + Arn: &policyArnCopy, + PolicyId: &policyId, + Path: &path, + DefaultVersionId: &defaultVersionId, + IsAttachable: &isAttachable, + }) + } + + if maxItems > 0 && len(resp.ListPoliciesResult.Policies) > maxItems { + resp.ListPoliciesResult.Policies = resp.ListPoliciesResult.Policies[:maxItems] + resp.ListPoliciesResult.IsTruncated = true + if name := resp.ListPoliciesResult.Policies[maxItems-1].PolicyName; name != nil { + resp.ListPoliciesResult.Marker = *name + } + return resp, nil + } + + resp.ListPoliciesResult.IsTruncated = false + return resp, nil +} + +// GetPolicy returns metadata for a managed policy. +func (e *EmbeddedIamApi) GetPolicy(ctx context.Context, values url.Values) (iamGetPolicyResponse, *iamError) { + var resp iamGetPolicyResponse + policyArn := values.Get("PolicyArn") + policyName, err := iamPolicyNameFromArn(policyArn) + if err != nil { + return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: err} + } + if e.credentialManager == nil { + return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("credential manager not configured")} + } + policy, err := e.credentialManager.GetPolicy(ctx, policyName) + if err != nil { + return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err} + } + if policy == nil { + return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)} + } + + policyNameCopy := policyName + policyArnCopy := iamPolicyArn(policyName) + policyId := iamHash(&policyNameCopy) + path := "/" + defaultVersionId := "v1" + isAttachable := true + resp.GetPolicyResult.Policy = iam.Policy{ + PolicyName: &policyNameCopy, + Arn: &policyArnCopy, + PolicyId: &policyId, + Path: &path, + DefaultVersionId: &defaultVersionId, + IsAttachable: &isAttachable, + } return resp, nil } @@ -1274,7 +1453,7 @@ func (e *EmbeddedIamApi) ExecuteAction(ctx context.Context, values url.Values, s action := values.Get("Action") if e.readOnly { switch action { - case "ListUsers", "ListAccessKeys", "GetUser", "GetUserPolicy", "ListAttachedUserPolicies", "ListServiceAccounts", "GetServiceAccount": + case "ListUsers", "ListAccessKeys", "GetUser", "GetUserPolicy", "ListAttachedUserPolicies", "ListPolicies", "GetPolicy", "ListServiceAccounts", "GetServiceAccount": // Allowed read-only actions default: return nil, &iamError{Code: s3err.GetAPIError(s3err.ErrAccessDenied).Code, Error: fmt.Errorf("IAM write operations are disabled on this server")} @@ -1331,15 +1510,18 @@ func (e *EmbeddedIamApi) ExecuteAction(ctx context.Context, values url.Values, s case "DeleteAccessKey": response = e.DeleteAccessKey(s3cfg, values) case "CreatePolicy": - response, iamErr = e.CreatePolicy(s3cfg, values) + response, iamErr = e.CreatePolicy(ctx, values) if iamErr != nil { glog.Errorf("CreatePolicy: %+v", iamErr.Error) return nil, iamErr } + changed = false case "DeletePolicy": - // Managed policies are not stored separately, so deletion is a no-op. - // Returns success for AWS compatibility. - response = struct{}{} + response, iamErr = e.DeletePolicy(ctx, values) + if iamErr != nil { + glog.Errorf("DeletePolicy: %+v", iamErr.Error) + return nil, iamErr + } changed = false case "PutUserPolicy": response, iamErr = e.PutUserPolicy(s3cfg, values) @@ -1376,6 +1558,18 @@ func (e *EmbeddedIamApi) ExecuteAction(ctx context.Context, values url.Values, s return nil, iamErr } changed = false + case "ListPolicies": + response, iamErr = e.ListPolicies(ctx, values) + if iamErr != nil { + return nil, iamErr + } + changed = false + case "GetPolicy": + response, iamErr = e.GetPolicy(ctx, values) + if iamErr != nil { + return nil, iamErr + } + changed = false case "SetUserStatus": response, iamErr = e.SetUserStatus(s3cfg, values) if iamErr != nil { @@ -1428,7 +1622,7 @@ func (e *EmbeddedIamApi) ExecuteAction(ctx context.Context, values url.Values, s glog.Errorf("Failed to reload IAM configuration after mutation: %v", err) // Don't fail the request since the persistent save succeeded } - } else if iamErr == nil && (action == "AttachUserPolicy" || action == "DetachUserPolicy") { + } else if action == "AttachUserPolicy" || action == "DetachUserPolicy" || action == "CreatePolicy" || action == "DeletePolicy" { // Even if changed=false (persisted via credentialManager), we should still reload // if we are utilizing the local in-memory cache for speed if err := e.ReloadConfiguration(); err != nil { diff --git a/weed/s3api/s3api_embedded_iam_test.go b/weed/s3api/s3api_embedded_iam_test.go index e2e3df8a1..906895f6d 100644 --- a/weed/s3api/s3api_embedded_iam_test.go +++ b/weed/s3api/s3api_embedded_iam_test.go @@ -518,6 +518,37 @@ func TestEmbeddedIamDetachUserPolicy(t *testing.T) { assert.Equal(t, []string{"KeepPolicy"}, api.mockConfig.Identities[0].PolicyNames) } +// TestEmbeddedIamDeletePolicyInUse ensures deleting a policy that is still attached returns conflict. +func TestEmbeddedIamDeletePolicyInUse(t *testing.T) { + api := NewEmbeddedIamApiForTest() + api.mockConfig = &iam_pb.S3ApiConfiguration{ + Identities: []*iam_pb.Identity{ + {Name: "TestUser", PolicyNames: []string{"TestPolicy"}}, + }, + Policies: []*iam_pb.Policy{ + {Name: "TestPolicy", Content: `{"Version":"2012-10-17","Statement":[]}`}, + }, + } + + params := &iam.DeletePolicyInput{ + PolicyArn: aws.String("arn:aws:iam:::policy/TestPolicy"), + } + req, _ := iam.New(session.New()).DeletePolicyRequest(params) + _ = req.Build() + + response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil) + assert.NoError(t, err) + assert.Equal(t, http.StatusConflict, response.Code) + code, _ := extractEmbeddedIamErrorCodeAndMessage(response) + assert.Equal(t, iam.ErrCodeDeleteConflictException, code) + + assert.Len(t, api.mockConfig.Policies, 1) + assert.Equal(t, "TestPolicy", api.mockConfig.Policies[0].Name) + assert.Len(t, api.mockConfig.Identities, 1) + assert.Equal(t, "TestUser", api.mockConfig.Identities[0].Name) + assert.Contains(t, api.mockConfig.Identities[0].PolicyNames, "TestPolicy") +} + // TestEmbeddedIamAttachAlreadyAttachedPolicy ensures attaching a policy already // present on the user is idempotent. func TestEmbeddedIamAttachAlreadyAttachedPolicy(t *testing.T) {