Implement managed policy storage (#8385)
* Persist managed IAM policies * Add IAM list/get policy integration test * Faster marker lookup and cleanup * Handle delete conflict and improve listing * Add delete-in-use policy integration test * Stabilize policy ID and guard path prefix * Tighten CreatePolicy guard and reload * Add ListPolicyNames to credential store
This commit is contained in:
@@ -2,6 +2,7 @@ package policy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"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 {
|
func execShell(t *testing.T, weedCmd, master, filer, shellCmd string) string {
|
||||||
// weed shell -master=... -filer=...
|
// weed shell -master=... -filer=...
|
||||||
args := []string{"shell", "-master=" + master, "-filer=" + filer}
|
args := []string{"shell", "-master=" + master, "-filer=" + filer}
|
||||||
@@ -249,6 +329,15 @@ func attachedPolicyContains(policies []*iam.AttachedPolicy, policyName string) b
|
|||||||
return false
|
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 {
|
func uniqueName(prefix string) string {
|
||||||
return fmt.Sprintf("%s-%s", prefix, strconv.FormatInt(time.Now().UnixNano(), 36))
|
return fmt.Sprintf("%s-%s", prefix, strconv.FormatInt(time.Now().UnixNano(), 36))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,6 +144,11 @@ func (cm *CredentialManager) GetPolicy(ctx context.Context, name string) (*polic
|
|||||||
return cm.Store.GetPolicy(ctx, name)
|
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)
|
// CreatePolicy creates a new policy (if supported by the store)
|
||||||
func (cm *CredentialManager) CreatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
|
func (cm *CredentialManager) CreatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
|
||||||
// Check if the store implements PolicyManager interface with CreatePolicy
|
// Check if the store implements PolicyManager interface with CreatePolicy
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ type CredentialStore interface {
|
|||||||
|
|
||||||
// Policy Management
|
// Policy Management
|
||||||
GetPolicies(ctx context.Context) (map[string]policy_engine.PolicyDocument, error)
|
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 creates or replaces a policy document.
|
||||||
PutPolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error
|
PutPolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error
|
||||||
DeletePolicy(ctx context.Context, name string) error
|
DeletePolicy(ctx context.Context, name string) error
|
||||||
|
|||||||
@@ -235,3 +235,45 @@ func (store *FilerEtcStore) GetPolicy(ctx context.Context, name string) (*policy
|
|||||||
|
|
||||||
return nil, nil // Policy not found
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -79,6 +79,22 @@ func (store *IamGrpcStore) CreatePolicy(ctx context.Context, name string, docume
|
|||||||
return store.PutPolicy(ctx, name, document)
|
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)
|
// UpdatePolicy updates an existing policy (delegates to PutPolicy)
|
||||||
func (store *IamGrpcStore) UpdatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
|
func (store *IamGrpcStore) UpdatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
|
||||||
return store.PutPolicy(ctx, name, document)
|
return store.PutPolicy(ctx, name, document)
|
||||||
|
|||||||
@@ -25,6 +25,23 @@ func (store *MemoryStore) GetPolicies(ctx context.Context) (map[string]policy_en
|
|||||||
return policies, nil
|
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
|
// GetPolicy retrieves a specific IAM policy by name from memory
|
||||||
func (store *MemoryStore) GetPolicy(ctx context.Context, name string) (*policy_engine.PolicyDocument, error) {
|
func (store *MemoryStore) GetPolicy(ctx context.Context, name string) (*policy_engine.PolicyDocument, error) {
|
||||||
store.mu.RLock()
|
store.mu.RLock()
|
||||||
|
|||||||
@@ -41,6 +41,30 @@ func (store *PostgresStore) GetPolicies(ctx context.Context) (map[string]policy_
|
|||||||
return policies, nil
|
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
|
// CreatePolicy creates a new IAM policy in PostgreSQL
|
||||||
func (store *PostgresStore) CreatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
|
func (store *PostgresStore) CreatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
|
||||||
if !store.configured {
|
if !store.configured {
|
||||||
|
|||||||
@@ -236,6 +236,10 @@ func (s *PropagatingCredentialStore) DeletePolicy(ctx context.Context, name stri
|
|||||||
return nil
|
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 {
|
func (s *PropagatingCredentialStore) CreatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
|
||||||
if pm, ok := s.CredentialStore.(PolicyManager); ok {
|
if pm, ok := s.CredentialStore.(PolicyManager); ok {
|
||||||
if err := pm.CreatePolicy(ctx, name, document); err != nil {
|
if err := pm.CreatePolicy(ctx, name, document); err != nil {
|
||||||
|
|||||||
@@ -55,6 +55,32 @@ type CreatePolicyResponse struct {
|
|||||||
} `xml:"CreatePolicyResult"`
|
} `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.
|
// CreateUserResponse is the response for CreateUser action.
|
||||||
type CreateUserResponse struct {
|
type CreateUserResponse struct {
|
||||||
CommonResponse
|
CommonResponse
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -71,6 +72,9 @@ type (
|
|||||||
iamListAccessKeysResponse = iamlib.ListAccessKeysResponse
|
iamListAccessKeysResponse = iamlib.ListAccessKeysResponse
|
||||||
iamDeleteAccessKeyResponse = iamlib.DeleteAccessKeyResponse
|
iamDeleteAccessKeyResponse = iamlib.DeleteAccessKeyResponse
|
||||||
iamCreatePolicyResponse = iamlib.CreatePolicyResponse
|
iamCreatePolicyResponse = iamlib.CreatePolicyResponse
|
||||||
|
iamDeletePolicyResponse = iamlib.DeletePolicyResponse
|
||||||
|
iamListPoliciesResponse = iamlib.ListPoliciesResponse
|
||||||
|
iamGetPolicyResponse = iamlib.GetPolicyResponse
|
||||||
iamCreateUserResponse = iamlib.CreateUserResponse
|
iamCreateUserResponse = iamlib.CreateUserResponse
|
||||||
iamDeleteUserResponse = iamlib.DeleteUserResponse
|
iamDeleteUserResponse = iamlib.DeleteUserResponse
|
||||||
iamGetUserResponse = iamlib.GetUserResponse
|
iamGetUserResponse = iamlib.GetUserResponse
|
||||||
@@ -172,6 +176,8 @@ func (e *EmbeddedIamApi) writeIamErrorResponse(w http.ResponseWriter, r *http.Re
|
|||||||
s3err.WriteXMLResponse(w, r, http.StatusInternalServerError, internalErrorResponse)
|
s3err.WriteXMLResponse(w, r, http.StatusInternalServerError, internalErrorResponse)
|
||||||
case "NotImplemented":
|
case "NotImplemented":
|
||||||
s3err.WriteXMLResponse(w, r, http.StatusNotImplemented, errorResp)
|
s3err.WriteXMLResponse(w, r, http.StatusNotImplemented, errorResp)
|
||||||
|
case iam.ErrCodeDeleteConflictException:
|
||||||
|
s3err.WriteXMLResponse(w, r, http.StatusConflict, errorResp)
|
||||||
default:
|
default:
|
||||||
s3err.WriteXMLResponse(w, r, http.StatusInternalServerError, internalErrorResponse)
|
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.
|
// CreatePolicy validates and creates a new IAM managed policy.
|
||||||
// NOTE: Currently this only validates the policy document and returns policy metadata.
|
func (e *EmbeddedIamApi) CreatePolicy(ctx context.Context, values url.Values) (iamCreatePolicyResponse, *iamError) {
|
||||||
// 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) {
|
|
||||||
var resp iamCreatePolicyResponse
|
var resp iamCreatePolicyResponse
|
||||||
policyName := values.Get("PolicyName")
|
policyName := values.Get("PolicyName")
|
||||||
policyDocumentString := values.Get("PolicyDocument")
|
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 {
|
if err != nil {
|
||||||
return resp, &iamError{Code: iam.ErrCodeMalformedPolicyDocumentException, Error: err}
|
return resp, &iamError{Code: iam.ErrCodeMalformedPolicyDocumentException, Error: err}
|
||||||
}
|
}
|
||||||
policyId := iamHash(&policyDocumentString)
|
existing, err := e.credentialManager.GetPolicy(ctx, policyName)
|
||||||
arn := fmt.Sprintf("arn:aws:iam:::policy/%s", 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.PolicyName = &policyName
|
||||||
resp.CreatePolicyResult.Policy.Arn = &arn
|
resp.CreatePolicyResult.Policy.Arn = &arn
|
||||||
resp.CreatePolicyResult.Policy.PolicyId = &policyId
|
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
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1274,7 +1453,7 @@ func (e *EmbeddedIamApi) ExecuteAction(ctx context.Context, values url.Values, s
|
|||||||
action := values.Get("Action")
|
action := values.Get("Action")
|
||||||
if e.readOnly {
|
if e.readOnly {
|
||||||
switch action {
|
switch action {
|
||||||
case "ListUsers", "ListAccessKeys", "GetUser", "GetUserPolicy", "ListAttachedUserPolicies", "ListServiceAccounts", "GetServiceAccount":
|
case "ListUsers", "ListAccessKeys", "GetUser", "GetUserPolicy", "ListAttachedUserPolicies", "ListPolicies", "GetPolicy", "ListServiceAccounts", "GetServiceAccount":
|
||||||
// Allowed read-only actions
|
// Allowed read-only actions
|
||||||
default:
|
default:
|
||||||
return nil, &iamError{Code: s3err.GetAPIError(s3err.ErrAccessDenied).Code, Error: fmt.Errorf("IAM write operations are disabled on this server")}
|
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":
|
case "DeleteAccessKey":
|
||||||
response = e.DeleteAccessKey(s3cfg, values)
|
response = e.DeleteAccessKey(s3cfg, values)
|
||||||
case "CreatePolicy":
|
case "CreatePolicy":
|
||||||
response, iamErr = e.CreatePolicy(s3cfg, values)
|
response, iamErr = e.CreatePolicy(ctx, values)
|
||||||
if iamErr != nil {
|
if iamErr != nil {
|
||||||
glog.Errorf("CreatePolicy: %+v", iamErr.Error)
|
glog.Errorf("CreatePolicy: %+v", iamErr.Error)
|
||||||
return nil, iamErr
|
return nil, iamErr
|
||||||
}
|
}
|
||||||
|
changed = false
|
||||||
case "DeletePolicy":
|
case "DeletePolicy":
|
||||||
// Managed policies are not stored separately, so deletion is a no-op.
|
response, iamErr = e.DeletePolicy(ctx, values)
|
||||||
// Returns success for AWS compatibility.
|
if iamErr != nil {
|
||||||
response = struct{}{}
|
glog.Errorf("DeletePolicy: %+v", iamErr.Error)
|
||||||
|
return nil, iamErr
|
||||||
|
}
|
||||||
changed = false
|
changed = false
|
||||||
case "PutUserPolicy":
|
case "PutUserPolicy":
|
||||||
response, iamErr = e.PutUserPolicy(s3cfg, values)
|
response, iamErr = e.PutUserPolicy(s3cfg, values)
|
||||||
@@ -1376,6 +1558,18 @@ func (e *EmbeddedIamApi) ExecuteAction(ctx context.Context, values url.Values, s
|
|||||||
return nil, iamErr
|
return nil, iamErr
|
||||||
}
|
}
|
||||||
changed = false
|
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":
|
case "SetUserStatus":
|
||||||
response, iamErr = e.SetUserStatus(s3cfg, values)
|
response, iamErr = e.SetUserStatus(s3cfg, values)
|
||||||
if iamErr != nil {
|
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)
|
glog.Errorf("Failed to reload IAM configuration after mutation: %v", err)
|
||||||
// Don't fail the request since the persistent save succeeded
|
// 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
|
// Even if changed=false (persisted via credentialManager), we should still reload
|
||||||
// if we are utilizing the local in-memory cache for speed
|
// if we are utilizing the local in-memory cache for speed
|
||||||
if err := e.ReloadConfiguration(); err != nil {
|
if err := e.ReloadConfiguration(); err != nil {
|
||||||
|
|||||||
@@ -518,6 +518,37 @@ func TestEmbeddedIamDetachUserPolicy(t *testing.T) {
|
|||||||
assert.Equal(t, []string{"KeepPolicy"}, api.mockConfig.Identities[0].PolicyNames)
|
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
|
// TestEmbeddedIamAttachAlreadyAttachedPolicy ensures attaching a policy already
|
||||||
// present on the user is idempotent.
|
// present on the user is idempotent.
|
||||||
func TestEmbeddedIamAttachAlreadyAttachedPolicy(t *testing.T) {
|
func TestEmbeddedIamAttachAlreadyAttachedPolicy(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user