S3 IAM: Added ListPolicyVersions and GetPolicyVersion support (#8395)
* test(s3/iam): add managed policy CRUD lifecycle integration coverage * s3/iam: add ListPolicyVersions and GetPolicyVersion support * test(s3/iam): cover ListPolicyVersions and GetPolicyVersion
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||||
"github.com/aws/aws-sdk-go/service/iam"
|
"github.com/aws/aws-sdk-go/service/iam"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -202,6 +203,116 @@ func TestIAMPolicyManagement(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("managed_policy_crud_lifecycle", func(t *testing.T) {
|
||||||
|
policyDoc := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::*"}]}`
|
||||||
|
|
||||||
|
policyNames := []string{"test-managed-policy-lifecycle-a", "test-managed-policy-lifecycle-b"}
|
||||||
|
policyArns := make([]*string, 0, len(policyNames))
|
||||||
|
for _, policyName := range policyNames {
|
||||||
|
createResp, err := iamClient.CreatePolicy(&iam.CreatePolicyInput{
|
||||||
|
PolicyName: aws.String(policyName),
|
||||||
|
PolicyDocument: aws.String(policyDoc),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
policyArns = append(policyArns, createResp.Policy.Arn)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
for _, policyArn := range policyArns {
|
||||||
|
_, _ = iamClient.DeletePolicy(&iam.DeletePolicyInput{PolicyArn: policyArn})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
listResp, err := iamClient.ListPolicies(&iam.ListPoliciesInput{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
foundByName := map[string]bool{}
|
||||||
|
for _, policy := range listResp.Policies {
|
||||||
|
if policy.PolicyName != nil {
|
||||||
|
foundByName[*policy.PolicyName] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, policyName := range policyNames {
|
||||||
|
assert.True(t, foundByName[policyName], "policy %s should be listed", policyName)
|
||||||
|
}
|
||||||
|
|
||||||
|
getResp, err := iamClient.GetPolicy(&iam.GetPolicyInput{PolicyArn: policyArns[0]})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, getResp.Policy)
|
||||||
|
assert.Equal(t, policyNames[0], aws.StringValue(getResp.Policy.PolicyName))
|
||||||
|
assert.Equal(t, aws.StringValue(policyArns[0]), aws.StringValue(getResp.Policy.Arn))
|
||||||
|
|
||||||
|
_, err = iamClient.DeletePolicy(&iam.DeletePolicyInput{PolicyArn: policyArns[0]})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = iamClient.GetPolicy(&iam.GetPolicyInput{PolicyArn: policyArns[0]})
|
||||||
|
require.Error(t, err)
|
||||||
|
awsErr, ok := err.(awserr.Error)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, iam.ErrCodeNoSuchEntityException, awsErr.Code())
|
||||||
|
|
||||||
|
listAfterDeleteResp, err := iamClient.ListPolicies(&iam.ListPoliciesInput{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
deletedPolicyFound := false
|
||||||
|
remainingPolicyFound := false
|
||||||
|
for _, policy := range listAfterDeleteResp.Policies {
|
||||||
|
if policy.PolicyName == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if *policy.PolicyName == policyNames[0] {
|
||||||
|
deletedPolicyFound = true
|
||||||
|
}
|
||||||
|
if *policy.PolicyName == policyNames[1] {
|
||||||
|
remainingPolicyFound = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.False(t, deletedPolicyFound, "deleted policy should no longer be listed")
|
||||||
|
assert.True(t, remainingPolicyFound, "remaining policy should still be listed")
|
||||||
|
|
||||||
|
policyArns[0] = nil
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("managed_policy_versions", func(t *testing.T) {
|
||||||
|
policyName := "test-managed-policy-version"
|
||||||
|
policyDoc := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:ListBucket","Resource":"*"}]}`
|
||||||
|
|
||||||
|
createResp, err := iamClient.CreatePolicy(&iam.CreatePolicyInput{
|
||||||
|
PolicyName: aws.String(policyName),
|
||||||
|
PolicyDocument: aws.String(policyDoc),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_, _ = iamClient.DeletePolicy(&iam.DeletePolicyInput{PolicyArn: createResp.Policy.Arn})
|
||||||
|
})
|
||||||
|
|
||||||
|
listVersionsResp, err := iamClient.ListPolicyVersions(&iam.ListPolicyVersionsInput{
|
||||||
|
PolicyArn: createResp.Policy.Arn,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, listVersionsResp.Versions)
|
||||||
|
assert.Equal(t, "v1", aws.StringValue(listVersionsResp.Versions[0].VersionId))
|
||||||
|
assert.Equal(t, true, aws.BoolValue(listVersionsResp.Versions[0].IsDefaultVersion))
|
||||||
|
|
||||||
|
getVersionResp, err := iamClient.GetPolicyVersion(&iam.GetPolicyVersionInput{
|
||||||
|
PolicyArn: createResp.Policy.Arn,
|
||||||
|
VersionId: aws.String("v1"),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, getVersionResp.PolicyVersion)
|
||||||
|
assert.Equal(t, "v1", aws.StringValue(getVersionResp.PolicyVersion.VersionId))
|
||||||
|
assert.Contains(t, aws.StringValue(getVersionResp.PolicyVersion.Document), "s3:ListBucket")
|
||||||
|
|
||||||
|
_, err = iamClient.GetPolicyVersion(&iam.GetPolicyVersionInput{
|
||||||
|
PolicyArn: createResp.Policy.Arn,
|
||||||
|
VersionId: aws.String("v2"),
|
||||||
|
})
|
||||||
|
require.Error(t, err)
|
||||||
|
awsErr, ok := err.(awserr.Error)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, iam.ErrCodeNoSuchEntityException, awsErr.Code())
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("user_inline_policy", func(t *testing.T) {
|
t.Run("user_inline_policy", func(t *testing.T) {
|
||||||
userName := "test-user-policy"
|
userName := "test-user-policy"
|
||||||
_, err := iamClient.CreateUser(&iam.CreateUserInput{
|
_, err := iamClient.CreateUser(&iam.CreateUserInput{
|
||||||
|
|||||||
@@ -81,6 +81,26 @@ type GetPolicyResponse struct {
|
|||||||
} `xml:"GetPolicyResult"`
|
} `xml:"GetPolicyResult"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListPolicyVersionsResponse is the response for ListPolicyVersions action.
|
||||||
|
type ListPolicyVersionsResponse struct {
|
||||||
|
CommonResponse
|
||||||
|
XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ ListPolicyVersionsResponse"`
|
||||||
|
ListPolicyVersionsResult struct {
|
||||||
|
Versions []*iam.PolicyVersion `xml:"Versions>member"`
|
||||||
|
IsTruncated bool `xml:"IsTruncated"`
|
||||||
|
Marker string `xml:"Marker,omitempty"`
|
||||||
|
} `xml:"ListPolicyVersionsResult"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPolicyVersionResponse is the response for GetPolicyVersion action.
|
||||||
|
type GetPolicyVersionResponse struct {
|
||||||
|
CommonResponse
|
||||||
|
XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ GetPolicyVersionResponse"`
|
||||||
|
GetPolicyVersionResult struct {
|
||||||
|
PolicyVersion iam.PolicyVersion `xml:"PolicyVersion"`
|
||||||
|
} `xml:"GetPolicyVersionResult"`
|
||||||
|
}
|
||||||
|
|
||||||
// CreateUserResponse is the response for CreateUser action.
|
// CreateUserResponse is the response for CreateUser action.
|
||||||
type CreateUserResponse struct {
|
type CreateUserResponse struct {
|
||||||
CommonResponse
|
CommonResponse
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ type (
|
|||||||
iamDeletePolicyResponse = iamlib.DeletePolicyResponse
|
iamDeletePolicyResponse = iamlib.DeletePolicyResponse
|
||||||
iamListPoliciesResponse = iamlib.ListPoliciesResponse
|
iamListPoliciesResponse = iamlib.ListPoliciesResponse
|
||||||
iamGetPolicyResponse = iamlib.GetPolicyResponse
|
iamGetPolicyResponse = iamlib.GetPolicyResponse
|
||||||
|
iamListPolicyVersionsResponse = iamlib.ListPolicyVersionsResponse
|
||||||
|
iamGetPolicyVersionResponse = iamlib.GetPolicyVersionResponse
|
||||||
iamCreateUserResponse = iamlib.CreateUserResponse
|
iamCreateUserResponse = iamlib.CreateUserResponse
|
||||||
iamDeleteUserResponse = iamlib.DeleteUserResponse
|
iamDeleteUserResponse = iamlib.DeleteUserResponse
|
||||||
iamGetUserResponse = iamlib.GetUserResponse
|
iamGetUserResponse = iamlib.GetUserResponse
|
||||||
@@ -577,6 +579,82 @@ func (e *EmbeddedIamApi) GetPolicy(ctx context.Context, values url.Values) (iamG
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListPolicyVersions lists versions for a managed policy.
|
||||||
|
// Current SeaweedFS implementation stores one version per policy (v1).
|
||||||
|
func (e *EmbeddedIamApi) ListPolicyVersions(ctx context.Context, values url.Values) (iamListPolicyVersionsResponse, *iamError) {
|
||||||
|
var resp iamListPolicyVersionsResponse
|
||||||
|
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)}
|
||||||
|
}
|
||||||
|
|
||||||
|
versionID := "v1"
|
||||||
|
isDefaultVersion := true
|
||||||
|
createDate := time.Now().UTC()
|
||||||
|
resp.ListPolicyVersionsResult.Versions = []*iam.PolicyVersion{{
|
||||||
|
VersionId: &versionID,
|
||||||
|
IsDefaultVersion: &isDefaultVersion,
|
||||||
|
CreateDate: &createDate,
|
||||||
|
}}
|
||||||
|
resp.ListPolicyVersionsResult.IsTruncated = false
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPolicyVersion returns the document for a specific policy version.
|
||||||
|
// Current SeaweedFS implementation stores one version per policy (v1).
|
||||||
|
func (e *EmbeddedIamApi) GetPolicyVersion(ctx context.Context, values url.Values) (iamGetPolicyVersionResponse, *iamError) {
|
||||||
|
var resp iamGetPolicyVersionResponse
|
||||||
|
policyArn := values.Get("PolicyArn")
|
||||||
|
versionID := values.Get("VersionId")
|
||||||
|
if versionID == "" {
|
||||||
|
return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("VersionId is required")}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)}
|
||||||
|
}
|
||||||
|
if versionID != "v1" {
|
||||||
|
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy version %s not found", versionID)}
|
||||||
|
}
|
||||||
|
policyDocumentJSON, err := json.Marshal(policy)
|
||||||
|
if err != nil {
|
||||||
|
return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
isDefaultVersion := true
|
||||||
|
createDate := time.Now().UTC()
|
||||||
|
document := string(policyDocumentJSON)
|
||||||
|
resp.GetPolicyVersionResult.PolicyVersion = iam.PolicyVersion{
|
||||||
|
VersionId: &versionID,
|
||||||
|
IsDefaultVersion: &isDefaultVersion,
|
||||||
|
CreateDate: &createDate,
|
||||||
|
Document: &document,
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
func iamPolicyNameFromArn(policyArn string) (string, error) {
|
func iamPolicyNameFromArn(policyArn string) (string, error) {
|
||||||
const policyPathDelimiter = ":policy/"
|
const policyPathDelimiter = ":policy/"
|
||||||
idx := strings.Index(policyArn, policyPathDelimiter)
|
idx := strings.Index(policyArn, policyPathDelimiter)
|
||||||
@@ -1453,7 +1531,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", "ListPolicies", "GetPolicy", "ListServiceAccounts", "GetServiceAccount":
|
case "ListUsers", "ListAccessKeys", "GetUser", "GetUserPolicy", "ListAttachedUserPolicies", "ListPolicies", "GetPolicy", "ListPolicyVersions", "GetPolicyVersion", "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")}
|
||||||
@@ -1570,6 +1648,18 @@ func (e *EmbeddedIamApi) ExecuteAction(ctx context.Context, values url.Values, s
|
|||||||
return nil, iamErr
|
return nil, iamErr
|
||||||
}
|
}
|
||||||
changed = false
|
changed = false
|
||||||
|
case "ListPolicyVersions":
|
||||||
|
response, iamErr = e.ListPolicyVersions(ctx, values)
|
||||||
|
if iamErr != nil {
|
||||||
|
return nil, iamErr
|
||||||
|
}
|
||||||
|
changed = false
|
||||||
|
case "GetPolicyVersion":
|
||||||
|
response, iamErr = e.GetPolicyVersion(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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user