Files
seaweedfs/weed/s3api/s3api_embedded_iam_role.go
T
895d49b55b s3/iam: manage roles through the IAM API, with an opt-in persistent role store (#11522)
* s3/iam: manage roles through the IAM API, with an opt-in persistent role store

Roles could only come from the IAM config file: the S3 server pinned the
role store to memory and the embedded IAM API had no role actions, so a
role could not be created, retrusted or revoked without editing the file
and restarting every gateway.

Role store
- Read the `roleStore` key (the IAMConfig field already existed). With an
  IAM config file the default stays memory; with none it is the filer, as
  for OIDC providers, so zero-config clusters keep runtime-created roles.
- Roles from the IAM config file never go into a persistent role store,
  which outlives the file and may be shared by S3 servers with different
  files. They are served from memory beneath the store, as OIDC providers
  are: a stored role of the same name takes precedence, and deleting it
  restores the file's. A config-file role cannot be changed or deleted
  through the API (UnmodifiableEntity), and removing one from the file
  removes it at the next start. An in-memory store holds them as records,
  as before. They have no creation time, so CreateDate is omitted rather
  than reporting when this server started. SetRoleStore installs a store
  the same way, so a store set after startup keeps the config-file roles,
  as SetOIDCProviderStore does for providers.
- Watch /etc/iam/roles and drop the cached role definitions on change. The
  cached filer store otherwise serves a peer's stale role for up to its 5m
  TTL, which keeps a revoked trust policy in force on the other gateways.
- Role stores wrap ErrRoleNotFound for a missing role; the filer store
  used to report any failed lookup as "role not found". CreateRole proceeds
  only on a confirmed absence, so an unreadable store cannot let it write
  over an existing role.

IAM actions
- CreateRole, GetRole, ListRoles, DeleteRole, UpdateAssumeRolePolicy,
  AttachRolePolicy, DetachRolePolicy, ListAttachedRolePolicies. The reads
  are allowed in read-only mode.
- A role defined in the config file is reloaded from it at every start, so
  changing or deleting it through the API is refused (UnmodifiableEntity)
  rather than silently reverted.
- DeleteRole with policies attached is refused (DeleteConflict), as on AWS.
- Role names follow AWS's rules ([\w+=,.@-]{1,64}); a role is stored as
  <name>.json in the filer, so this also keeps a name from leaving the role
  store's directory. At most 10 managed policies per role (AWS's default
  quota; MaxManagedPoliciesPerUser is 10 too), LimitExceeded beyond.
- DeletePolicy is refused (DeleteConflict) while a role attaches the
  policy, as it already is for users and groups: roles attach policies by
  name, so a policy created later under the deleted one's name would
  otherwise take effect on the role.
- Role paths other than "/" and role tags are not stored, so they are
  refused rather than dropped.

Role IDs and sessions
- Roles get a unique RoleId when first stored (random, AWS AROA form),
  kept across updates; a config-file role gets a stable ID derived from its
  name, since it is created again at every start.
- Sessions issued through AssumeRoleWithWebIdentity, AssumeRoleWithCredentials
  and AssumeRole carry the role's ID (claim "rid"), and a request under a role
  whose current ID differs is denied. Resolving a session's policies by role
  name let a session outlive its role: once a role was deleted, a role later
  created under the same name — with a different trust policy and different
  policies — revived every unexpired session of the old one with the new
  role's permissions. Sessions issued before this change carry no ID and are
  unaffected until they expire.

Integration test (test/s3/iam, run with `make start-services`):
TestWebIdentityWithProviderAndRoleManagedThroughIAMAPI configures an OIDC
provider, a managed policy and a role entirely through the IAM API against a
JWKS served by the test, then checks the trusted subject gets credentials
scoped to the attached policy; another subject, a token signed by another
key, an unsigned token and a token for another audience are refused; and UpdateAssumeRolePolicy moves the
trust at once.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* s3/iam: bind every role session to its role, and change roles atomically

Review follow-ups.

Session binding
- The role-ID check ran only when a session carried no policy names, and
  AssumeRole embeds the role's attached policies, so those sessions kept
  their permissions after the role was deleted or recreated. The check
  now runs for every session carrying a role ID, before policy selection.
- A named role that cannot be resolved at issuance gets no session,
  instead of one with no role ID (which nothing binds).
- A config-file role's ID is derived from its name and trust policy, not
  the name alone: a different role put in the file under the same name
  gets a new ID, while an unchanged role keeps its sessions across restarts.

Role writes
- RoleStore gains UpdateRole, a read-modify-write that lands only if the
  role is unchanged since the read, and otherwise re-reads and retries. The
  filer store uses the filer's write conditions (IF_NOT_EXISTS for a new
  role, IF_ENTRY_EQUAL otherwise). CreateRole, UpdateAssumeRolePolicy and
  Attach/DetachRolePolicy all go through it, so two gateways no longer
  overwrite each other's changes, a change racing a delete no longer
  writes the role back, and of two concurrent creates one gets
  EntityAlreadyExists.
- The filer store's ListRoles pages past 1,000 entries and fails on a
  broken stream instead of returning what arrived, so DeletePolicy's
  attachment check sees every role. ListRoles skips a role deleted between
  listing and reading it.
- CreateRole validates first; a failed write is ServiceFailure, not
  InvalidInput. Any Tags.* parameter is refused, not only the first key.
- ExecuteAction's skipPersist covers the S3ApiConfiguration only; the
  comment now says so. Role and OIDC provider actions write their own stores.

Each fix has a test that fails without it. Against a real filer with two
gateways, concurrent AttachRolePolicy calls lost 1-4 of 8 attachments per
run before this change and none after.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* s3/iam: one role snapshot per decision; DeleteRole is atomic; watch a custom role store path

Review follow-ups.

- Authorization evaluates the policies of the role definition the session's
  binding was checked against, instead of reading the role again: a role
  replaced in between cannot lend a session its policies.
- AssumeRole and AssumeRoleWithLDAPIdentity issue the session from the
  definition whose trust admits the caller (IAMManager.ResolveRoleForPrincipal),
  and take its ID, duration cap and embedded policies from that same
  definition. A role replaced after the caller's trust check by one that does
  not trust the caller now yields AccessDenied, not a session bound to the
  replacement.
- A RoleUpdate that returns nil deletes the role, on the same condition as a
  write: the filer store deletes with ObjectTransaction on IF_ENTRY_EQUAL,
  routed and locked like the conditional CreateEntry. DeleteRole decides
  against the role it deletes, so a policy attached meanwhile on another
  server is a DeleteConflict, and a delete never removes a role written
  after its check.
- S3 servers watch the role store's configured basePath, not only
  /etc/iam/roles, so a custom path also drops peers' cached roles on change.

Each has a test that fails without it. Live against a real filer: DeleteRole
refuses while a policy is attached and removes the entry once detached; all
test/s3/iam CI stages pass.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* s3/iam: state which roles DeletePolicy's attachment check can see

RolesAttachingPolicy sees the stored roles and this server's config-file
roles. A role defined only in another server's IAM config file is invisible
to it, so a config-file role that attaches a managed policy is protected
only on the servers whose file defines it. The doc comment now says so and
how to avoid it: keep such roles in every server's file, or attach only
config-file policies to config-file roles.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* iam: note that a role store set after startup is not watched for peer changes

S3 servers build their metadata watch list once, at startup, from the role
store installed then. SetRoleStore's doc now says that a filer-backed store
installed later with a different basePath is not watched, so peers' changes
to it reach this server's cached roles only when the cache expires.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 20:45:47 +08:00

420 lines
16 KiB
Go

package s3api
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"slices"
"strconv"
"strings"
"time"
"github.com/aws/aws-sdk-go/service/iam"
iamlib "github.com/seaweedfs/seaweedfs/weed/iam"
"github.com/seaweedfs/seaweedfs/weed/iam/integration"
"github.com/seaweedfs/seaweedfs/weed/iam/policy"
)
// Role IAM actions handled by this file.
const (
actionCreateRole = "CreateRole"
actionGetRole = "GetRole"
actionListRoles = "ListRoles"
actionDeleteRole = "DeleteRole"
actionUpdateAssumeRolePolicy = "UpdateAssumeRolePolicy"
actionAttachRolePolicy = "AttachRolePolicy"
actionDetachRolePolicy = "DetachRolePolicy"
actionListAttachedRolePolicies = "ListAttachedRolePolicies"
)
// isRoleAction reports whether an action belongs to the role family.
func isRoleAction(action string) bool {
switch action {
case actionCreateRole, actionGetRole, actionListRoles, actionDeleteRole,
actionUpdateAssumeRolePolicy, actionAttachRolePolicy, actionDetachRolePolicy,
actionListAttachedRolePolicies:
return true
default:
return false
}
}
// dispatchRoleAction handles the role IAM actions. Roles live in the IAM
// manager's role store, not in S3ApiConfiguration, so like the OIDC provider
// actions they are dispatched before the configuration load. The boolean
// reports whether the action was recognised.
func (e *EmbeddedIamApi) dispatchRoleAction(ctx context.Context, values url.Values) (iamlib.RequestIDSetter, *iamError, bool) {
if !isRoleAction(values.Get("Action")) {
return nil, nil, false
}
mgr := e.oidcIAMManager()
if mgr == nil {
return nil, &iamError{Code: iam.ErrCodeServiceFailureException, Error: errors.New("role store not configured: start the S3 server with an IAM config")}, true
}
switch values.Get("Action") {
case actionCreateRole:
resp, err := e.createRole(ctx, mgr, values)
return resp, err, true
case actionGetRole:
resp, err := e.getRole(ctx, mgr, values)
return resp, err, true
case actionListRoles:
resp, err := e.listRoles(ctx, mgr)
return resp, err, true
case actionDeleteRole:
resp, err := e.deleteRole(ctx, mgr, values)
return resp, err, true
case actionUpdateAssumeRolePolicy:
resp, err := e.updateAssumeRolePolicy(ctx, mgr, values)
return resp, err, true
case actionAttachRolePolicy:
resp, err := e.attachRolePolicy(ctx, mgr, values)
return resp, err, true
case actionDetachRolePolicy:
resp, err := e.detachRolePolicy(ctx, mgr, values)
return resp, err, true
case actionListAttachedRolePolicies:
resp, err := e.listAttachedRolePolicies(ctx, mgr, values)
return resp, err, true
}
return nil, nil, false
}
// parseTrustPolicy decodes and validates an AssumeRolePolicyDocument.
func parseTrustPolicy(document string) (*policy.PolicyDocument, *iamError) {
if strings.TrimSpace(document) == "" {
return nil, &iamError{Code: iam.ErrCodeInvalidInputException, Error: errors.New("AssumeRolePolicyDocument is required")}
}
var doc policy.PolicyDocument
if err := json.Unmarshal([]byte(document), &doc); err != nil {
return nil, &iamError{Code: iam.ErrCodeMalformedPolicyDocumentException, Error: fmt.Errorf("parse trust policy: %w", err)}
}
if err := policy.ValidateTrustPolicyDocument(&doc); err != nil {
return nil, &iamError{Code: iam.ErrCodeMalformedPolicyDocumentException, Error: err}
}
return &doc, nil
}
// requireRole loads the named role, mapping a missing role to NoSuchEntity.
func requireRole(ctx context.Context, mgr *integration.IAMManager, values url.Values) (*integration.RoleDefinition, *iamError) {
name := strings.TrimSpace(values.Get("RoleName"))
if name == "" {
return nil, &iamError{Code: iam.ErrCodeInvalidInputException, Error: errors.New("RoleName is required")}
}
role, err := mgr.GetRole(ctx, name)
if errors.Is(err, integration.ErrRoleNotFound) || (err == nil && role == nil) {
return nil, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("role %s not found", name)}
}
if err != nil {
return nil, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
return role, nil
}
// requireMutableRole is requireRole for actions that change or delete the
// role. A role loaded from the IAM config file is reloaded from it at every
// start, so a change made through the API would be silently reverted; it is
// refused instead.
func requireMutableRole(ctx context.Context, mgr *integration.IAMManager, values url.Values) (*integration.RoleDefinition, *iamError) {
role, iamErr := requireRole(ctx, mgr, values)
if iamErr != nil {
return nil, iamErr
}
if role.Source == integration.RoleSourceStaticConfig {
return nil, &iamError{Code: iam.ErrCodeUnmodifiableEntityException, Error: fmt.Errorf("role %s is defined in the IAM config file; change it there", role.RoleName)}
}
return role, nil
}
// errRoleUpdateRefused aborts a RoleStore update whose refusal the handler
// has already recorded as an iamError.
var errRoleUpdateRefused = errors.New("role update refused")
// updateRole applies change to the named role through the role store's atomic
// update, so a concurrent change on another server is neither lost nor
// written over a delete. change sees the current role and returns an iamError
// to refuse; a missing role is NoSuchEntity and a config-file role is
// UnmodifiableEntity, as requireMutableRole reports them.
func updateRole(ctx context.Context, mgr *integration.IAMManager, name string, change func(role *integration.RoleDefinition) *iamError) *iamError {
var refused *iamError
err := mgr.UpdateRole(ctx, name, func(current *integration.RoleDefinition) (*integration.RoleDefinition, error) {
if current == nil {
refused = &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("role %s not found", name)}
return nil, errRoleUpdateRefused
}
if refused = change(current); refused != nil {
return nil, errRoleUpdateRefused
}
return current, nil
})
return roleWriteError(err, refused)
}
// roleWriteError maps a role store write's outcome to the IAM error to
// report: the handler's own refusal, else the store's error by kind. A write
// that failed for another reason is a service failure, which clients retry.
func roleWriteError(err error, refused *iamError) *iamError {
switch {
case err == nil:
return nil
case errors.Is(err, errRoleUpdateRefused) && refused != nil:
return refused
case errors.Is(err, integration.ErrRoleStatic):
return &iamError{Code: iam.ErrCodeUnmodifiableEntityException, Error: fmt.Errorf("%w; change it there", err)}
case errors.Is(err, integration.ErrRoleExists):
return &iamError{Code: iam.ErrCodeEntityAlreadyExistsException, Error: err}
default:
return &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
}
// roleID is the role's stored ID. A role stored before IDs were recorded has
// none; it is reported with the ID StaticRoleID derives for it.
func roleID(role *integration.RoleDefinition) string {
if role.RoleId != "" {
return role.RoleId
}
return integration.StaticRoleID(role)
}
func toIAMRole(role *integration.RoleDefinition) iamlib.IAMRole {
out := iamlib.IAMRole{
Path: "/",
RoleName: role.RoleName,
RoleId: roleID(role),
Arn: role.RoleArn,
Description: role.Description,
MaxSessionDuration: role.MaxSessionDuration,
}
if !role.CreatedAt.IsZero() {
out.CreateDate = role.CreatedAt.UTC().Format(time.RFC3339)
}
if role.TrustPolicy != nil {
if doc, err := json.Marshal(role.TrustPolicy); err == nil {
// AWS returns the document URL-encoded.
out.AssumeRolePolicyDocument = url.PathEscape(string(doc))
}
}
return out
}
func (e *EmbeddedIamApi) createRole(ctx context.Context, mgr *integration.IAMManager, values url.Values) (*iamlib.CreateRoleResponse, *iamError) {
name := strings.TrimSpace(values.Get("RoleName"))
if name == "" {
return nil, &iamError{Code: iam.ErrCodeInvalidInputException, Error: errors.New("RoleName is required")}
}
if err := integration.ValidateRoleName(name); err != nil {
return nil, &iamError{Code: iam.ErrCodeInvalidInputException, Error: err}
}
if path := values.Get("Path"); path != "" && path != "/" {
return nil, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("role paths are not supported: %s", path)}
}
for key := range values {
if strings.HasPrefix(key, "Tags.") {
return nil, &iamError{Code: iam.ErrCodeInvalidInputException, Error: errors.New("role tags are not supported")}
}
}
// A config-file role is served beside the store, not from it, so the
// store's create-if-absent cannot see it.
existing, err := mgr.GetRole(ctx, name)
if err == nil && existing != nil {
return nil, &iamError{Code: iam.ErrCodeEntityAlreadyExistsException, Error: fmt.Errorf("role %s already exists", name)}
}
if err != nil && !errors.Is(err, integration.ErrRoleNotFound) {
return nil, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
trust, iamErr := parseTrustPolicy(values.Get("AssumeRolePolicyDocument"))
if iamErr != nil {
return nil, iamErr
}
var maxSession int64
if raw := values.Get("MaxSessionDuration"); raw != "" {
n, err := strconv.ParseInt(raw, 10, 64)
if err != nil {
return nil, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("MaxSessionDuration: %w", err)}
}
maxSession = n
}
role := &integration.RoleDefinition{
RoleName: name,
TrustPolicy: trust,
Description: values.Get("Description"),
MaxSessionDuration: maxSession,
CreatedAt: time.Now().UTC(),
RoleId: integration.NewRoleID(),
}
// Validation first: only a bad request is InvalidInput. The write below
// is created only if no role of this name exists by then, so of two
// concurrent creates, one fails with EntityAlreadyExists.
if err := integration.PrepareRoleDefinition(name, role); err != nil {
return nil, &iamError{Code: iam.ErrCodeInvalidInputException, Error: err}
}
err = mgr.UpdateRole(ctx, name, func(current *integration.RoleDefinition) (*integration.RoleDefinition, error) {
if current != nil {
return nil, fmt.Errorf("%w: %s", integration.ErrRoleExists, name)
}
return role, nil
})
if iamErr := roleWriteError(err, nil); iamErr != nil {
return nil, iamErr
}
resp := &iamlib.CreateRoleResponse{}
resp.CreateRoleResult.Role = toIAMRole(role)
return resp, nil
}
func (e *EmbeddedIamApi) getRole(ctx context.Context, mgr *integration.IAMManager, values url.Values) (*iamlib.GetRoleResponse, *iamError) {
role, iamErr := requireRole(ctx, mgr, values)
if iamErr != nil {
return nil, iamErr
}
resp := &iamlib.GetRoleResponse{}
resp.GetRoleResult.Role = toIAMRole(role)
return resp, nil
}
func (e *EmbeddedIamApi) listRoles(ctx context.Context, mgr *integration.IAMManager) (*iamlib.ListRolesResponse, *iamError) {
roles, err := mgr.ListRoles(ctx)
if err != nil {
return nil, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
resp := &iamlib.ListRolesResponse{}
resp.ListRolesResult.Roles = make([]*iamlib.IAMRole, 0, len(roles))
for _, role := range roles {
view := toIAMRole(role)
resp.ListRolesResult.Roles = append(resp.ListRolesResult.Roles, &view)
}
return resp, nil
}
func (e *EmbeddedIamApi) deleteRole(ctx context.Context, mgr *integration.IAMManager, values url.Values) (*iamlib.DeleteRoleResponse, *iamError) {
role, iamErr := requireMutableRole(ctx, mgr, values)
if iamErr != nil {
return nil, iamErr
}
// The delete is decided against the role as it is when deleted, in the
// store's atomic update: a policy attached meanwhile on another server is
// a DeleteConflict, as AWS reports a role with managed policies attached.
var refused *iamError
err := mgr.UpdateRole(ctx, role.RoleName, func(current *integration.RoleDefinition) (*integration.RoleDefinition, error) {
if current == nil {
refused = &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("role %s not found", role.RoleName)}
return nil, errRoleUpdateRefused
}
if len(current.AttachedPolicies) > 0 {
refused = &iamError{Code: iam.ErrCodeDeleteConflictException, Error: fmt.Errorf("role %s has attached policies; detach them first", role.RoleName)}
return nil, errRoleUpdateRefused
}
return nil, nil
})
if iamErr := roleWriteError(err, refused); iamErr != nil {
return nil, iamErr
}
return &iamlib.DeleteRoleResponse{}, nil
}
func (e *EmbeddedIamApi) updateAssumeRolePolicy(ctx context.Context, mgr *integration.IAMManager, values url.Values) (*iamlib.UpdateAssumeRolePolicyResponse, *iamError) {
role, iamErr := requireMutableRole(ctx, mgr, values)
if iamErr != nil {
return nil, iamErr
}
trust, iamErr := parseTrustPolicy(values.Get("PolicyDocument"))
if iamErr != nil {
return nil, iamErr
}
if iamErr := updateRole(ctx, mgr, role.RoleName, func(current *integration.RoleDefinition) *iamError {
current.TrustPolicy = trust
return nil
}); iamErr != nil {
return nil, iamErr
}
return &iamlib.UpdateAssumeRolePolicyResponse{}, nil
}
// rolePolicyName resolves PolicyArn to the name of an existing managed policy.
func (e *EmbeddedIamApi) rolePolicyName(ctx context.Context, values url.Values) (string, *iamError) {
name, err := iamPolicyNameFromArn(values.Get("PolicyArn"))
if err != nil {
return "", &iamError{Code: iam.ErrCodeInvalidInputException, Error: err}
}
if e.credentialManager == nil {
return "", &iamError{Code: iam.ErrCodeServiceFailureException, Error: errors.New("credential manager not configured")}
}
existing, err := e.credentialManager.GetPolicy(ctx, name)
if err != nil {
return "", &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
if existing == nil {
return "", &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", name)}
}
return name, nil
}
func (e *EmbeddedIamApi) attachRolePolicy(ctx context.Context, mgr *integration.IAMManager, values url.Values) (*iamlib.AttachRolePolicyResponse, *iamError) {
role, iamErr := requireMutableRole(ctx, mgr, values)
if iamErr != nil {
return nil, iamErr
}
name, iamErr := e.rolePolicyName(ctx, values)
if iamErr != nil {
return nil, iamErr
}
if slices.Contains(role.AttachedPolicies, name) {
return &iamlib.AttachRolePolicyResponse{}, nil
}
if iamErr := updateRole(ctx, mgr, role.RoleName, func(current *integration.RoleDefinition) *iamError {
if slices.Contains(current.AttachedPolicies, name) {
return nil
}
if len(current.AttachedPolicies) >= integration.MaxManagedPoliciesPerRole {
return &iamError{Code: iam.ErrCodeLimitExceededException,
Error: fmt.Errorf("cannot attach more than %d managed policies to role %s", integration.MaxManagedPoliciesPerRole, current.RoleName)}
}
current.AttachedPolicies = append(current.AttachedPolicies, name)
return nil
}); iamErr != nil {
return nil, iamErr
}
return &iamlib.AttachRolePolicyResponse{}, nil
}
func (e *EmbeddedIamApi) detachRolePolicy(ctx context.Context, mgr *integration.IAMManager, values url.Values) (*iamlib.DetachRolePolicyResponse, *iamError) {
role, iamErr := requireMutableRole(ctx, mgr, values)
if iamErr != nil {
return nil, iamErr
}
name, err := iamPolicyNameFromArn(values.Get("PolicyArn"))
if err != nil {
return nil, &iamError{Code: iam.ErrCodeInvalidInputException, Error: err}
}
if iamErr := updateRole(ctx, mgr, role.RoleName, func(current *integration.RoleDefinition) *iamError {
idx := slices.Index(current.AttachedPolicies, name)
if idx < 0 {
return &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s is not attached to role %s", name, current.RoleName)}
}
current.AttachedPolicies = slices.Delete(current.AttachedPolicies, idx, idx+1)
return nil
}); iamErr != nil {
return nil, iamErr
}
return &iamlib.DetachRolePolicyResponse{}, nil
}
func (e *EmbeddedIamApi) listAttachedRolePolicies(ctx context.Context, mgr *integration.IAMManager, values url.Values) (*iamlib.ListAttachedRolePoliciesResponse, *iamError) {
role, iamErr := requireRole(ctx, mgr, values)
if iamErr != nil {
return nil, iamErr
}
resp := &iamlib.ListAttachedRolePoliciesResponse{}
resp.ListAttachedRolePoliciesResult.AttachedPolicies = make([]*iamlib.IAMAttachedPolicy, 0, len(role.AttachedPolicies))
for _, name := range role.AttachedPolicies {
resp.ListAttachedRolePoliciesResult.AttachedPolicies = append(resp.ListAttachedRolePoliciesResult.AttachedPolicies,
&iamlib.IAMAttachedPolicy{PolicyName: name, PolicyArn: iamPolicyArn(name)})
}
return resp, nil
}