mirror of
https://github.com/versity/versitygw.git
synced 2026-07-23 16:42:51 +00:00
feat: add IAM role inline policy CRUD
Add support for the `PutRolePolicy`, `GetRolePolicy`, `DeleteRolePolicy`, and `ListRolePolicies` actions in the IAM-compatible gateway service, extending role management with the same inline-policy lifecycle already available for IAM users. `PutRolePolicy` validates the policy name and document, parses the document for AWS-compatible syntax and semantic errors (missing actions/resources, malformed ARNs, disallowed principals, duplicate statement IDs, and so on), and rejects documents once the role's aggregate inline-policy size would exceed `MaxInlinePolicyBytesPerRole` (10240 bytes, distinct from the 2048-byte quota enforced for users). Putting a policy under an existing name overwrites its document in place. `GetRolePolicy` and `DeleteRolePolicy` look up or remove a named inline policy from a role, returning a `NoSuchEntity` error when the role or the policy is not found. `ListRolePolicies` returns a role's inline policy names in sorted order with marker-based pagination. These actions are implemented for both the internal file-backed store and the Vault-backed store, wired into the IAM API router, and given their own XML response types under `iamapi/types`. A new `NoSuchEntityRolePolicy` error was added to `iamapi/iamerr` to mirror the existing user-policy error.
This commit is contained in:
@@ -718,3 +718,133 @@ func (c IAMApiController) UpdateAssumeRolePolicy(ctx fiber.Ctx) (*Response, erro
|
||||
|
||||
return &Response{Data: &types.UpdateAssumeRolePolicyResponse{}}, nil
|
||||
}
|
||||
|
||||
func (c IAMApiController) PutRolePolicy(ctx fiber.Ctx) (*Response, error) {
|
||||
policyDocument, ok := iamutil.RequestParam(ctx, "PolicyDocument")
|
||||
if !ok {
|
||||
debuglogger.Logf("missing required PutRolePolicy parameter: PolicyDocument")
|
||||
return nil, iamerr.MissingValue("policyDocument")
|
||||
}
|
||||
if err := policy.Validate("policyDocument", policyDocument); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
policyName, ok := iamutil.RequestParam(ctx, "PolicyName")
|
||||
if !ok {
|
||||
debuglogger.Logf("missing required PutRolePolicy parameter: PolicyName")
|
||||
return nil, iamerr.MissingValue("policyName")
|
||||
}
|
||||
if err := iamutil.ValidateName("policyName", policyName, iamutil.MaxUserLookupLen); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roleName, err := iamutil.GetRoleName(ctx, "PutRolePolicy", iamutil.MaxUserLookupLen, iamerr.MissingValue("roleName"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Confirm the role exists before inspecting policy document content
|
||||
if _, err := c.store.GetRole(ctx.Context(), roleName); err != nil {
|
||||
debuglogger.Logf("failed to get IAM role %q for PutRolePolicy: %v", roleName, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := policy.Parse(policyDocument); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := c.store.PutRolePolicy(ctx.Context(), storage.PutRolePolicyInput{
|
||||
RoleName: roleName,
|
||||
PolicyName: policyName,
|
||||
PolicyDocument: policyDocument,
|
||||
}); err != nil {
|
||||
debuglogger.Logf("failed to put IAM role policy %q for role %q: %v", policyName, roleName, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Response{Data: &types.PutRolePolicyResponse{}}, nil
|
||||
}
|
||||
|
||||
func (c IAMApiController) GetRolePolicy(ctx fiber.Ctx) (*Response, error) {
|
||||
policyName, ok := iamutil.RequestParam(ctx, "PolicyName")
|
||||
if !ok {
|
||||
debuglogger.Logf("missing required GetRolePolicy parameter: PolicyName")
|
||||
return nil, iamerr.MissingValue("policyName")
|
||||
}
|
||||
if err := iamutil.ValidateName("policyName", policyName, iamutil.MaxUserLookupLen); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roleName, err := iamutil.GetRoleName(ctx, "GetRolePolicy", iamutil.MaxUserLookupLen, iamerr.MissingValue("roleName"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entry, err := c.store.GetRolePolicy(ctx.Context(), roleName, policyName)
|
||||
if err != nil {
|
||||
debuglogger.Logf("failed to get IAM role policy %q for role %q: %v", policyName, roleName, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Response{Data: &types.GetRolePolicyResponse{
|
||||
Result: types.GetRolePolicyResult{
|
||||
RoleName: roleName,
|
||||
PolicyName: entry.PolicyName,
|
||||
PolicyDocument: iamutil.EncodePolicyDocument(entry.PolicyDocument),
|
||||
},
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (c IAMApiController) DeleteRolePolicy(ctx fiber.Ctx) (*Response, error) {
|
||||
policyName, ok := iamutil.RequestParam(ctx, "PolicyName")
|
||||
if !ok {
|
||||
debuglogger.Logf("missing required DeleteRolePolicy parameter: PolicyName")
|
||||
return nil, iamerr.MissingValue("policyName")
|
||||
}
|
||||
if err := iamutil.ValidateName("policyName", policyName, iamutil.MaxUserLookupLen); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roleName, err := iamutil.GetRoleName(ctx, "DeleteRolePolicy", iamutil.MaxUserLookupLen, iamerr.MissingValue("roleName"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := c.store.DeleteRolePolicy(ctx.Context(), roleName, policyName); err != nil {
|
||||
debuglogger.Logf("failed to delete IAM role policy %q for role %q: %v", policyName, roleName, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Response{Data: &types.DeleteRolePolicyResponse{}}, nil
|
||||
}
|
||||
|
||||
func (c IAMApiController) ListRolePolicies(ctx fiber.Ctx) (*Response, error) {
|
||||
roleName, err := iamutil.GetRoleName(ctx, "ListRolePolicies", iamutil.MaxUserLookupLen, iamerr.MissingValue("roleName"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
maxItems, err := iamutil.ParseMaxItems(ctx, "ListRolePolicies")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
marker, _ := iamutil.RequestParam(ctx, "Marker")
|
||||
out, err := c.store.ListRolePolicies(ctx.Context(), storage.ListRolePoliciesInput{
|
||||
RoleName: roleName,
|
||||
Marker: marker,
|
||||
MaxItems: maxItems,
|
||||
})
|
||||
if err != nil {
|
||||
debuglogger.Logf("failed to list IAM role policies for role %q: %v", roleName, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Response{Data: &types.ListRolePoliciesResponse{
|
||||
Result: types.ListRolePoliciesResult{
|
||||
PolicyNames: types.PolicyNameList{Members: out.PolicyNames},
|
||||
IsTruncated: out.IsTruncated,
|
||||
Marker: out.Marker,
|
||||
},
|
||||
}}, nil
|
||||
}
|
||||
|
||||
@@ -1301,6 +1301,376 @@ func TestIAMApiControllerDeleteAndUpdateAssumeRolePolicyErrors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIAMApiControllerRolePolicyLifecycle(t *testing.T) {
|
||||
server := newIAMControllerTestServer(t)
|
||||
|
||||
createRole := doIAMAction(t, server, url.Values{
|
||||
"Action": {"CreateRole"},
|
||||
"RoleName": {"my-role"},
|
||||
"AssumeRolePolicyDocument": {validTrustPolicy},
|
||||
})
|
||||
if createRole.StatusCode != http.StatusOK {
|
||||
t.Fatalf("CreateRole status = %d, body=%s", createRole.StatusCode, readBody(t, createRole))
|
||||
}
|
||||
|
||||
policyDoc := `{"Version": "2012-10-17", "Statement": [{"Effect": "Allow", "Action": "s3:GetObject", "Resource": "*"}]}`
|
||||
|
||||
put := doIAMAction(t, server, url.Values{
|
||||
"Action": {"PutRolePolicy"},
|
||||
"RoleName": {"my-role"},
|
||||
"PolicyName": {"ReadOnly"},
|
||||
"PolicyDocument": {policyDoc},
|
||||
})
|
||||
if put.StatusCode != http.StatusOK {
|
||||
t.Fatalf("PutRolePolicy status = %d, body=%s", put.StatusCode, readBody(t, put))
|
||||
}
|
||||
var putOut iamtypes.PutRolePolicyResponse
|
||||
unmarshalXML(t, readBody(t, put), &putOut)
|
||||
if putOut.XMLName.Space != "https://iam.amazonaws.com/doc/2010-05-08/" || putOut.XMLName.Local != "PutRolePolicyResponse" {
|
||||
t.Fatalf("PutRolePolicy XMLName = %#v", putOut.XMLName)
|
||||
}
|
||||
if putOut.ResponseMetadata.RequestID == "" {
|
||||
t.Fatal("PutRolePolicy missing RequestId")
|
||||
}
|
||||
|
||||
get := doIAMAction(t, server, url.Values{
|
||||
"Action": {"GetRolePolicy"},
|
||||
"RoleName": {"my-role"},
|
||||
"PolicyName": {"ReadOnly"},
|
||||
})
|
||||
if get.StatusCode != http.StatusOK {
|
||||
t.Fatalf("GetRolePolicy status = %d, body=%s", get.StatusCode, readBody(t, get))
|
||||
}
|
||||
var getOut iamtypes.GetRolePolicyResponse
|
||||
unmarshalXML(t, readBody(t, get), &getOut)
|
||||
if getOut.Result.RoleName != "my-role" || getOut.Result.PolicyName != "ReadOnly" {
|
||||
t.Fatalf("GetRolePolicy result = %#v", getOut.Result)
|
||||
}
|
||||
if !strings.Contains(getOut.Result.PolicyDocument, "%20") {
|
||||
t.Fatalf("GetRolePolicy PolicyDocument = %q, want RFC 3986 percent-encoding (%%20 for space)", getOut.Result.PolicyDocument)
|
||||
}
|
||||
decoded, err := url.QueryUnescape(getOut.Result.PolicyDocument)
|
||||
if err != nil {
|
||||
t.Fatalf("QueryUnescape: %v", err)
|
||||
}
|
||||
if decoded != policyDoc {
|
||||
t.Fatalf("GetRolePolicy PolicyDocument = %q, want verbatim %q", decoded, policyDoc)
|
||||
}
|
||||
|
||||
list := doIAMAction(t, server, url.Values{
|
||||
"Action": {"ListRolePolicies"},
|
||||
"RoleName": {"my-role"},
|
||||
})
|
||||
if list.StatusCode != http.StatusOK {
|
||||
t.Fatalf("ListRolePolicies status = %d, body=%s", list.StatusCode, readBody(t, list))
|
||||
}
|
||||
var listOut iamtypes.ListRolePoliciesResponse
|
||||
unmarshalXML(t, readBody(t, list), &listOut)
|
||||
if len(listOut.Result.PolicyNames.Members) != 1 || listOut.Result.PolicyNames.Members[0] != "ReadOnly" {
|
||||
t.Fatalf("ListRolePolicies = %#v, want [ReadOnly]", listOut.Result.PolicyNames.Members)
|
||||
}
|
||||
if listOut.Result.IsTruncated {
|
||||
t.Fatal("ListRolePolicies IsTruncated = true, want false")
|
||||
}
|
||||
|
||||
// Re-Put-ing the same PolicyName replaces it rather than erroring or
|
||||
// stacking toward the aggregate size quota.
|
||||
overwritePut := doIAMAction(t, server, url.Values{
|
||||
"Action": {"PutRolePolicy"},
|
||||
"RoleName": {"my-role"},
|
||||
"PolicyName": {"ReadOnly"},
|
||||
"PolicyDocument": {`{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"s3:DeleteObject","Resource":"*"}]}`},
|
||||
})
|
||||
if overwritePut.StatusCode != http.StatusOK {
|
||||
t.Fatalf("overwrite PutRolePolicy status = %d, body=%s", overwritePut.StatusCode, readBody(t, overwritePut))
|
||||
}
|
||||
overwriteGet := doIAMAction(t, server, url.Values{
|
||||
"Action": {"GetRolePolicy"},
|
||||
"RoleName": {"my-role"},
|
||||
"PolicyName": {"ReadOnly"},
|
||||
})
|
||||
var overwriteOut iamtypes.GetRolePolicyResponse
|
||||
unmarshalXML(t, readBody(t, overwriteGet), &overwriteOut)
|
||||
overwriteDecoded, err := url.QueryUnescape(overwriteOut.Result.PolicyDocument)
|
||||
if err != nil {
|
||||
t.Fatalf("QueryUnescape: %v", err)
|
||||
}
|
||||
if !strings.Contains(overwriteDecoded, "Deny") {
|
||||
t.Fatalf("GetRolePolicy after overwrite = %q, want the Deny statement", overwriteDecoded)
|
||||
}
|
||||
|
||||
del := doIAMAction(t, server, url.Values{
|
||||
"Action": {"DeleteRolePolicy"},
|
||||
"RoleName": {"my-role"},
|
||||
"PolicyName": {"ReadOnly"},
|
||||
})
|
||||
if del.StatusCode != http.StatusOK {
|
||||
t.Fatalf("DeleteRolePolicy status = %d, body=%s", del.StatusCode, readBody(t, del))
|
||||
}
|
||||
var delOut iamtypes.DeleteRolePolicyResponse
|
||||
unmarshalXML(t, readBody(t, del), &delOut)
|
||||
if delOut.XMLName.Local != "DeleteRolePolicyResponse" || delOut.ResponseMetadata.RequestID == "" {
|
||||
t.Fatalf("DeleteRolePolicy output = %#v", delOut)
|
||||
}
|
||||
|
||||
missing := doIAMAction(t, server, url.Values{
|
||||
"Action": {"GetRolePolicy"},
|
||||
"RoleName": {"my-role"},
|
||||
"PolicyName": {"ReadOnly"},
|
||||
})
|
||||
requireIAMError(t, missing, http.StatusNotFound, "Sender", "NoSuchEntity", "The role policy with name ReadOnly cannot be found.")
|
||||
|
||||
// A second delete of the same (now-gone) policy is a hard error, not an
|
||||
// idempotent success.
|
||||
doubleDelete := doIAMAction(t, server, url.Values{
|
||||
"Action": {"DeleteRolePolicy"},
|
||||
"RoleName": {"my-role"},
|
||||
"PolicyName": {"ReadOnly"},
|
||||
})
|
||||
requireIAMError(t, doubleDelete, http.StatusNotFound, "Sender", "NoSuchEntity", "The role policy with name ReadOnly cannot be found.")
|
||||
}
|
||||
|
||||
func TestIAMApiControllerRolePolicyValidationErrors(t *testing.T) {
|
||||
validDoc := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}`
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
setupRole bool
|
||||
params url.Values
|
||||
status int
|
||||
code string
|
||||
message string
|
||||
}{
|
||||
{
|
||||
name: "put missing policy document",
|
||||
setupRole: true,
|
||||
params: url.Values{"Action": {"PutRolePolicy"}, "RoleName": {"my-role"}, "PolicyName": {"P"}},
|
||||
status: http.StatusBadRequest,
|
||||
code: "ValidationError",
|
||||
message: "1 validation error detected: Value at 'policyDocument' failed to satisfy constraint: Member must not be null",
|
||||
},
|
||||
{
|
||||
name: "put missing policy name",
|
||||
setupRole: true,
|
||||
params: url.Values{"Action": {"PutRolePolicy"}, "RoleName": {"my-role"}, "PolicyDocument": {validDoc}},
|
||||
status: http.StatusBadRequest,
|
||||
code: "ValidationError",
|
||||
message: "1 validation error detected: Value at 'policyName' failed to satisfy constraint: Member must not be null",
|
||||
},
|
||||
{
|
||||
name: "put missing role name",
|
||||
params: url.Values{"Action": {"PutRolePolicy"}, "PolicyName": {"P"}, "PolicyDocument": {validDoc}},
|
||||
status: http.StatusBadRequest,
|
||||
code: "ValidationError",
|
||||
message: "1 validation error detected: Value at 'roleName' failed to satisfy constraint: Member must not be null",
|
||||
},
|
||||
{
|
||||
name: "put invalid policy name characters",
|
||||
setupRole: true,
|
||||
params: url.Values{"Action": {"PutRolePolicy"}, "RoleName": {"my-role"}, "PolicyName": {"bad/name"}, "PolicyDocument": {validDoc}},
|
||||
status: http.StatusBadRequest,
|
||||
code: "ValidationError",
|
||||
message: "The specified value for policyName is invalid. It must contain only alphanumeric characters and/or the following: +=,.@_-",
|
||||
},
|
||||
{
|
||||
name: "put long policy name",
|
||||
setupRole: true,
|
||||
params: url.Values{"Action": {"PutRolePolicy"}, "RoleName": {"my-role"}, "PolicyName": {strings.Repeat("p", 129)}, "PolicyDocument": {validDoc}},
|
||||
status: http.StatusBadRequest,
|
||||
code: "ValidationError",
|
||||
message: "1 validation error detected: Value at 'policyName' failed to satisfy constraint: Member must have length less than or equal to 128",
|
||||
},
|
||||
{
|
||||
name: "put non-ascii policy document",
|
||||
setupRole: true,
|
||||
params: url.Values{"Action": {"PutRolePolicy"}, "RoleName": {"my-role"}, "PolicyName": {"P"}, "PolicyDocument": {"emoji\U0001F600test"}},
|
||||
status: http.StatusBadRequest,
|
||||
code: "ValidationError",
|
||||
message: "The specified value for policyDocument is invalid. It must contain only printable ASCII characters.",
|
||||
},
|
||||
{
|
||||
name: "put role does not exist",
|
||||
params: url.Values{"Action": {"PutRolePolicy"}, "RoleName": {"nonexistent"}, "PolicyName": {"P"}, "PolicyDocument": {validDoc}},
|
||||
status: http.StatusNotFound,
|
||||
code: "NoSuchEntity",
|
||||
message: "The role with name nonexistent cannot be found.",
|
||||
},
|
||||
{
|
||||
name: "put nonexistent role wins over malformed document",
|
||||
params: url.Values{"Action": {"PutRolePolicy"}, "RoleName": {"nonexistent"}, "PolicyName": {"P"}, "PolicyDocument": {"{not valid json"}},
|
||||
status: http.StatusNotFound,
|
||||
code: "NoSuchEntity",
|
||||
message: "The role with name nonexistent cannot be found.",
|
||||
},
|
||||
{
|
||||
name: "put malformed policy document",
|
||||
setupRole: true,
|
||||
params: url.Values{"Action": {"PutRolePolicy"}, "RoleName": {"my-role"}, "PolicyName": {"P"}, "PolicyDocument": {"{not valid json"}},
|
||||
status: http.StatusBadRequest,
|
||||
code: "MalformedPolicyDocument",
|
||||
message: "Syntax errors in policy.",
|
||||
},
|
||||
{
|
||||
name: "put policy document with principal",
|
||||
setupRole: true,
|
||||
params: url.Values{"Action": {"PutRolePolicy"}, "RoleName": {"my-role"}, "PolicyName": {"P"}, "PolicyDocument": {
|
||||
`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":"*","Action":"s3:GetObject","Resource":"*"}]}`,
|
||||
}},
|
||||
status: http.StatusBadRequest,
|
||||
code: "MalformedPolicyDocument",
|
||||
message: "Policy document should not specify a principal.",
|
||||
},
|
||||
{
|
||||
name: "get role does not exist",
|
||||
params: url.Values{"Action": {"GetRolePolicy"}, "RoleName": {"nonexistent"}, "PolicyName": {"P"}},
|
||||
status: http.StatusNotFound,
|
||||
code: "NoSuchEntity",
|
||||
message: "The role with name nonexistent cannot be found.",
|
||||
},
|
||||
{
|
||||
name: "get policy does not exist",
|
||||
setupRole: true,
|
||||
params: url.Values{"Action": {"GetRolePolicy"}, "RoleName": {"my-role"}, "PolicyName": {"NoSuchPolicy"}},
|
||||
status: http.StatusNotFound,
|
||||
code: "NoSuchEntity",
|
||||
message: "The role policy with name NoSuchPolicy cannot be found.",
|
||||
},
|
||||
{
|
||||
name: "delete role does not exist",
|
||||
params: url.Values{"Action": {"DeleteRolePolicy"}, "RoleName": {"nonexistent"}, "PolicyName": {"P"}},
|
||||
status: http.StatusNotFound,
|
||||
code: "NoSuchEntity",
|
||||
message: "The role with name nonexistent cannot be found.",
|
||||
},
|
||||
{
|
||||
name: "delete policy does not exist",
|
||||
setupRole: true,
|
||||
params: url.Values{"Action": {"DeleteRolePolicy"}, "RoleName": {"my-role"}, "PolicyName": {"NoSuchPolicy"}},
|
||||
status: http.StatusNotFound,
|
||||
code: "NoSuchEntity",
|
||||
message: "The role policy with name NoSuchPolicy cannot be found.",
|
||||
},
|
||||
{
|
||||
name: "list role does not exist",
|
||||
params: url.Values{"Action": {"ListRolePolicies"}, "RoleName": {"nonexistent"}},
|
||||
status: http.StatusNotFound,
|
||||
code: "NoSuchEntity",
|
||||
message: "The role with name nonexistent cannot be found.",
|
||||
},
|
||||
{
|
||||
name: "list max items too large",
|
||||
setupRole: true,
|
||||
params: url.Values{"Action": {"ListRolePolicies"}, "RoleName": {"my-role"}, "MaxItems": {"1001"}},
|
||||
status: http.StatusBadRequest,
|
||||
code: "ValidationError",
|
||||
message: "1 validation error detected: Value '1001' at 'maxItems' failed to satisfy constraint: Member must have value between 1 and 1000",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
server := newIAMControllerTestServer(t)
|
||||
if tt.setupRole {
|
||||
resp := doIAMAction(t, server, url.Values{
|
||||
"Action": {"CreateRole"},
|
||||
"RoleName": {"my-role"},
|
||||
"AssumeRolePolicyDocument": {validTrustPolicy},
|
||||
})
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("CreateRole status = %d, body=%s", resp.StatusCode, readBody(t, resp))
|
||||
}
|
||||
}
|
||||
resp := doIAMAction(t, server, tt.params)
|
||||
requireIAMError(t, resp, tt.status, "Sender", tt.code, tt.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIAMApiControllerDeleteRolePolicyConflict(t *testing.T) {
|
||||
server := newIAMControllerTestServer(t)
|
||||
|
||||
create := doIAMAction(t, server, url.Values{
|
||||
"Action": {"CreateRole"},
|
||||
"RoleName": {"my-role"},
|
||||
"AssumeRolePolicyDocument": {validTrustPolicy},
|
||||
})
|
||||
if create.StatusCode != http.StatusOK {
|
||||
t.Fatalf("CreateRole status = %d, body=%s", create.StatusCode, readBody(t, create))
|
||||
}
|
||||
put := doIAMAction(t, server, url.Values{
|
||||
"Action": {"PutRolePolicy"},
|
||||
"RoleName": {"my-role"},
|
||||
"PolicyName": {"P"},
|
||||
"PolicyDocument": {`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}`},
|
||||
})
|
||||
if put.StatusCode != http.StatusOK {
|
||||
t.Fatalf("PutRolePolicy status = %d, body=%s", put.StatusCode, readBody(t, put))
|
||||
}
|
||||
|
||||
deleteRole := doIAMAction(t, server, url.Values{"Action": {"DeleteRole"}, "RoleName": {"my-role"}})
|
||||
requireIAMError(t, deleteRole, http.StatusConflict, "Sender", "DeleteConflict", "Cannot delete entity, must delete policies first.")
|
||||
|
||||
delPolicy := doIAMAction(t, server, url.Values{"Action": {"DeleteRolePolicy"}, "RoleName": {"my-role"}, "PolicyName": {"P"}})
|
||||
if delPolicy.StatusCode != http.StatusOK {
|
||||
t.Fatalf("DeleteRolePolicy status = %d, body=%s", delPolicy.StatusCode, readBody(t, delPolicy))
|
||||
}
|
||||
|
||||
deleteRoleAfter := doIAMAction(t, server, url.Values{"Action": {"DeleteRole"}, "RoleName": {"my-role"}})
|
||||
if deleteRoleAfter.StatusCode != http.StatusOK {
|
||||
t.Fatalf("DeleteRole status = %d, body=%s", deleteRoleAfter.StatusCode, readBody(t, deleteRoleAfter))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIAMApiControllerPutRolePolicyOversizedDocument(t *testing.T) {
|
||||
// A >131072 byte PolicyDocument does not fit in a GET query string
|
||||
// against this test server's header/URL read-buffer limit, matching
|
||||
// real IAM's own guidance to use POST rather than GET for large
|
||||
// policy documents - so this one case is exercised over POST directly
|
||||
// rather than through the doIAMAction GET helper used elsewhere.
|
||||
server := newIAMControllerTestServer(t)
|
||||
create := doIAMAction(t, server, url.Values{
|
||||
"Action": {"CreateRole"},
|
||||
"RoleName": {"my-role"},
|
||||
"AssumeRolePolicyDocument": {validTrustPolicy},
|
||||
})
|
||||
if create.StatusCode != http.StatusOK {
|
||||
t.Fatalf("CreateRole status = %d, body=%s", create.StatusCode, readBody(t, create))
|
||||
}
|
||||
|
||||
resp := doIAMActionPost(t, server, url.Values{
|
||||
"Action": {"PutRolePolicy"},
|
||||
"RoleName": {"my-role"},
|
||||
"PolicyName": {"P"},
|
||||
"PolicyDocument": {strings.Repeat("x", 131073)},
|
||||
})
|
||||
requireIAMError(t, resp, http.StatusBadRequest, "Sender", "ValidationError",
|
||||
"1 validation error detected: Value at 'policyDocument' failed to satisfy constraint: Member must have length less than or equal to 131072")
|
||||
}
|
||||
|
||||
func TestIAMApiControllerPutRolePolicyExceedsQuota(t *testing.T) {
|
||||
// The role's aggregate inline-policy quota (10240 bytes) is well over
|
||||
// this test server's GET header/URL read-buffer limit, so this case
|
||||
// is exercised over POST, same as TestIAMApiControllerPutRolePolicyOversizedDocument.
|
||||
server := newIAMControllerTestServer(t)
|
||||
create := doIAMAction(t, server, url.Values{
|
||||
"Action": {"CreateRole"},
|
||||
"RoleName": {"my-role"},
|
||||
"AssumeRolePolicyDocument": {validTrustPolicy},
|
||||
})
|
||||
if create.StatusCode != http.StatusOK {
|
||||
t.Fatalf("CreateRole status = %d, body=%s", create.StatusCode, readBody(t, create))
|
||||
}
|
||||
|
||||
oversizedDoc := `{"Version":"2012-10-17","Statement":[{"Sid":"` + strings.Repeat("x", 10300) + `","Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}`
|
||||
resp := doIAMActionPost(t, server, url.Values{
|
||||
"Action": {"PutRolePolicy"},
|
||||
"RoleName": {"my-role"},
|
||||
"PolicyName": {"P"},
|
||||
"PolicyDocument": {oversizedDoc},
|
||||
})
|
||||
requireIAMError(t, resp, http.StatusConflict, "Sender", "LimitExceeded", "Maximum policy size of 10240 bytes exceeded for role my-role")
|
||||
}
|
||||
|
||||
func newIAMControllerTestServer(t *testing.T) *IAMApiServer {
|
||||
t.Helper()
|
||||
|
||||
|
||||
@@ -453,6 +453,10 @@ func NoSuchEntityUserPolicy(userName, policyName string) Error {
|
||||
return newSenderError("NoSuchEntity", fmt.Sprintf("The user policy with name %s cannot be found.", policyName), http.StatusNotFound)
|
||||
}
|
||||
|
||||
func NoSuchEntityRolePolicy(roleName, policyName string) Error {
|
||||
return newSenderError("NoSuchEntity", fmt.Sprintf("The role policy with name %s cannot be found.", policyName), http.StatusNotFound)
|
||||
}
|
||||
|
||||
func InlinePolicyQuotaExceeded(entityKind, entityName string, maxBytes int) Error {
|
||||
return newSenderError("LimitExceeded", fmt.Sprintf("Maximum policy size of %d bytes exceeded for %s %s", maxBytes, entityKind, entityName), http.StatusConflict)
|
||||
}
|
||||
|
||||
@@ -68,6 +68,11 @@ func (r *IAMApiRouter) Init() {
|
||||
"ListRoles": ctrl.ListRoles,
|
||||
"DeleteRole": ctrl.DeleteRole,
|
||||
"UpdateAssumeRolePolicy": ctrl.UpdateAssumeRolePolicy,
|
||||
// Role Inline Policy CRUD
|
||||
"PutRolePolicy": ctrl.PutRolePolicy,
|
||||
"GetRolePolicy": ctrl.GetRolePolicy,
|
||||
"DeleteRolePolicy": ctrl.DeleteRolePolicy,
|
||||
"ListRolePolicies": ctrl.ListRolePolicies,
|
||||
}
|
||||
|
||||
actionRoute := ProcessHandlers(r.routeAction, iammiddleware.VerifyIAMAuth(r.rootCreds))
|
||||
|
||||
@@ -816,6 +816,159 @@ func (s *InternalStore) UpdateAssumeRolePolicy(_ context.Context, input UpdateAs
|
||||
return cloneRole(updated), nil
|
||||
}
|
||||
|
||||
func (s *InternalStore) PutRolePolicy(_ context.Context, input PutRolePolicyInput) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
err := s.engine.StoreIAM(func(data []byte) ([]byte, error) {
|
||||
conf, err := s.engine.ParseIAM(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
canonical, role, ok := lookupRole(conf, input.RoleName)
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityRole(input.RoleName)
|
||||
}
|
||||
|
||||
now := time.Now().UTC().Truncate(time.Second)
|
||||
newTotal := len(input.PolicyDocument)
|
||||
replaceAt := -1
|
||||
for i, p := range role.Policies.Inline {
|
||||
if p.PolicyName == input.PolicyName {
|
||||
replaceAt = i
|
||||
continue
|
||||
}
|
||||
newTotal += len(p.PolicyDocument)
|
||||
}
|
||||
if newTotal > MaxInlinePolicyBytesPerRole {
|
||||
return nil, iamerr.InlinePolicyQuotaExceeded("role", input.RoleName, MaxInlinePolicyBytesPerRole)
|
||||
}
|
||||
|
||||
if replaceAt >= 0 {
|
||||
role.Policies.Inline[replaceAt].PolicyDocument = input.PolicyDocument
|
||||
role.Policies.Inline[replaceAt].UpdateDate = now
|
||||
} else {
|
||||
role.Policies.Inline = append(role.Policies.Inline, types.PolicyEntry{
|
||||
PolicyName: input.PolicyName,
|
||||
PolicyDocument: input.PolicyDocument,
|
||||
CreateDate: now,
|
||||
UpdateDate: now,
|
||||
})
|
||||
}
|
||||
|
||||
conf.Roles[canonical] = role
|
||||
return json.Marshal(conf)
|
||||
})
|
||||
return unwrapAPIError(err)
|
||||
}
|
||||
|
||||
func (s *InternalStore) GetRolePolicy(_ context.Context, roleName, policyName string) (*types.PolicyEntry, error) {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
conf, err := s.engine.GetIAM()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, role, ok := lookupRole(conf, roleName)
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityRole(roleName)
|
||||
}
|
||||
|
||||
for _, p := range role.Policies.Inline {
|
||||
if p.PolicyName == policyName {
|
||||
cloned := p
|
||||
return &cloned, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, iamerr.NoSuchEntityRolePolicy(roleName, policyName)
|
||||
}
|
||||
|
||||
func (s *InternalStore) DeleteRolePolicy(_ context.Context, roleName, policyName string) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
err := s.engine.StoreIAM(func(data []byte) ([]byte, error) {
|
||||
conf, err := s.engine.ParseIAM(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
canonical, role, ok := lookupRole(conf, roleName)
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityRole(roleName)
|
||||
}
|
||||
|
||||
idx := -1
|
||||
for i, p := range role.Policies.Inline {
|
||||
if p.PolicyName == policyName {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if idx == -1 {
|
||||
return nil, iamerr.NoSuchEntityRolePolicy(roleName, policyName)
|
||||
}
|
||||
|
||||
role.Policies.Inline = slices.Delete(role.Policies.Inline, idx, idx+1)
|
||||
conf.Roles[canonical] = role
|
||||
return json.Marshal(conf)
|
||||
})
|
||||
return unwrapAPIError(err)
|
||||
}
|
||||
|
||||
func (s *InternalStore) ListRolePolicies(_ context.Context, input ListRolePoliciesInput) (*ListRolePoliciesOutput, error) {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
conf, err := s.engine.GetIAM()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, role, ok := lookupRole(conf, input.RoleName)
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityRole(input.RoleName)
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(role.Policies.Inline))
|
||||
for _, p := range role.Policies.Inline {
|
||||
names = append(names, p.PolicyName)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
start := 0
|
||||
if input.Marker != "" {
|
||||
start = len(names)
|
||||
for i, name := range names {
|
||||
if name == input.Marker {
|
||||
start = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
names = names[start:]
|
||||
|
||||
limit := len(names)
|
||||
if input.MaxItems > 0 && int(input.MaxItems) < limit {
|
||||
limit = int(input.MaxItems)
|
||||
}
|
||||
|
||||
out := &ListRolePoliciesOutput{
|
||||
PolicyNames: make([]string, limit),
|
||||
}
|
||||
copy(out.PolicyNames, names[:limit])
|
||||
if limit < len(names) {
|
||||
out.IsTruncated = true
|
||||
out.Marker = out.PolicyNames[limit-1]
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func cloneUser(user types.User) *types.User {
|
||||
cloned := user
|
||||
cloned.Tags = slices.Clone(user.Tags)
|
||||
|
||||
@@ -33,6 +33,10 @@ const MaxAccessKeysPerUser = 2
|
||||
// all of a single IAM user's inline policy documents combined
|
||||
const MaxInlinePolicyBytesPerUser = 2048
|
||||
|
||||
// MaxInlinePolicyBytesPerRole is the maximum aggregate size, in bytes, of
|
||||
// all of a single IAM role's inline policy documents combined
|
||||
const MaxInlinePolicyBytesPerRole = 10240
|
||||
|
||||
var (
|
||||
ErrUserIDAlreadyExists = errors.New("iamapi: user id already exists")
|
||||
ErrAccessKeyIDAlreadyExists = errors.New("iamapi: access key id already exists")
|
||||
@@ -126,6 +130,24 @@ type UpdateAssumeRolePolicyInput struct {
|
||||
PolicyDocument string
|
||||
}
|
||||
|
||||
type PutRolePolicyInput struct {
|
||||
RoleName string
|
||||
PolicyName string
|
||||
PolicyDocument string
|
||||
}
|
||||
|
||||
type ListRolePoliciesInput struct {
|
||||
RoleName string
|
||||
Marker string
|
||||
MaxItems int32
|
||||
}
|
||||
|
||||
type ListRolePoliciesOutput struct {
|
||||
PolicyNames []string
|
||||
IsTruncated bool
|
||||
Marker string
|
||||
}
|
||||
|
||||
// Storer is the IAM API storage backend contract.
|
||||
type Storer interface {
|
||||
CreateUser(ctx context.Context, user types.User) (*types.User, error)
|
||||
@@ -150,6 +172,11 @@ type Storer interface {
|
||||
ListRoles(ctx context.Context, input ListRolesInput) (*ListRolesOutput, error)
|
||||
DeleteRole(ctx context.Context, roleName string) error
|
||||
UpdateAssumeRolePolicy(ctx context.Context, input UpdateAssumeRolePolicyInput) (*types.Role, error)
|
||||
|
||||
PutRolePolicy(ctx context.Context, input PutRolePolicyInput) error
|
||||
GetRolePolicy(ctx context.Context, roleName, policyName string) (*types.PolicyEntry, error)
|
||||
DeleteRolePolicy(ctx context.Context, roleName, policyName string) error
|
||||
ListRolePolicies(ctx context.Context, input ListRolePoliciesInput) (*ListRolePoliciesOutput, error)
|
||||
}
|
||||
|
||||
func unwrapAPIError(err error) error {
|
||||
|
||||
@@ -384,3 +384,130 @@ func TestInternalStoreRoleCRUDAndPagination(t *testing.T) {
|
||||
t.Fatalf("DeleteRole missing err = %v, want NoSuchEntity", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInternalStoreRolePolicyCRUD(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
dir := t.TempDir()
|
||||
store, err := NewInternal(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("NewInternal: %v", err)
|
||||
}
|
||||
|
||||
if _, err := store.CreateRole(ctx, types.Role{
|
||||
RoleName: "alice-role",
|
||||
RoleID: "AROA22222222222222222",
|
||||
AssumeRolePolicyDocument: `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"*"},"Action":"sts:AssumeRole"}]}`,
|
||||
}); err != nil {
|
||||
t.Fatalf("CreateRole: %v", err)
|
||||
}
|
||||
|
||||
if err := store.PutRolePolicy(ctx, PutRolePolicyInput{
|
||||
RoleName: "ALICE-ROLE",
|
||||
PolicyName: "ReadOnly",
|
||||
PolicyDocument: `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}`,
|
||||
}); err != nil {
|
||||
t.Fatalf("PutRolePolicy: %v", err)
|
||||
}
|
||||
if err := store.PutRolePolicy(ctx, PutRolePolicyInput{RoleName: "missing-role", PolicyName: "P", PolicyDocument: "{}"}); !errors.Is(err, iamerr.NoSuchEntityRole("missing-role")) {
|
||||
t.Fatalf("PutRolePolicy missing role err = %v, want NoSuchEntity", err)
|
||||
}
|
||||
|
||||
entry, err := store.GetRolePolicy(ctx, "alice-role", "ReadOnly")
|
||||
if err != nil {
|
||||
t.Fatalf("GetRolePolicy: %v", err)
|
||||
}
|
||||
if entry.PolicyDocument != `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}` {
|
||||
t.Fatalf("GetRolePolicy document = %q", entry.PolicyDocument)
|
||||
}
|
||||
if entry.CreateDate.IsZero() || entry.UpdateDate.IsZero() {
|
||||
t.Fatalf("GetRolePolicy CreateDate/UpdateDate zero: %#v", entry)
|
||||
}
|
||||
if _, err := store.GetRolePolicy(ctx, "alice-role", "NoSuchPolicy"); !errors.Is(err, iamerr.NoSuchEntityRolePolicy("alice-role", "NoSuchPolicy")) {
|
||||
t.Fatalf("GetRolePolicy missing policy err = %v, want NoSuchEntity", err)
|
||||
}
|
||||
if _, err := store.GetRolePolicy(ctx, "missing-role", "P"); !errors.Is(err, iamerr.NoSuchEntityRole("missing-role")) {
|
||||
t.Fatalf("GetRolePolicy missing role err = %v, want NoSuchEntity", err)
|
||||
}
|
||||
|
||||
// Overwriting an existing PolicyName replaces its document rather than
|
||||
// stacking toward the aggregate size quota.
|
||||
if err := store.PutRolePolicy(ctx, PutRolePolicyInput{
|
||||
RoleName: "alice-role",
|
||||
PolicyName: "ReadOnly",
|
||||
PolicyDocument: `{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"s3:DeleteObject","Resource":"*"}]}`,
|
||||
}); err != nil {
|
||||
t.Fatalf("overwrite PutRolePolicy: %v", err)
|
||||
}
|
||||
overwritten, err := store.GetRolePolicy(ctx, "alice-role", "ReadOnly")
|
||||
if err != nil {
|
||||
t.Fatalf("GetRolePolicy after overwrite: %v", err)
|
||||
}
|
||||
if !strings.Contains(overwritten.PolicyDocument, "Deny") {
|
||||
t.Fatalf("GetRolePolicy after overwrite = %q, want the Deny statement", overwritten.PolicyDocument)
|
||||
}
|
||||
|
||||
// Aggregate inline policy size for a role is capped at
|
||||
// MaxInlinePolicyBytesPerRole (10240), distinct from and larger than
|
||||
// the 2048 byte cap for users.
|
||||
oversized := `{"Version":"2012-10-17","Statement":[{"Sid":"` + strings.Repeat("x", 10300) + `","Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}`
|
||||
if err := store.PutRolePolicy(ctx, PutRolePolicyInput{RoleName: "alice-role", PolicyName: "TooBig", PolicyDocument: oversized}); !errors.Is(err, iamerr.InlinePolicyQuotaExceeded("role", "alice-role", MaxInlinePolicyBytesPerRole)) {
|
||||
t.Fatalf("PutRolePolicy oversized err = %v, want LimitExceeded", err)
|
||||
}
|
||||
|
||||
if err := store.PutRolePolicy(ctx, PutRolePolicyInput{
|
||||
RoleName: "alice-role",
|
||||
PolicyName: "SecondPolicy",
|
||||
PolicyDocument: `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutObject","Resource":"*"}]}`,
|
||||
}); err != nil {
|
||||
t.Fatalf("PutRolePolicy second policy: %v", err)
|
||||
}
|
||||
|
||||
list, err := store.ListRolePolicies(ctx, ListRolePoliciesInput{RoleName: "ALICE-ROLE", MaxItems: 1})
|
||||
if err != nil {
|
||||
t.Fatalf("ListRolePolicies page1: %v", err)
|
||||
}
|
||||
if len(list.PolicyNames) != 1 || list.PolicyNames[0] != "ReadOnly" || !list.IsTruncated || list.Marker != "ReadOnly" {
|
||||
t.Fatalf("ListRolePolicies page1 = %#v, want truncated ReadOnly page", list)
|
||||
}
|
||||
page2, err := store.ListRolePolicies(ctx, ListRolePoliciesInput{RoleName: "alice-role", Marker: list.Marker, MaxItems: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("ListRolePolicies page2: %v", err)
|
||||
}
|
||||
if len(page2.PolicyNames) != 1 || page2.PolicyNames[0] != "SecondPolicy" || page2.IsTruncated {
|
||||
t.Fatalf("ListRolePolicies page2 = %#v, want final SecondPolicy page", page2)
|
||||
}
|
||||
if _, err := store.ListRolePolicies(ctx, ListRolePoliciesInput{RoleName: "missing-role"}); !errors.Is(err, iamerr.NoSuchEntityRole("missing-role")) {
|
||||
t.Fatalf("ListRolePolicies missing role err = %v, want NoSuchEntity", err)
|
||||
}
|
||||
|
||||
// A role with attached inline policies cannot be deleted until they are
|
||||
// all removed first.
|
||||
if err := store.DeleteRole(ctx, "alice-role"); !errors.Is(err, iamerr.GetAPIError(iamerr.ErrDeleteConflictPolicies)) {
|
||||
t.Fatalf("DeleteRole with policies err = %v, want DeleteConflict", err)
|
||||
}
|
||||
|
||||
if err := store.DeleteRolePolicy(ctx, "alice-role", "SecondPolicy"); err != nil {
|
||||
t.Fatalf("DeleteRolePolicy: %v", err)
|
||||
}
|
||||
if err := store.DeleteRolePolicy(ctx, "alice-role", "NoSuchPolicy"); !errors.Is(err, iamerr.NoSuchEntityRolePolicy("alice-role", "NoSuchPolicy")) {
|
||||
t.Fatalf("DeleteRolePolicy missing policy err = %v, want NoSuchEntity", err)
|
||||
}
|
||||
if err := store.DeleteRolePolicy(ctx, "missing-role", "P"); !errors.Is(err, iamerr.NoSuchEntityRole("missing-role")) {
|
||||
t.Fatalf("DeleteRolePolicy missing role err = %v, want NoSuchEntity", err)
|
||||
}
|
||||
|
||||
reopened, err := NewInternal(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("reopen NewInternal: %v", err)
|
||||
}
|
||||
if _, err := reopened.GetRolePolicy(ctx, "alice-role", "ReadOnly"); err != nil {
|
||||
t.Fatalf("GetRolePolicy after reopen: %v", err)
|
||||
}
|
||||
|
||||
if err := reopened.DeleteRolePolicy(ctx, "alice-role", "ReadOnly"); err != nil {
|
||||
t.Fatalf("DeleteRolePolicy: %v", err)
|
||||
}
|
||||
if err := reopened.DeleteRole(ctx, "alice-role"); err != nil {
|
||||
t.Fatalf("DeleteRole after removing all policies: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -940,6 +940,122 @@ func (s *VaultStore) UpdateAssumeRolePolicy(ctx context.Context, input UpdateAss
|
||||
return s.replaceRole(ctx, *role)
|
||||
}
|
||||
|
||||
func (s *VaultStore) PutRolePolicy(ctx context.Context, input PutRolePolicyInput) error {
|
||||
role, err := s.GetRole(ctx, input.RoleName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newTotal := len(input.PolicyDocument)
|
||||
replaceAt := -1
|
||||
for i, p := range role.Policies.Inline {
|
||||
if p.PolicyName == input.PolicyName {
|
||||
replaceAt = i
|
||||
continue
|
||||
}
|
||||
newTotal += len(p.PolicyDocument)
|
||||
}
|
||||
if newTotal > MaxInlinePolicyBytesPerRole {
|
||||
return iamerr.InlinePolicyQuotaExceeded("role", input.RoleName, MaxInlinePolicyBytesPerRole)
|
||||
}
|
||||
|
||||
now := time.Now().UTC().Truncate(time.Second)
|
||||
if replaceAt >= 0 {
|
||||
role.Policies.Inline[replaceAt].PolicyDocument = input.PolicyDocument
|
||||
role.Policies.Inline[replaceAt].UpdateDate = now
|
||||
} else {
|
||||
role.Policies.Inline = append(role.Policies.Inline, types.PolicyEntry{
|
||||
PolicyName: input.PolicyName,
|
||||
PolicyDocument: input.PolicyDocument,
|
||||
CreateDate: now,
|
||||
UpdateDate: now,
|
||||
})
|
||||
}
|
||||
|
||||
_, err = s.replaceRole(ctx, *role)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *VaultStore) GetRolePolicy(ctx context.Context, roleName, policyName string) (*types.PolicyEntry, error) {
|
||||
role, err := s.GetRole(ctx, roleName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, p := range role.Policies.Inline {
|
||||
if p.PolicyName == policyName {
|
||||
cloned := p
|
||||
return &cloned, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, iamerr.NoSuchEntityRolePolicy(roleName, policyName)
|
||||
}
|
||||
|
||||
func (s *VaultStore) DeleteRolePolicy(ctx context.Context, roleName, policyName string) error {
|
||||
role, err := s.GetRole(ctx, roleName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
idx := -1
|
||||
for i, p := range role.Policies.Inline {
|
||||
if p.PolicyName == policyName {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if idx == -1 {
|
||||
return iamerr.NoSuchEntityRolePolicy(roleName, policyName)
|
||||
}
|
||||
|
||||
role.Policies.Inline = slices.Delete(role.Policies.Inline, idx, idx+1)
|
||||
|
||||
_, err = s.replaceRole(ctx, *role)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *VaultStore) ListRolePolicies(ctx context.Context, input ListRolePoliciesInput) (*ListRolePoliciesOutput, error) {
|
||||
role, err := s.GetRole(ctx, input.RoleName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(role.Policies.Inline))
|
||||
for _, p := range role.Policies.Inline {
|
||||
names = append(names, p.PolicyName)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
start := 0
|
||||
if input.Marker != "" {
|
||||
start = len(names)
|
||||
for i, name := range names {
|
||||
if name == input.Marker {
|
||||
start = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
names = names[start:]
|
||||
|
||||
limit := len(names)
|
||||
if input.MaxItems > 0 && int(input.MaxItems) < limit {
|
||||
limit = int(input.MaxItems)
|
||||
}
|
||||
|
||||
out := &ListRolePoliciesOutput{
|
||||
PolicyNames: make([]string, limit),
|
||||
}
|
||||
copy(out.PolicyNames, names[:limit])
|
||||
if limit < len(names) {
|
||||
out.IsTruncated = true
|
||||
out.Marker = out.PolicyNames[limit-1]
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// replaceRole overwrites the stored document for role.RoleName by deleting
|
||||
// all existing versions and recreating with CAS=0.
|
||||
func (s *VaultStore) replaceRole(ctx context.Context, role types.Role) (*types.Role, error) {
|
||||
|
||||
@@ -94,3 +94,53 @@ type ListUserPoliciesResult struct {
|
||||
type PolicyNameList struct {
|
||||
Members []string `xml:"member"`
|
||||
}
|
||||
|
||||
type PutRolePolicyResponse struct {
|
||||
XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ PutRolePolicyResponse"`
|
||||
ResponseMetadata ResponseMetadata
|
||||
}
|
||||
|
||||
func (r *PutRolePolicyResponse) SetRequestID(requestID string) {
|
||||
r.ResponseMetadata.RequestID = requestID
|
||||
}
|
||||
|
||||
type DeleteRolePolicyResponse struct {
|
||||
XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ DeleteRolePolicyResponse"`
|
||||
ResponseMetadata ResponseMetadata
|
||||
}
|
||||
|
||||
func (r *DeleteRolePolicyResponse) SetRequestID(requestID string) {
|
||||
r.ResponseMetadata.RequestID = requestID
|
||||
}
|
||||
|
||||
type GetRolePolicyResponse struct {
|
||||
XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ GetRolePolicyResponse"`
|
||||
Result GetRolePolicyResult `xml:"GetRolePolicyResult"`
|
||||
ResponseMetadata ResponseMetadata
|
||||
}
|
||||
|
||||
func (r *GetRolePolicyResponse) SetRequestID(requestID string) {
|
||||
r.ResponseMetadata.RequestID = requestID
|
||||
}
|
||||
|
||||
type GetRolePolicyResult struct {
|
||||
RoleName string
|
||||
PolicyName string
|
||||
PolicyDocument string
|
||||
}
|
||||
|
||||
type ListRolePoliciesResponse struct {
|
||||
XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ ListRolePoliciesResponse"`
|
||||
Result ListRolePoliciesResult `xml:"ListRolePoliciesResult"`
|
||||
ResponseMetadata ResponseMetadata
|
||||
}
|
||||
|
||||
func (r *ListRolePoliciesResponse) SetRequestID(requestID string) {
|
||||
r.ResponseMetadata.RequestID = requestID
|
||||
}
|
||||
|
||||
type ListRolePoliciesResult struct {
|
||||
PolicyNames PolicyNameList
|
||||
IsTruncated bool
|
||||
Marker string `xml:",omitempty"`
|
||||
}
|
||||
|
||||
@@ -1322,6 +1322,7 @@ func TestIAMDeleteRole(ts *TestState) {
|
||||
ts.Run(IAMDeleteRole_invalid_role_name)
|
||||
ts.Run(IAMDeleteRole_long_role_name)
|
||||
ts.Run(IAMDeleteRole_non_existing_role)
|
||||
ts.Run(IAMDeleteRole_has_policies)
|
||||
ts.Run(IAMDeleteRole_success)
|
||||
}
|
||||
|
||||
@@ -1337,6 +1338,47 @@ func TestIAMUpdateAssumeRolePolicy(ts *TestState) {
|
||||
ts.Run(IAMUpdateAssumeRolePolicy_trust_policy_document_grammar)
|
||||
}
|
||||
|
||||
func TestIAMPutRolePolicy(ts *TestState) {
|
||||
ts.Run(IAMPutRolePolicy_missing_role_name)
|
||||
ts.Run(IAMPutRolePolicy_missing_policy_name)
|
||||
ts.Run(IAMPutRolePolicy_missing_policy_document)
|
||||
ts.Run(IAMPutRolePolicy_invalid_policy_name)
|
||||
ts.Run(IAMPutRolePolicy_long_policy_name)
|
||||
ts.Run(IAMPutRolePolicy_non_ascii_policy_document)
|
||||
ts.Run(IAMPutRolePolicy_non_existing_role)
|
||||
ts.Run(IAMPutRolePolicy_malformed_policy_document)
|
||||
ts.Run(IAMPutRolePolicy_principal_not_allowed)
|
||||
ts.Run(IAMPutRolePolicy_limit_exceeded)
|
||||
ts.Run(IAMPutRolePolicy_success)
|
||||
ts.Run(IAMPutRolePolicy_overwrite_updates_existing)
|
||||
}
|
||||
|
||||
func TestIAMGetRolePolicy(ts *TestState) {
|
||||
ts.Run(IAMGetRolePolicy_missing_role_name)
|
||||
ts.Run(IAMGetRolePolicy_missing_policy_name)
|
||||
ts.Run(IAMGetRolePolicy_non_existing_role)
|
||||
ts.Run(IAMGetRolePolicy_non_existing_policy)
|
||||
ts.Run(IAMGetRolePolicy_success)
|
||||
}
|
||||
|
||||
func TestIAMDeleteRolePolicy(ts *TestState) {
|
||||
ts.Run(IAMDeleteRolePolicy_missing_role_name)
|
||||
ts.Run(IAMDeleteRolePolicy_missing_policy_name)
|
||||
ts.Run(IAMDeleteRolePolicy_non_existing_role)
|
||||
ts.Run(IAMDeleteRolePolicy_non_existing_policy)
|
||||
ts.Run(IAMDeleteRolePolicy_success)
|
||||
ts.Run(IAMDeleteRolePolicy_blocks_role_deletion)
|
||||
}
|
||||
|
||||
func TestIAMListRolePolicies(ts *TestState) {
|
||||
ts.Run(IAMListRolePolicies_missing_role_name)
|
||||
ts.Run(IAMListRolePolicies_non_existing_role)
|
||||
ts.Run(IAMListRolePolicies_invalid_max_items)
|
||||
ts.Run(IAMListRolePolicies_empty_result)
|
||||
ts.Run(IAMListRolePolicies_success)
|
||||
ts.Run(IAMListRolePolicies_pagination)
|
||||
}
|
||||
|
||||
func TestIAM(ts *TestState) {
|
||||
TestIAMAuth(ts)
|
||||
TestIAMQueryAuth(ts)
|
||||
@@ -1359,6 +1401,10 @@ func TestIAM(ts *TestState) {
|
||||
TestIAMListRoles(ts)
|
||||
TestIAMDeleteRole(ts)
|
||||
TestIAMUpdateAssumeRolePolicy(ts)
|
||||
TestIAMPutRolePolicy(ts)
|
||||
TestIAMGetRolePolicy(ts)
|
||||
TestIAMDeleteRolePolicy(ts)
|
||||
TestIAMListRolePolicies(ts)
|
||||
}
|
||||
|
||||
func TestAccessControl(ts *TestState) {
|
||||
@@ -1854,6 +1900,7 @@ func GetIntTests() IntTests {
|
||||
"IAMDeleteRole_invalid_role_name": IAMDeleteRole_invalid_role_name,
|
||||
"IAMDeleteRole_long_role_name": IAMDeleteRole_long_role_name,
|
||||
"IAMDeleteRole_non_existing_role": IAMDeleteRole_non_existing_role,
|
||||
"IAMDeleteRole_has_policies": IAMDeleteRole_has_policies,
|
||||
"IAMDeleteRole_success": IAMDeleteRole_success,
|
||||
"IAMUpdateAssumeRolePolicy_missing_role_name": IAMUpdateAssumeRolePolicy_missing_role_name,
|
||||
"IAMUpdateAssumeRolePolicy_missing_policy_document": IAMUpdateAssumeRolePolicy_missing_policy_document,
|
||||
@@ -1864,6 +1911,35 @@ func GetIntTests() IntTests {
|
||||
"IAMUpdateAssumeRolePolicy_trust_policy_size_limit_exceeded": IAMUpdateAssumeRolePolicy_trust_policy_size_limit_exceeded,
|
||||
"IAMUpdateAssumeRolePolicy_success": IAMUpdateAssumeRolePolicy_success,
|
||||
"IAMUpdateAssumeRolePolicy_trust_policy_document_grammar": IAMUpdateAssumeRolePolicy_trust_policy_document_grammar,
|
||||
"IAMPutRolePolicy_missing_role_name": IAMPutRolePolicy_missing_role_name,
|
||||
"IAMPutRolePolicy_missing_policy_name": IAMPutRolePolicy_missing_policy_name,
|
||||
"IAMPutRolePolicy_missing_policy_document": IAMPutRolePolicy_missing_policy_document,
|
||||
"IAMPutRolePolicy_invalid_policy_name": IAMPutRolePolicy_invalid_policy_name,
|
||||
"IAMPutRolePolicy_long_policy_name": IAMPutRolePolicy_long_policy_name,
|
||||
"IAMPutRolePolicy_non_ascii_policy_document": IAMPutRolePolicy_non_ascii_policy_document,
|
||||
"IAMPutRolePolicy_non_existing_role": IAMPutRolePolicy_non_existing_role,
|
||||
"IAMPutRolePolicy_malformed_policy_document": IAMPutRolePolicy_malformed_policy_document,
|
||||
"IAMPutRolePolicy_principal_not_allowed": IAMPutRolePolicy_principal_not_allowed,
|
||||
"IAMPutRolePolicy_limit_exceeded": IAMPutRolePolicy_limit_exceeded,
|
||||
"IAMPutRolePolicy_success": IAMPutRolePolicy_success,
|
||||
"IAMPutRolePolicy_overwrite_updates_existing": IAMPutRolePolicy_overwrite_updates_existing,
|
||||
"IAMGetRolePolicy_missing_role_name": IAMGetRolePolicy_missing_role_name,
|
||||
"IAMGetRolePolicy_missing_policy_name": IAMGetRolePolicy_missing_policy_name,
|
||||
"IAMGetRolePolicy_non_existing_role": IAMGetRolePolicy_non_existing_role,
|
||||
"IAMGetRolePolicy_non_existing_policy": IAMGetRolePolicy_non_existing_policy,
|
||||
"IAMGetRolePolicy_success": IAMGetRolePolicy_success,
|
||||
"IAMDeleteRolePolicy_missing_role_name": IAMDeleteRolePolicy_missing_role_name,
|
||||
"IAMDeleteRolePolicy_missing_policy_name": IAMDeleteRolePolicy_missing_policy_name,
|
||||
"IAMDeleteRolePolicy_non_existing_role": IAMDeleteRolePolicy_non_existing_role,
|
||||
"IAMDeleteRolePolicy_non_existing_policy": IAMDeleteRolePolicy_non_existing_policy,
|
||||
"IAMDeleteRolePolicy_success": IAMDeleteRolePolicy_success,
|
||||
"IAMDeleteRolePolicy_blocks_role_deletion": IAMDeleteRolePolicy_blocks_role_deletion,
|
||||
"IAMListRolePolicies_missing_role_name": IAMListRolePolicies_missing_role_name,
|
||||
"IAMListRolePolicies_non_existing_role": IAMListRolePolicies_non_existing_role,
|
||||
"IAMListRolePolicies_invalid_max_items": IAMListRolePolicies_invalid_max_items,
|
||||
"IAMListRolePolicies_empty_result": IAMListRolePolicies_empty_result,
|
||||
"IAMListRolePolicies_success": IAMListRolePolicies_success,
|
||||
"IAMListRolePolicies_pagination": IAMListRolePolicies_pagination,
|
||||
"PresignedAuth_security_token_not_supported": PresignedAuth_security_token_not_supported,
|
||||
"PresignedAuth_unsupported_algorithm": PresignedAuth_unsupported_algorithm,
|
||||
"PresignedAuth_ECDSA_not_supported": PresignedAuth_ECDSA_not_supported,
|
||||
|
||||
@@ -67,6 +67,39 @@ func IAMDeleteRole_non_existing_role(s *S3Conf) error {
|
||||
})
|
||||
}
|
||||
|
||||
func IAMDeleteRole_has_policies(s *S3Conf) error {
|
||||
testName := "IAMDeleteRole_has_policies"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := newIAMRoleName()
|
||||
if _, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &roleName,
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := putIAMRolePolicy(client, &iam.PutRolePolicyInput{
|
||||
RoleName: &roleName,
|
||||
PolicyName: aws.String("p"),
|
||||
PolicyDocument: aws.String(validIAMPolicyDocument),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := checkIAMApiErr(deleteIAMRole(client, roleName), iamerr.GetAPIError(iamerr.ErrDeleteConflictPolicies))
|
||||
|
||||
deletePolicyErr := deleteIAMRolePolicy(client, roleName, "p")
|
||||
deleteRoleErr := deleteIAMRole(client, roleName)
|
||||
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
if deletePolicyErr != nil {
|
||||
return deletePolicyErr
|
||||
}
|
||||
return deleteRoleErr
|
||||
})
|
||||
}
|
||||
|
||||
func IAMDeleteRole_success(s *S3Conf) error {
|
||||
testName := "IAMDeleteRole_success"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
// Copyright 2026 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
"github.com/versity/versitygw/iamapi/iamerr"
|
||||
)
|
||||
|
||||
func IAMDeleteRolePolicy_missing_role_name(s *S3Conf) error {
|
||||
testName := "IAMDeleteRolePolicy_missing_role_name"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"DeleteRolePolicy"},
|
||||
"Version": {"2010-05-08"},
|
||||
"PolicyName": {"p"},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingValue("roleName"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMDeleteRolePolicy_missing_policy_name(s *S3Conf) error {
|
||||
testName := "IAMDeleteRolePolicy_missing_policy_name"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"DeleteRolePolicy"},
|
||||
"Version": {"2010-05-08"},
|
||||
"RoleName": {newIAMRoleName()},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingValue("policyName"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMDeleteRolePolicy_non_existing_role(s *S3Conf) error {
|
||||
testName := "IAMDeleteRolePolicy_non_existing_role"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := "non-existing-" + genRandString(16)
|
||||
_, err := deleteIAMRolePolicyRaw(client, &iam.DeleteRolePolicyInput{
|
||||
RoleName: &roleName,
|
||||
PolicyName: aws.String("p"),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.NoSuchEntityRole(roleName))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMDeleteRolePolicy_non_existing_policy(s *S3Conf) error {
|
||||
testName := "IAMDeleteRolePolicy_non_existing_policy"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := newIAMRoleName()
|
||||
if _, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &roleName,
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := checkIAMApiErr(
|
||||
func() error {
|
||||
_, err := deleteIAMRolePolicyRaw(client, &iam.DeleteRolePolicyInput{RoleName: &roleName, PolicyName: aws.String("missing")})
|
||||
return err
|
||||
}(),
|
||||
iamerr.NoSuchEntityRolePolicy(roleName, "missing"),
|
||||
)
|
||||
|
||||
deleteErr := deleteIAMRole(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func IAMDeleteRolePolicy_success(s *S3Conf) error {
|
||||
testName := "IAMDeleteRolePolicy_success"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := newIAMRoleName()
|
||||
if _, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &roleName,
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
if _, err := putIAMRolePolicy(client, &iam.PutRolePolicyInput{
|
||||
RoleName: &roleName,
|
||||
PolicyName: aws.String("p"),
|
||||
PolicyDocument: aws.String(validIAMPolicyDocument),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := deleteIAMRolePolicyRaw(client, &iam.DeleteRolePolicyInput{RoleName: &roleName, PolicyName: aws.String("p")})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if requestID, ok := awsmiddleware.GetRequestIDMetadata(out.ResultMetadata); !ok || requestID == "" {
|
||||
return fmt.Errorf("expected DeleteRolePolicy response request id")
|
||||
}
|
||||
|
||||
_, err = getIAMRolePolicy(client, &iam.GetRolePolicyInput{RoleName: &roleName, PolicyName: aws.String("p")})
|
||||
return checkIAMApiErr(err, iamerr.NoSuchEntityRolePolicy(roleName, "p"))
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMRole(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func IAMDeleteRolePolicy_blocks_role_deletion(s *S3Conf) error {
|
||||
testName := "IAMDeleteRolePolicy_blocks_role_deletion"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := newIAMRoleName()
|
||||
if _, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &roleName,
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := putIAMRolePolicy(client, &iam.PutRolePolicyInput{
|
||||
RoleName: &roleName,
|
||||
PolicyName: aws.String("p"),
|
||||
PolicyDocument: aws.String(validIAMPolicyDocument),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := checkIAMApiErr(deleteIAMRole(client, roleName), iamerr.GetAPIError(iamerr.ErrDeleteConflictPolicies))
|
||||
|
||||
deletePolicyErr := deleteIAMRolePolicy(client, roleName, "p")
|
||||
deleteRoleErr := deleteIAMRole(client, roleName)
|
||||
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
if deletePolicyErr != nil {
|
||||
return deletePolicyErr
|
||||
}
|
||||
return deleteRoleErr
|
||||
})
|
||||
}
|
||||
|
||||
func deleteIAMRolePolicyRaw(client *iam.Client, input *iam.DeleteRolePolicyInput) (*iam.DeleteRolePolicyOutput, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
defer cancel()
|
||||
return client.DeleteRolePolicy(ctx, input)
|
||||
}
|
||||
|
||||
func deleteIAMRolePolicy(client *iam.Client, roleName, policyName string) error {
|
||||
_, err := deleteIAMRolePolicyRaw(client, &iam.DeleteRolePolicyInput{RoleName: &roleName, PolicyName: &policyName})
|
||||
return err
|
||||
}
|
||||
|
||||
// deleteIAMRoleAndPolicies deletes all of the role's inline policies before
|
||||
// deleting the role, since DeleteRole rejects roles with policies still
|
||||
// attached. Use this for test cleanup after a test has created inline
|
||||
// policies.
|
||||
func deleteIAMRoleAndPolicies(client *iam.Client, roleName string) error {
|
||||
out, err := listIAMRolePolicies(client, &iam.ListRolePoliciesInput{RoleName: &roleName})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, policyName := range out.PolicyNames {
|
||||
if err := deleteIAMRolePolicy(client, roleName, policyName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return deleteIAMRole(client, roleName)
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
// Copyright 2026 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
"github.com/versity/versitygw/iamapi/iamerr"
|
||||
)
|
||||
|
||||
func IAMGetRolePolicy_missing_role_name(s *S3Conf) error {
|
||||
testName := "IAMGetRolePolicy_missing_role_name"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"GetRolePolicy"},
|
||||
"Version": {"2010-05-08"},
|
||||
"PolicyName": {"p"},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingValue("roleName"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMGetRolePolicy_missing_policy_name(s *S3Conf) error {
|
||||
testName := "IAMGetRolePolicy_missing_policy_name"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"GetRolePolicy"},
|
||||
"Version": {"2010-05-08"},
|
||||
"RoleName": {newIAMRoleName()},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingValue("policyName"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMGetRolePolicy_non_existing_role(s *S3Conf) error {
|
||||
testName := "IAMGetRolePolicy_non_existing_role"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := "non-existing-" + genRandString(16)
|
||||
_, err := getIAMRolePolicy(client, &iam.GetRolePolicyInput{
|
||||
RoleName: &roleName,
|
||||
PolicyName: aws.String("p"),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.NoSuchEntityRole(roleName))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMGetRolePolicy_non_existing_policy(s *S3Conf) error {
|
||||
testName := "IAMGetRolePolicy_non_existing_policy"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := newIAMRoleName()
|
||||
if _, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &roleName,
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := checkIAMApiErr(
|
||||
func() error {
|
||||
_, err := getIAMRolePolicy(client, &iam.GetRolePolicyInput{RoleName: &roleName, PolicyName: aws.String("missing")})
|
||||
return err
|
||||
}(),
|
||||
iamerr.NoSuchEntityRolePolicy(roleName, "missing"),
|
||||
)
|
||||
|
||||
deleteErr := deleteIAMRole(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func IAMGetRolePolicy_success(s *S3Conf) error {
|
||||
testName := "IAMGetRolePolicy_success"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := newIAMRoleName()
|
||||
if _, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &roleName,
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
if _, err := putIAMRolePolicy(client, &iam.PutRolePolicyInput{
|
||||
RoleName: &roleName,
|
||||
PolicyName: aws.String("ReadOnly"),
|
||||
PolicyDocument: aws.String(validIAMPolicyDocument),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := getIAMRolePolicy(client, &iam.GetRolePolicyInput{RoleName: &roleName, PolicyName: aws.String("ReadOnly")})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if out == nil {
|
||||
return fmt.Errorf("expected GetRolePolicy output")
|
||||
}
|
||||
if aws.ToString(out.RoleName) != roleName {
|
||||
return fmt.Errorf("expected role name %q, instead got %q", roleName, aws.ToString(out.RoleName))
|
||||
}
|
||||
if aws.ToString(out.PolicyName) != "ReadOnly" {
|
||||
return fmt.Errorf("expected policy name %q, instead got %q", "ReadOnly", aws.ToString(out.PolicyName))
|
||||
}
|
||||
gotDocument, err := url.QueryUnescape(aws.ToString(out.PolicyDocument))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to url-decode policy document %q: %w", aws.ToString(out.PolicyDocument), err)
|
||||
}
|
||||
if gotDocument != validIAMPolicyDocument {
|
||||
return fmt.Errorf("expected policy document %q, instead got %q", validIAMPolicyDocument, gotDocument)
|
||||
}
|
||||
if requestID, ok := awsmiddleware.GetRequestIDMetadata(out.ResultMetadata); !ok || requestID == "" {
|
||||
return fmt.Errorf("expected GetRolePolicy response request id")
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMRoleAndPolicies(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func getIAMRolePolicy(client *iam.Client, input *iam.GetRolePolicyInput) (*iam.GetRolePolicyOutput, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
defer cancel()
|
||||
return client.GetRolePolicy(ctx, input)
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
// Copyright 2026 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
"github.com/versity/versitygw/iamapi/iamerr"
|
||||
)
|
||||
|
||||
func IAMListRolePolicies_missing_role_name(s *S3Conf) error {
|
||||
testName := "IAMListRolePolicies_missing_role_name"
|
||||
body := []byte("Action=ListRolePolicies&Version=2010-05-08")
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingValue("roleName"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListRolePolicies_non_existing_role(s *S3Conf) error {
|
||||
testName := "IAMListRolePolicies_non_existing_role"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := "non-existing-" + genRandString(16)
|
||||
_, err := listIAMRolePolicies(client, &iam.ListRolePoliciesInput{RoleName: &roleName})
|
||||
return checkIAMApiErr(err, iamerr.NoSuchEntityRole(roleName))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListRolePolicies_invalid_max_items(s *S3Conf) error {
|
||||
testName := "IAMListRolePolicies_invalid_max_items"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := newIAMRoleName()
|
||||
if _, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &roleName,
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := checkIAMApiErr(
|
||||
func() error {
|
||||
_, err := listIAMRolePolicies(client, &iam.ListRolePoliciesInput{RoleName: &roleName, MaxItems: aws.Int32(1001)})
|
||||
return err
|
||||
}(),
|
||||
iamerr.InvalidMaxItems("1001"),
|
||||
)
|
||||
|
||||
deleteErr := deleteIAMRole(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListRolePolicies_empty_result(s *S3Conf) error {
|
||||
testName := "IAMListRolePolicies_empty_result"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := newIAMRoleName()
|
||||
if _, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &roleName,
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
out, err := listIAMRolePolicies(client, &iam.ListRolePoliciesInput{RoleName: &roleName})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(out.PolicyNames) != 0 {
|
||||
return fmt.Errorf("expected no policies, instead got %v", out.PolicyNames)
|
||||
}
|
||||
if out.IsTruncated {
|
||||
return fmt.Errorf("expected IsTruncated to be false")
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMRole(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListRolePolicies_success(s *S3Conf) error {
|
||||
testName := "IAMListRolePolicies_success"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := newIAMRoleName()
|
||||
if _, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &roleName,
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
want := []string{"Alpha", "Beta"}
|
||||
for _, name := range want {
|
||||
if _, err := putIAMRolePolicy(client, &iam.PutRolePolicyInput{
|
||||
RoleName: &roleName,
|
||||
PolicyName: aws.String(name),
|
||||
PolicyDocument: aws.String(validIAMPolicyDocument),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
out, err := listIAMRolePolicies(client, &iam.ListRolePoliciesInput{RoleName: &roleName})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if requestID, ok := awsmiddleware.GetRequestIDMetadata(out.ResultMetadata); !ok || requestID == "" {
|
||||
return fmt.Errorf("expected ListRolePolicies response request id")
|
||||
}
|
||||
got := slices.Clone(out.PolicyNames)
|
||||
slices.Sort(got)
|
||||
if !slices.Equal(got, want) {
|
||||
return fmt.Errorf("expected policy names %v, instead got %v", want, got)
|
||||
}
|
||||
if out.IsTruncated {
|
||||
return fmt.Errorf("expected IsTruncated to be false")
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMRoleAndPolicies(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListRolePolicies_pagination(s *S3Conf) error {
|
||||
testName := "IAMListRolePolicies_pagination"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := newIAMRoleName()
|
||||
if _, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &roleName,
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
want := []string{"Alpha", "Beta", "Gamma"}
|
||||
for _, name := range want {
|
||||
if _, err := putIAMRolePolicy(client, &iam.PutRolePolicyInput{
|
||||
RoleName: &roleName,
|
||||
PolicyName: aws.String(name),
|
||||
PolicyDocument: aws.String(validIAMPolicyDocument),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
input := iam.ListRolePoliciesInput{RoleName: &roleName, MaxItems: aws.Int32(1)}
|
||||
var pages []*iam.ListRolePoliciesOutput
|
||||
for {
|
||||
out, err := listIAMRolePolicies(client, &input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pages = append(pages, out)
|
||||
if !out.IsTruncated {
|
||||
break
|
||||
}
|
||||
input.Marker = out.Marker
|
||||
}
|
||||
|
||||
if len(pages) != len(want) {
|
||||
return fmt.Errorf("expected %d pages, instead got %d", len(want), len(pages))
|
||||
}
|
||||
var got []string
|
||||
for i, page := range pages {
|
||||
if len(page.PolicyNames) != 1 {
|
||||
return fmt.Errorf("expected page %d to contain 1 policy, instead got %d", i+1, len(page.PolicyNames))
|
||||
}
|
||||
if page.IsTruncated != (i < len(pages)-1) {
|
||||
return fmt.Errorf("unexpected IsTruncated value on page %d", i+1)
|
||||
}
|
||||
got = append(got, page.PolicyNames...)
|
||||
}
|
||||
slices.Sort(got)
|
||||
if !slices.Equal(got, want) {
|
||||
return fmt.Errorf("expected policy names %v, instead got %v", want, got)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMRoleAndPolicies(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func listIAMRolePolicies(client *iam.Client, input *iam.ListRolePoliciesInput) (*iam.ListRolePoliciesOutput, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
defer cancel()
|
||||
return client.ListRolePolicies(ctx, input)
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
// Copyright 2026 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
"github.com/versity/versitygw/iamapi/iamerr"
|
||||
"github.com/versity/versitygw/iamapi/storage"
|
||||
)
|
||||
|
||||
func IAMPutRolePolicy_missing_role_name(s *S3Conf) error {
|
||||
testName := "IAMPutRolePolicy_missing_role_name"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"PutRolePolicy"},
|
||||
"Version": {"2010-05-08"},
|
||||
"PolicyName": {"p"},
|
||||
"PolicyDocument": {validIAMPolicyDocument},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingValue("roleName"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMPutRolePolicy_missing_policy_name(s *S3Conf) error {
|
||||
testName := "IAMPutRolePolicy_missing_policy_name"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"PutRolePolicy"},
|
||||
"Version": {"2010-05-08"},
|
||||
"RoleName": {newIAMRoleName()},
|
||||
"PolicyDocument": {validIAMPolicyDocument},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingValue("policyName"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMPutRolePolicy_missing_policy_document(s *S3Conf) error {
|
||||
testName := "IAMPutRolePolicy_missing_policy_document"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"PutRolePolicy"},
|
||||
"Version": {"2010-05-08"},
|
||||
"RoleName": {newIAMRoleName()},
|
||||
"PolicyName": {"p"},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingValue("policyDocument"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMPutRolePolicy_invalid_policy_name(s *S3Conf) error {
|
||||
testName := "IAMPutRolePolicy_invalid_policy_name"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := putIAMRolePolicy(client, &iam.PutRolePolicyInput{
|
||||
RoleName: aws.String(newIAMRoleName()),
|
||||
PolicyName: aws.String("bad/name"),
|
||||
PolicyDocument: aws.String(validIAMPolicyDocument),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.InvalidUserName("policyName"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMPutRolePolicy_long_policy_name(s *S3Conf) error {
|
||||
testName := "IAMPutRolePolicy_long_policy_name"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := putIAMRolePolicy(client, &iam.PutRolePolicyInput{
|
||||
RoleName: aws.String(newIAMRoleName()),
|
||||
PolicyName: aws.String(strings.Repeat("p", 129)),
|
||||
PolicyDocument: aws.String(validIAMPolicyDocument),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.UserNameTooLong("policyName", 128))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMPutRolePolicy_non_ascii_policy_document(s *S3Conf) error {
|
||||
testName := "IAMPutRolePolicy_non_ascii_policy_document"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := putIAMRolePolicy(client, &iam.PutRolePolicyInput{
|
||||
RoleName: aws.String(newIAMRoleName()),
|
||||
PolicyName: aws.String("p"),
|
||||
PolicyDocument: aws.String("emoji\U0001F600test"),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.InvalidCharset("policyDocument"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMPutRolePolicy_non_existing_role(s *S3Conf) error {
|
||||
testName := "IAMPutRolePolicy_non_existing_role"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := "non-existing-" + genRandString(16)
|
||||
_, err := putIAMRolePolicy(client, &iam.PutRolePolicyInput{
|
||||
RoleName: &roleName,
|
||||
PolicyName: aws.String("p"),
|
||||
PolicyDocument: aws.String(validIAMPolicyDocument),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.NoSuchEntityRole(roleName))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMPutRolePolicy_malformed_policy_document(s *S3Conf) error {
|
||||
testName := "IAMPutRolePolicy_malformed_policy_document"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
cases := []struct {
|
||||
name string
|
||||
doc string
|
||||
wantErr iamerr.APIError
|
||||
}{
|
||||
{"invalid json syntax", `{not valid json`, iamerr.MalformedPolicyDocument("Syntax errors in policy.")},
|
||||
{"empty object", `{}`, iamerr.MalformedPolicyDocument("Syntax errors in policy.")},
|
||||
{"invalid version", `{"Version":"2020-01-01","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}`, iamerr.MalformedPolicyDocument("Syntax errors in policy.")},
|
||||
{"missing statement", `{"Version":"2012-10-17"}`, iamerr.MalformedPolicyDocument("Syntax errors in policy.")},
|
||||
{"null statement", `{"Version":"2012-10-17","Statement":null}`, iamerr.MalformedPolicyDocument("Syntax errors in policy.")},
|
||||
{"empty statement array", `{"Version":"2012-10-17","Statement":[]}`, iamerr.MalformedPolicyDocument("Syntax errors in policy.")},
|
||||
{"statement is a string", `{"Version":"2012-10-17","Statement":"hello"}`, iamerr.MalformedPolicyDocument("Syntax errors in policy.")},
|
||||
{"missing effect", `{"Version":"2012-10-17","Statement":[{"Action":"s3:GetObject","Resource":"*"}]}`, iamerr.MalformedPolicyDocument("Syntax errors in policy.")},
|
||||
{"invalid effect value", `{"Version":"2012-10-17","Statement":[{"Effect":"Maybe","Action":"s3:GetObject","Resource":"*"}]}`, iamerr.MalformedPolicyDocument("Syntax errors in policy.")},
|
||||
{"action and notaction both present", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","NotAction":"s3:PutObject","Resource":"*"}]}`, iamerr.MalformedPolicyDocument("Syntax errors in policy.")},
|
||||
{"resource and notresource both present", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*","NotResource":"foo"}]}`, iamerr.MalformedPolicyDocument("Syntax errors in policy.")},
|
||||
{"numeric action wrong type", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":123,"Resource":"*"}]}`, iamerr.MalformedPolicyDocument("Syntax errors in policy.")},
|
||||
|
||||
{"missing action and notaction", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Resource":"*"}]}`, iamerr.MalformedPolicyDocument("Policy statement must contain actions.")},
|
||||
|
||||
{"missing resource and notresource", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject"}]}`, iamerr.MalformedPolicyDocument("Policy statement must contain resources.")},
|
||||
{"empty resource array", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":[]}]}`, iamerr.MalformedPolicyDocument("Policy statement must contain resources.")},
|
||||
|
||||
{"empty string action", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"","Resource":"*"}]}`, iamerr.MalformedPolicyDocument("Actions/Conditions must be prefaced by a vendor, e.g., iam, sdb, ec2, etc.")},
|
||||
{"action missing vendor colon", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"GetObject","Resource":"*"}]}`, iamerr.MalformedPolicyDocument("Actions/Conditions must be prefaced by a vendor, e.g., iam, sdb, ec2, etc.")},
|
||||
{"notaction missing vendor colon", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","NotAction":"GetObject","Resource":"*"}]}`, iamerr.MalformedPolicyDocument("Actions/Conditions must be prefaced by a vendor, e.g., iam, sdb, ec2, etc.")},
|
||||
{"empty vendor prefix", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":":GetObject","Resource":"*"}]}`, iamerr.MalformedPolicyDocument("Vendor is not valid")},
|
||||
{"vendor with invalid character", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"iam :Get","Resource":"*"}]}`, iamerr.MalformedPolicyDocument("Vendor iam is not valid")},
|
||||
|
||||
{"resource with no colon at all", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"invalid"}]}`, iamerr.MalformedPolicyDocument(`Resource invalid must be in ARN format or "*".`)},
|
||||
{"notresource with no colon at all", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","NotResource":"invalid"}]}`, iamerr.MalformedPolicyDocument(`Resource invalid must be in ARN format or "*".`)},
|
||||
{"resource with colon but no arn prefix", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"s3::example-bucket/*"}]}`, iamerr.MalformedPolicyDocument(`Partition "" is not valid for resource "arn::example-bucket/*:*:*:*".`)},
|
||||
{"resource with arn prefix but too few fields", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:awss3::example-bucket/*"}]}`, iamerr.MalformedPolicyDocument("The policy failed legacy parsing")},
|
||||
{"resource with invalid partition", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws2:s3:::example-bucket/*"}]}`, iamerr.MalformedPolicyDocument(`Partition "aws2" is not valid for resource "arn:aws2:s3:::example-bucket/*".`)},
|
||||
|
||||
{"duplicate sid across statements", `{"Version":"2012-10-17","Statement":[{"Sid":"Dup","Effect":"Allow","Action":"s3:GetObject","Resource":"*"},{"Sid":"Dup","Effect":"Allow","Action":"s3:PutObject","Resource":"*"}]}`, iamerr.MalformedPolicyDocument("Statement IDs (SID) in a single policy must be unique.")},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
if err := func() error {
|
||||
roleName := newIAMRoleName()
|
||||
if _, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &roleName,
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
}); err != nil {
|
||||
return fmt.Errorf("%s: %w", c.name, err)
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
_, err := putIAMRolePolicy(client, &iam.PutRolePolicyInput{
|
||||
RoleName: &roleName,
|
||||
PolicyName: aws.String("p"),
|
||||
PolicyDocument: aws.String(c.doc),
|
||||
})
|
||||
if err := checkIAMApiErr(err, c.wantErr); err != nil {
|
||||
return fmt.Errorf("%s: %w", c.name, err)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMRole(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func IAMPutRolePolicy_principal_not_allowed(s *S3Conf) error {
|
||||
testName := "IAMPutRolePolicy_principal_not_allowed"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := newIAMRoleName()
|
||||
if _, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &roleName,
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
doc := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":"*","Action":"s3:GetObject","Resource":"*"}]}`
|
||||
_, err := putIAMRolePolicy(client, &iam.PutRolePolicyInput{
|
||||
RoleName: &roleName,
|
||||
PolicyName: aws.String("p"),
|
||||
PolicyDocument: aws.String(doc),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.MalformedPolicyDocument("Policy document should not specify a principal."))
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMRole(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func IAMPutRolePolicy_limit_exceeded(s *S3Conf) error {
|
||||
testName := "IAMPutRolePolicy_limit_exceeded"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := newIAMRoleName()
|
||||
if _, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &roleName,
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
oversized := `{"Version":"2012-10-17","Statement":[{"Sid":"` + strings.Repeat("x", 10500) + `","Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}`
|
||||
_, err := putIAMRolePolicy(client, &iam.PutRolePolicyInput{
|
||||
RoleName: &roleName,
|
||||
PolicyName: aws.String("p"),
|
||||
PolicyDocument: aws.String(oversized),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.InlinePolicyQuotaExceeded("role", roleName, storage.MaxInlinePolicyBytesPerRole))
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMRole(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func IAMPutRolePolicy_success(s *S3Conf) error {
|
||||
testName := "IAMPutRolePolicy_success"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := newIAMRoleName()
|
||||
if _, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &roleName,
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := putIAMRolePolicy(client, &iam.PutRolePolicyInput{
|
||||
RoleName: &roleName,
|
||||
PolicyName: aws.String("ReadOnly"),
|
||||
PolicyDocument: aws.String(validIAMPolicyDocument),
|
||||
})
|
||||
checkErr := func() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if out == nil {
|
||||
return fmt.Errorf("expected PutRolePolicy output")
|
||||
}
|
||||
if requestID, ok := awsmiddleware.GetRequestIDMetadata(out.ResultMetadata); !ok || requestID == "" {
|
||||
return fmt.Errorf("expected PutRolePolicy response request id")
|
||||
}
|
||||
|
||||
got, err := getIAMRolePolicy(client, &iam.GetRolePolicyInput{RoleName: &roleName, PolicyName: aws.String("ReadOnly")})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gotDocument, err := url.QueryUnescape(aws.ToString(got.PolicyDocument))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to url-decode policy document %q: %w", aws.ToString(got.PolicyDocument), err)
|
||||
}
|
||||
if gotDocument != validIAMPolicyDocument {
|
||||
return fmt.Errorf("expected policy document %q, instead got %q", validIAMPolicyDocument, gotDocument)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMRoleAndPolicies(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func IAMPutRolePolicy_overwrite_updates_existing(s *S3Conf) error {
|
||||
testName := "IAMPutRolePolicy_overwrite_updates_existing"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := newIAMRoleName()
|
||||
if _, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &roleName,
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
if _, err := putIAMRolePolicy(client, &iam.PutRolePolicyInput{
|
||||
RoleName: &roleName,
|
||||
PolicyName: aws.String("p"),
|
||||
PolicyDocument: aws.String(validIAMPolicyDocument),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updated := `{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"s3:DeleteObject","Resource":"*"}]}`
|
||||
if _, err := putIAMRolePolicy(client, &iam.PutRolePolicyInput{
|
||||
RoleName: &roleName,
|
||||
PolicyName: aws.String("p"),
|
||||
PolicyDocument: aws.String(updated),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
got, err := getIAMRolePolicy(client, &iam.GetRolePolicyInput{RoleName: &roleName, PolicyName: aws.String("p")})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gotDocument, err := url.QueryUnescape(aws.ToString(got.PolicyDocument))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to url-decode policy document %q: %w", aws.ToString(got.PolicyDocument), err)
|
||||
}
|
||||
if gotDocument != updated {
|
||||
return fmt.Errorf("expected overwritten policy document %q, instead got %q", updated, gotDocument)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMRoleAndPolicies(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func putIAMRolePolicy(client *iam.Client, input *iam.PutRolePolicyInput) (*iam.PutRolePolicyOutput, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
defer cancel()
|
||||
return client.PutRolePolicy(ctx, input)
|
||||
}
|
||||
Reference in New Issue
Block a user