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:
Chris Lu
2026-02-20 11:04:18 -08:00
committed by GitHub
parent 964a8f5fde
commit bd0b1fe9d5
3 changed files with 222 additions and 1 deletions

View File

@@ -81,6 +81,26 @@ type GetPolicyResponse struct {
} `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.
type CreateUserResponse struct {
CommonResponse

View File

@@ -75,6 +75,8 @@ type (
iamDeletePolicyResponse = iamlib.DeletePolicyResponse
iamListPoliciesResponse = iamlib.ListPoliciesResponse
iamGetPolicyResponse = iamlib.GetPolicyResponse
iamListPolicyVersionsResponse = iamlib.ListPolicyVersionsResponse
iamGetPolicyVersionResponse = iamlib.GetPolicyVersionResponse
iamCreateUserResponse = iamlib.CreateUserResponse
iamDeleteUserResponse = iamlib.DeleteUserResponse
iamGetUserResponse = iamlib.GetUserResponse
@@ -577,6 +579,82 @@ func (e *EmbeddedIamApi) GetPolicy(ctx context.Context, values url.Values) (iamG
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) {
const policyPathDelimiter = ":policy/"
idx := strings.Index(policyArn, policyPathDelimiter)
@@ -1453,7 +1531,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", "ListPolicies", "GetPolicy", "ListServiceAccounts", "GetServiceAccount":
case "ListUsers", "ListAccessKeys", "GetUser", "GetUserPolicy", "ListAttachedUserPolicies", "ListPolicies", "GetPolicy", "ListPolicyVersions", "GetPolicyVersion", "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")}
@@ -1570,6 +1648,18 @@ func (e *EmbeddedIamApi) ExecuteAction(ctx context.Context, values url.Values, s
return nil, iamErr
}
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":
response, iamErr = e.SetUserStatus(s3cfg, values)
if iamErr != nil {