fix(iam): implement CreatePolicyVersion for managed policies (#9795)

* fix(iam): implement CreatePolicyVersion for managed policies

The AWS Terraform provider updates a managed policy in place via
CreatePolicyVersion, which returned 501 NotImplemented and broke
terraform apply on any policy change.

Implement CreatePolicyVersion (plus ListPolicyVersions, GetPolicyVersion
and DeletePolicyVersion) on both the standalone IAM server and the
embedded S3 IAM API. Managed policies keep a single current document, so
each is modeled as one default version "v1": CreatePolicyVersion replaces
the document, List/GetPolicyVersion expose it, and DeletePolicyVersion
rejects deleting the default. GetPolicy now reports DefaultVersionId so
the provider's read can fetch the document. The standalone path also
refreshes the cached Identity.Actions of every identity the policy is
attached to so the new document takes effect.

* fix(iam): reject CreatePolicyVersion unless SetAsDefault=true

With a single always-default managed-policy version, a request with
SetAsDefault=false (or omitted) would stage a non-default version on AWS
but here silently replaced the active document. Reject it on both the
standalone and embedded paths.

Isolate the new policy-version tests from the shared package fixtures so
they stay order-independent, and assert IsDefaultVersion on the response.
This commit is contained in:
Chris Lu
2026-06-02 21:35:02 -07:00
committed by GitHub
parent b6a0bde16b
commit 7b44cf5627
6 changed files with 619 additions and 1 deletions
+15
View File
@@ -183,6 +183,21 @@ type GetPolicyVersionResponse struct {
CommonResponse
}
// CreatePolicyVersionResponse is the response for CreatePolicyVersion action.
type CreatePolicyVersionResponse struct {
XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ CreatePolicyVersionResponse"`
CreatePolicyVersionResult struct {
PolicyVersion iam.PolicyVersion `xml:"PolicyVersion"`
} `xml:"CreatePolicyVersionResult"`
CommonResponse
}
// DeletePolicyVersionResponse is the response for DeletePolicyVersion action.
type DeletePolicyVersionResponse struct {
XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ DeletePolicyVersionResponse"`
CommonResponse
}
// CreateUserResponse is the response for CreateUser action.
type CreateUserResponse struct {
XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ CreateUserResponse"`
+239
View File
@@ -46,6 +46,11 @@ var policyLock = sync.RWMutex{}
const policyArnPrefix = "arn:aws:iam:::policy/"
// policyDefaultVersionId is the single managed-policy version SeaweedFS exposes.
// Managed policies are stored as one current document with no version history,
// so the API presents exactly one version ("v1") which is always the default.
const policyDefaultVersionId = "v1"
// parsePolicyArn validates an IAM policy ARN and extracts the policy name.
func parsePolicyArn(policyArn string) (string, *IamError) {
if !strings.HasPrefix(policyArn, policyArnPrefix) {
@@ -618,9 +623,15 @@ func (iama *IamApiServer) GetPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url
}
policyId := Hash(&policyName)
path := "/"
defaultVersionId := policyDefaultVersionId
isAttachable := true
resp.GetPolicyResult.Policy.PolicyName = &policyName
resp.GetPolicyResult.Policy.Arn = &policyArn
resp.GetPolicyResult.Policy.PolicyId = &policyId
resp.GetPolicyResult.Policy.Path = &path
resp.GetPolicyResult.Policy.DefaultVersionId = &defaultVersionId
resp.GetPolicyResult.Policy.IsAttachable = &isAttachable
return resp, nil
}
@@ -692,6 +703,200 @@ func (iama *IamApiServer) ListPolicies(s3cfg *iam_pb.S3ApiConfiguration, values
return resp, nil
}
// CreatePolicyVersion replaces a managed policy's document with a new version.
// SeaweedFS keeps a single current document per managed policy (no version
// history), so the new document always becomes version "v1" / the default. This
// is what the AWS Terraform provider calls to update an aws_iam_policy in place.
// https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreatePolicyVersion.html
func (iama *IamApiServer) CreatePolicyVersion(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *CreatePolicyVersionResponse, iamError *IamError) {
resp = &CreatePolicyVersionResponse{}
policyArn := values.Get("PolicyArn")
policyName, iamError := parsePolicyArn(policyArn)
if iamError != nil {
return resp, iamError
}
policyDocumentString := values.Get("PolicyDocument")
if policyDocumentString == "" {
return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("PolicyDocument is required")}
}
// SeaweedFS stores a single, always-default managed policy version. On AWS,
// SetAsDefault=false stages a non-default version without activating it; we
// can't honor that, so reject it rather than silently changing permissions.
if !strings.EqualFold(values.Get("SetAsDefault"), "true") {
return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("SetAsDefault must be true: SeaweedFS stores a single managed policy version")}
}
policyDocument, err := GetPolicyDocument(&policyDocumentString)
if err != nil {
return resp, &IamError{Code: iam.ErrCodeMalformedPolicyDocumentException, Error: err}
}
if _, err := GetActions(&policyDocument); err != nil {
return resp, &IamError{Code: iam.ErrCodeMalformedPolicyDocumentException, Error: err}
}
policies := Policies{}
if err = iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
if _, exists := policies.Policies[policyName]; !exists {
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)}
}
policies.Policies[policyName] = policyDocument
if err = iama.s3ApiConfig.PutPolicies(&policies); err != nil {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
// The denormalized Identity.Actions caches were derived from the old
// document; refresh every identity that has this managed policy attached
// (directly or via group membership) so the new document takes effect.
recomputeActionsForManagedPolicy(iama, s3cfg, &policies, policyName)
versionId := policyDefaultVersionId
isDefault := true
document := policyDocumentString
resp.CreatePolicyVersionResult.PolicyVersion = iam.PolicyVersion{
VersionId: &versionId,
IsDefaultVersion: &isDefault,
Document: &document,
}
return resp, nil
}
// ListPolicyVersions lists the versions of a managed policy. SeaweedFS stores a
// single current document per policy, so exactly one version ("v1", the
// default) is reported.
// https://docs.aws.amazon.com/IAM/latest/APIReference/API_ListPolicyVersions.html
func (iama *IamApiServer) ListPolicyVersions(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *ListPolicyVersionsResponse, iamError *IamError) {
resp = &ListPolicyVersionsResponse{}
policyName, iamError := parsePolicyArn(values.Get("PolicyArn"))
if iamError != nil {
return resp, iamError
}
policies := Policies{}
if err := iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
if _, exists := policies.Policies[policyName]; !exists {
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)}
}
versionId := policyDefaultVersionId
isDefault := true
resp.ListPolicyVersionsResult.Versions = []*iam.PolicyVersion{{
VersionId: &versionId,
IsDefaultVersion: &isDefault,
}}
resp.ListPolicyVersionsResult.IsTruncated = false
return resp, nil
}
// GetPolicyVersion returns the document for a managed policy version. Only the
// single stored version ("v1") exists.
// https://docs.aws.amazon.com/IAM/latest/APIReference/API_GetPolicyVersion.html
func (iama *IamApiServer) GetPolicyVersion(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *GetPolicyVersionResponse, iamError *IamError) {
resp = &GetPolicyVersionResponse{}
policyName, iamError := parsePolicyArn(values.Get("PolicyArn"))
if iamError != nil {
return resp, iamError
}
versionId := values.Get("VersionId")
if versionId == "" {
return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("VersionId is required")}
}
policies := Policies{}
if err := iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
policyDocument, exists := policies.Policies[policyName]
if !exists {
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)}
}
if versionId != policyDefaultVersionId {
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy version %s not found", versionId)}
}
policyDocumentJSON, err := json.Marshal(policyDocument)
if err != nil {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
isDefault := true
document := string(policyDocumentJSON)
resp.GetPolicyVersionResult.PolicyVersion = iam.PolicyVersion{
VersionId: &versionId,
IsDefaultVersion: &isDefault,
Document: &document,
}
return resp, nil
}
// DeletePolicyVersion is accepted for API completeness. With a single stored
// version that is always the default, the only valid responses are NoSuchEntity
// (unknown version) and the AWS "cannot delete the default version" conflict.
// https://docs.aws.amazon.com/IAM/latest/APIReference/API_DeletePolicyVersion.html
func (iama *IamApiServer) DeletePolicyVersion(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *DeletePolicyVersionResponse, iamError *IamError) {
resp = &DeletePolicyVersionResponse{}
policyName, iamError := parsePolicyArn(values.Get("PolicyArn"))
if iamError != nil {
return resp, iamError
}
versionId := values.Get("VersionId")
if versionId == "" {
return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("VersionId is required")}
}
policies := Policies{}
if err := iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
if _, exists := policies.Policies[policyName]; !exists {
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)}
}
if versionId == policyDefaultVersionId {
return resp, &IamError{Code: iam.ErrCodeDeleteConflictException, Error: fmt.Errorf("cannot delete the default version of policy %s", policyName)}
}
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy version %s not found", versionId)}
}
// recomputeActionsForManagedPolicy refreshes Identity.Actions for every identity
// affected by a change to the managed policy named policyName: users with the
// policy attached directly, and members of groups the policy is attached to.
func recomputeActionsForManagedPolicy(iama *IamApiServer, s3cfg *iam_pb.S3ApiConfiguration, policies *Policies, policyName string) {
affected := make(map[string]*iam_pb.Identity)
identIndex := make(map[string]*iam_pb.Identity, len(s3cfg.Identities))
for _, ident := range s3cfg.Identities {
identIndex[ident.Name] = ident
for _, pn := range ident.PolicyNames {
if pn == policyName {
affected[ident.Name] = ident
break
}
}
}
for _, g := range s3cfg.Groups {
attached := false
for _, pn := range g.PolicyNames {
if pn == policyName {
attached = true
break
}
}
if !attached {
continue
}
for _, member := range g.Members {
if ident, ok := identIndex[member]; ok {
affected[member] = ident
}
}
}
for name, ident := range affected {
aggregatedActions, err := computeAllActionsForUser(iama, name, policies, ident, s3cfg)
if err != nil {
glog.Warningf("Failed to recompute actions for user %s after managed policy %s change: %v", name, policyName, err)
continue
}
ident.Actions = aggregatedActions
}
}
// AttachUserPolicy attaches a managed policy to a user.
func (iama *IamApiServer) AttachUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *AttachUserPolicyResponse, iamError *IamError) {
resp = &AttachUserPolicyResponse{}
@@ -1246,6 +1451,40 @@ func (iama *IamApiServer) DoActions(w http.ResponseWriter, r *http.Request) {
return
}
changed = false
case "CreatePolicyVersion":
var err *IamError
response, err = iama.CreatePolicyVersion(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
// CreatePolicyVersion persists the new document via PutPolicies and
// recomputes affected Identity.Actions on s3cfg; keep changed=true so the
// updated identities are saved.
case "ListPolicyVersions":
var err *IamError
response, err = iama.ListPolicyVersions(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
changed = false
case "GetPolicyVersion":
var err *IamError
response, err = iama.GetPolicyVersion(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
changed = false
case "DeletePolicyVersion":
var err *IamError
response, err = iama.DeletePolicyVersion(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
changed = false
case "AttachUserPolicy":
var err *IamError
response, err = iama.AttachUserPolicy(s3cfg, values)
+4
View File
@@ -27,6 +27,10 @@ type (
GetPolicyResponse = iamlib.GetPolicyResponse
DeletePolicyResponse = iamlib.DeletePolicyResponse
ListPoliciesResponse = iamlib.ListPoliciesResponse
ListPolicyVersionsResponse = iamlib.ListPolicyVersionsResponse
GetPolicyVersionResponse = iamlib.GetPolicyVersionResponse
CreatePolicyVersionResponse = iamlib.CreatePolicyVersionResponse
DeletePolicyVersionResponse = iamlib.DeletePolicyVersionResponse
AttachUserPolicyResponse = iamlib.AttachUserPolicyResponse
DetachUserPolicyResponse = iamlib.DetachUserPolicyResponse
ListAttachedUserPoliciesResponse = iamlib.ListAttachedUserPoliciesResponse
+170
View File
@@ -197,6 +197,176 @@ func TestCreatePolicy(t *testing.T) {
assert.Equal(t, http.StatusOK, response.Code)
}
// isolatedIamConfig is a self-contained IamS3ApiConfig backing whose state lives
// on the instance (not the package globals that executeRequest shares), so
// policy-version tests stay order-independent under -shuffle.
type isolatedIamConfig struct {
identities []*iam_pb.Identity
groups []*iam_pb.Group
policies Policies
}
func newIsolatedIamServer() *IamApiServer {
return &IamApiServer{s3ApiConfig: &isolatedIamConfig{
policies: Policies{Policies: make(map[string]policy_engine.PolicyDocument)},
}}
}
func (m *isolatedIamConfig) GetS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) error {
_ = copier.Copy(&s3cfg.Identities, &m.identities)
_ = copier.Copy(&s3cfg.Groups, &m.groups)
return nil
}
func (m *isolatedIamConfig) PutS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) error {
_ = copier.Copy(&m.identities, &s3cfg.Identities)
_ = copier.Copy(&m.groups, &s3cfg.Groups)
return nil
}
func (m *isolatedIamConfig) GetPolicies(policies *Policies) error {
_ = copier.Copy(&policies, &m.policies)
return nil
}
func (m *isolatedIamConfig) PutPolicies(policies *Policies) error {
_ = copier.Copy(&m.policies, &policies)
return nil
}
// TestCreatePolicyVersion reproduces issue #9785: the AWS Terraform provider
// updates a managed policy in place via CreatePolicyVersion, which previously
// returned 501 NotImplemented. The whole read/update surface Terraform relies on
// is exercised here: GetPolicy (DefaultVersionId), CreatePolicyVersion,
// GetPolicyVersion and ListPolicyVersions.
func TestCreatePolicyVersion(t *testing.T) {
srv := newIsolatedIamServer()
svc := iam.New(session.New())
policyName := "tf-managed-policy"
policyArn := aws.String("arn:aws:iam:::policy/" + policyName)
// Create the managed policy.
createReq, _ := svc.CreatePolicyRequest(&iam.CreatePolicyInput{
PolicyName: aws.String(policyName),
PolicyDocument: aws.String(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:Get*","s3:List*"],"Resource":["arn:aws:s3:::EXAMPLE-BUCKET"]}]}`),
})
_ = createReq.Build()
resp, err := executeRequestWith(srv, createReq.HTTPRequest, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.Code)
// Update it in place (what Terraform does). This used to return 501.
cpvReq, _ := svc.CreatePolicyVersionRequest(&iam.CreatePolicyVersionInput{
PolicyArn: policyArn,
PolicyDocument: aws.String(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:Get*"],"Resource":["arn:aws:s3:::EXAMPLE-BUCKET"]}]}`),
SetAsDefault: aws.Bool(true),
})
_ = cpvReq.Build()
resp, err = executeRequestWith(srv, cpvReq.HTTPRequest, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.Code)
var cpvResp CreatePolicyVersionResponse
require.NoError(t, xml.Unmarshal(resp.Body.Bytes(), &cpvResp))
require.NotNil(t, cpvResp.CreatePolicyVersionResult.PolicyVersion.VersionId)
assert.Equal(t, "v1", *cpvResp.CreatePolicyVersionResult.PolicyVersion.VersionId)
require.NotNil(t, cpvResp.CreatePolicyVersionResult.PolicyVersion.IsDefaultVersion)
assert.True(t, *cpvResp.CreatePolicyVersionResult.PolicyVersion.IsDefaultVersion)
// GetPolicy must advertise a default version so Terraform's read can chain
// into GetPolicyVersion.
gpReq, _ := svc.GetPolicyRequest(&iam.GetPolicyInput{PolicyArn: policyArn})
_ = gpReq.Build()
resp, err = executeRequestWith(srv, gpReq.HTTPRequest, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.Code)
var gpResp GetPolicyResponse
require.NoError(t, xml.Unmarshal(resp.Body.Bytes(), &gpResp))
require.NotNil(t, gpResp.GetPolicyResult.Policy.DefaultVersionId)
assert.Equal(t, "v1", *gpResp.GetPolicyResult.Policy.DefaultVersionId)
// GetPolicyVersion must return the updated document (Get* only, List* gone).
gpvReq, _ := svc.GetPolicyVersionRequest(&iam.GetPolicyVersionInput{PolicyArn: policyArn, VersionId: aws.String("v1")})
_ = gpvReq.Build()
resp, err = executeRequestWith(srv, gpvReq.HTTPRequest, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.Code)
var gpvResp GetPolicyVersionResponse
require.NoError(t, xml.Unmarshal(resp.Body.Bytes(), &gpvResp))
require.NotNil(t, gpvResp.GetPolicyVersionResult.PolicyVersion.Document)
assert.Contains(t, *gpvResp.GetPolicyVersionResult.PolicyVersion.Document, "s3:Get*")
assert.NotContains(t, *gpvResp.GetPolicyVersionResult.PolicyVersion.Document, "s3:List*")
// ListPolicyVersions must report exactly the single default version.
lpvReq, _ := svc.ListPolicyVersionsRequest(&iam.ListPolicyVersionsInput{PolicyArn: policyArn})
_ = lpvReq.Build()
resp, err = executeRequestWith(srv, lpvReq.HTTPRequest, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.Code)
var lpvResp ListPolicyVersionsResponse
require.NoError(t, xml.Unmarshal(resp.Body.Bytes(), &lpvResp))
require.Len(t, lpvResp.ListPolicyVersionsResult.Versions, 1)
assert.Equal(t, "v1", *lpvResp.ListPolicyVersionsResult.Versions[0].VersionId)
}
// TestCreatePolicyVersionMissingPolicy verifies a NoSuchEntity (404) when the
// target policy does not exist, matching AWS.
func TestCreatePolicyVersionMissingPolicy(t *testing.T) {
srv := newIsolatedIamServer()
req, _ := iam.New(session.New()).CreatePolicyVersionRequest(&iam.CreatePolicyVersionInput{
PolicyArn: aws.String("arn:aws:iam:::policy/does-not-exist"),
PolicyDocument: aws.String(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:Get*"],"Resource":["arn:aws:s3:::EXAMPLE-BUCKET"]}]}`),
SetAsDefault: aws.Bool(true),
})
_ = req.Build()
resp, err := executeRequestWith(srv, req.HTTPRequest, nil)
require.NoError(t, err)
assert.Equal(t, http.StatusNotFound, resp.Code)
code, _ := extractErrorCodeAndMessage(resp)
assert.Equal(t, "NoSuchEntity", code)
}
// TestCreatePolicyVersionRequiresSetAsDefault verifies that, given the
// single-version model, a request that does not set SetAsDefault=true is
// rejected rather than silently overwriting the active document.
func TestCreatePolicyVersionRequiresSetAsDefault(t *testing.T) {
srv := newIsolatedIamServer()
svc := iam.New(session.New())
policyArn := aws.String("arn:aws:iam:::policy/tf-managed-policy")
createReq, _ := svc.CreatePolicyRequest(&iam.CreatePolicyInput{
PolicyName: aws.String("tf-managed-policy"),
PolicyDocument: aws.String(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:Get*"],"Resource":["arn:aws:s3:::EXAMPLE-BUCKET"]}]}`),
})
_ = createReq.Build()
resp, err := executeRequestWith(srv, createReq.HTTPRequest, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.Code)
// SetAsDefault omitted (defaults to false) must be rejected.
cpvReq, _ := svc.CreatePolicyVersionRequest(&iam.CreatePolicyVersionInput{
PolicyArn: policyArn,
PolicyDocument: aws.String(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:Put*"],"Resource":["arn:aws:s3:::EXAMPLE-BUCKET"]}]}`),
})
_ = cpvReq.Build()
resp, err = executeRequestWith(srv, cpvReq.HTTPRequest, nil)
require.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, resp.Code)
code, _ := extractErrorCodeAndMessage(resp)
assert.Equal(t, "InvalidInput", code)
// The live document must be untouched (still Get*, not Put*).
gpvReq, _ := svc.GetPolicyVersionRequest(&iam.GetPolicyVersionInput{PolicyArn: policyArn, VersionId: aws.String("v1")})
_ = gpvReq.Build()
resp, err = executeRequestWith(srv, gpvReq.HTTPRequest, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.Code)
var gpvResp GetPolicyVersionResponse
require.NoError(t, xml.Unmarshal(resp.Body.Bytes(), &gpvResp))
require.NotNil(t, gpvResp.GetPolicyVersionResult.PolicyVersion.Document)
assert.Contains(t, *gpvResp.GetPolicyVersionResult.PolicyVersion.Document, "s3:Get*")
assert.NotContains(t, *gpvResp.GetPolicyVersionResult.PolicyVersion.Document, "s3:Put*")
}
func TestPutUserPolicy(t *testing.T) {
userName := aws.String("Test")
params := &iam.PutUserPolicyInput{
+99 -1
View File
@@ -96,6 +96,8 @@ type (
iamGetPolicyResponse = iamlib.GetPolicyResponse
iamListPolicyVersionsResponse = iamlib.ListPolicyVersionsResponse
iamGetPolicyVersionResponse = iamlib.GetPolicyVersionResponse
iamCreatePolicyVersionResponse = iamlib.CreatePolicyVersionResponse
iamDeletePolicyVersionResponse = iamlib.DeletePolicyVersionResponse
iamCreateUserResponse = iamlib.CreateUserResponse
iamDeleteUserResponse = iamlib.DeleteUserResponse
iamGetUserResponse = iamlib.GetUserResponse
@@ -829,6 +831,87 @@ func (e *EmbeddedIamApi) GetPolicyVersion(ctx context.Context, values url.Values
return resp, nil
}
// CreatePolicyVersion replaces a managed policy's document with a new version.
// SeaweedFS keeps a single current document per managed policy (no version
// history), so the new document always becomes version "v1" / the default. This
// is what the AWS Terraform provider calls to update an aws_iam_policy in place.
func (e *EmbeddedIamApi) CreatePolicyVersion(ctx context.Context, values url.Values) (*iamCreatePolicyVersionResponse, *iamError) {
resp := &iamCreatePolicyVersionResponse{}
policyName, err := iamPolicyNameFromArn(values.Get("PolicyArn"))
if err != nil {
return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: err}
}
policyDocumentString := values.Get("PolicyDocument")
if policyDocumentString == "" {
return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("PolicyDocument is required")}
}
// SeaweedFS stores a single, always-default managed policy version. On AWS,
// SetAsDefault=false stages a non-default version without activating it; we
// can't honor that, so reject it rather than silently changing permissions.
if !strings.EqualFold(values.Get("SetAsDefault"), "true") {
return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("SetAsDefault must be true: SeaweedFS stores a single managed policy version")}
}
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}
}
if _, err := e.getActions(&policyDocument); err != nil {
return resp, &iamError{Code: iam.ErrCodeMalformedPolicyDocumentException, Error: err}
}
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.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)}
}
if err := e.credentialManager.UpdatePolicy(ctx, policyName, policyDocument); err != nil {
return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
versionID := "v1"
isDefaultVersion := true
document := policyDocumentString
resp.CreatePolicyVersionResult.PolicyVersion = iam.PolicyVersion{
VersionId: &versionID,
IsDefaultVersion: &isDefaultVersion,
Document: &document,
}
return resp, nil
}
// DeletePolicyVersion is accepted for API completeness. With a single stored
// version that is always the default, the only valid responses are NoSuchEntity
// (unknown version) and the AWS "cannot delete the default version" conflict.
func (e *EmbeddedIamApi) DeletePolicyVersion(ctx context.Context, values url.Values) (*iamDeletePolicyVersionResponse, *iamError) {
resp := &iamDeletePolicyVersionResponse{}
policyName, err := iamPolicyNameFromArn(values.Get("PolicyArn"))
if err != nil {
return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: err}
}
versionID := values.Get("VersionId")
if versionID == "" {
return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("VersionId is required")}
}
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)}
}
if versionID == "v1" {
return resp, &iamError{Code: iam.ErrCodeDeleteConflictException, Error: fmt.Errorf("cannot delete the default version of policy %s", policyName)}
}
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy version %s not found", versionID)}
}
func iamPolicyNameFromArn(policyArn string) (string, error) {
const policyPathDelimiter = ":policy/"
idx := strings.Index(policyArn, policyPathDelimiter)
@@ -2680,6 +2763,21 @@ func (e *EmbeddedIamApi) ExecuteAction(ctx context.Context, values url.Values, s
return nil, iamErr
}
changed = false
case "CreatePolicyVersion":
var iamErr *iamError
response, iamErr = e.CreatePolicyVersion(ctx, values)
if iamErr != nil {
glog.Errorf("CreatePolicyVersion: %+v", iamErr.Error)
return nil, iamErr
}
changed = false
case "DeletePolicyVersion":
var iamErr *iamError
response, iamErr = e.DeletePolicyVersion(ctx, values)
if iamErr != nil {
return nil, iamErr
}
changed = false
case "SetUserStatus":
var iamErr *iamError
response, iamErr = e.SetUserStatus(s3cfg, values)
@@ -2832,7 +2930,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 action == "AttachUserPolicy" || action == "DetachUserPolicy" || action == "CreatePolicy" || action == "DeletePolicy" || action == "CreateUser" || action == "PutGroupPolicy" || action == "DeleteGroupPolicy" {
} else if action == "AttachUserPolicy" || action == "DetachUserPolicy" || action == "CreatePolicy" || action == "CreatePolicyVersion" || action == "DeletePolicy" || action == "CreateUser" || action == "PutGroupPolicy" || action == "DeleteGroupPolicy" {
// 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 {
+92
View File
@@ -416,6 +416,98 @@ func TestEmbeddedIamCreatePolicy(t *testing.T) {
assert.NotNil(t, out.CreatePolicyResult.Policy.PolicyId)
}
// TestEmbeddedIamCreatePolicyVersion covers the embedded IAM path for issue
// #9785: updating a managed policy in place via CreatePolicyVersion (used by the
// AWS Terraform provider) instead of returning 501 NotImplemented.
func TestEmbeddedIamCreatePolicyVersion(t *testing.T) {
api := NewEmbeddedIamApiForTest()
svc := iam.New(session.New())
policyArn := aws.String("arn:aws:iam:::policy/tf-managed")
// Create the managed policy.
createReq, _ := svc.CreatePolicyRequest(&iam.CreatePolicyInput{
PolicyName: aws.String("tf-managed"),
PolicyDocument: aws.String(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:Get*","s3:List*"],"Resource":["arn:aws:s3:::EXAMPLE-BUCKET"]}]}`),
})
_ = createReq.Build()
resp, err := executeEmbeddedIamRequest(api, createReq.HTTPRequest, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.Code)
// Update it in place. This used to return 501 NotImplemented.
cpvReq, _ := svc.CreatePolicyVersionRequest(&iam.CreatePolicyVersionInput{
PolicyArn: policyArn,
PolicyDocument: aws.String(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:Get*"],"Resource":["arn:aws:s3:::EXAMPLE-BUCKET"]}]}`),
SetAsDefault: aws.Bool(true),
})
_ = cpvReq.Build()
out := iamCreatePolicyVersionResponse{}
resp, err = executeEmbeddedIamRequest(api, cpvReq.HTTPRequest, &out)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.Code)
require.NotNil(t, out.CreatePolicyVersionResult.PolicyVersion.VersionId)
assert.Equal(t, "v1", *out.CreatePolicyVersionResult.PolicyVersion.VersionId)
require.NotNil(t, out.CreatePolicyVersionResult.PolicyVersion.IsDefaultVersion)
assert.True(t, *out.CreatePolicyVersionResult.PolicyVersion.IsDefaultVersion)
// The stored document must reflect the update.
gpvReq, _ := svc.GetPolicyVersionRequest(&iam.GetPolicyVersionInput{PolicyArn: policyArn, VersionId: aws.String("v1")})
_ = gpvReq.Build()
gpvOut := iamGetPolicyVersionResponse{}
resp, err = executeEmbeddedIamRequest(api, gpvReq.HTTPRequest, &gpvOut)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.Code)
require.NotNil(t, gpvOut.GetPolicyVersionResult.PolicyVersion.Document)
assert.Contains(t, *gpvOut.GetPolicyVersionResult.PolicyVersion.Document, "s3:Get*")
assert.NotContains(t, *gpvOut.GetPolicyVersionResult.PolicyVersion.Document, "s3:List*")
}
// TestEmbeddedIamCreatePolicyVersionMissingPolicy verifies NoSuchEntity when the
// target policy does not exist.
func TestEmbeddedIamCreatePolicyVersionMissingPolicy(t *testing.T) {
api := NewEmbeddedIamApiForTest()
req, _ := iam.New(session.New()).CreatePolicyVersionRequest(&iam.CreatePolicyVersionInput{
PolicyArn: aws.String("arn:aws:iam:::policy/does-not-exist"),
PolicyDocument: aws.String(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:Get*"],"Resource":["arn:aws:s3:::EXAMPLE-BUCKET"]}]}`),
SetAsDefault: aws.Bool(true),
})
_ = req.Build()
resp, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
require.NoError(t, err)
assert.Equal(t, http.StatusNotFound, resp.Code)
code, _ := extractEmbeddedIamErrorCodeAndMessage(resp)
assert.Equal(t, "NoSuchEntity", code)
}
// TestEmbeddedIamCreatePolicyVersionRequiresSetAsDefault verifies the embedded
// path also rejects a non-default version request given the single-version model.
func TestEmbeddedIamCreatePolicyVersionRequiresSetAsDefault(t *testing.T) {
api := NewEmbeddedIamApiForTest()
svc := iam.New(session.New())
policyArn := aws.String("arn:aws:iam:::policy/tf-managed")
createReq, _ := svc.CreatePolicyRequest(&iam.CreatePolicyInput{
PolicyName: aws.String("tf-managed"),
PolicyDocument: aws.String(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:Get*"],"Resource":["arn:aws:s3:::EXAMPLE-BUCKET"]}]}`),
})
_ = createReq.Build()
resp, err := executeEmbeddedIamRequest(api, createReq.HTTPRequest, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.Code)
// SetAsDefault omitted must be rejected.
cpvReq, _ := svc.CreatePolicyVersionRequest(&iam.CreatePolicyVersionInput{
PolicyArn: policyArn,
PolicyDocument: aws.String(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:Put*"],"Resource":["arn:aws:s3:::EXAMPLE-BUCKET"]}]}`),
})
_ = cpvReq.Build()
resp, err = executeEmbeddedIamRequest(api, cpvReq.HTTPRequest, nil)
require.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, resp.Code)
code, _ := extractEmbeddedIamErrorCodeAndMessage(resp)
assert.Equal(t, "InvalidInput", code)
}
// TestEmbeddedIamPutUserPolicy tests attaching a policy to a user
func TestEmbeddedIamPutUserPolicy(t *testing.T) {
api := NewEmbeddedIamApiForTest()