Identity and Access

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

See the section bibliography for the reference material consulted while preparing these pages.

Commands and behaviour below are current for AWS CLI v2 (2.37 line), AWS SDK for Java 2.x and the aws-actions/configure-aws-credentials action v6 (checked 2026-09-30); re-verify against aws --version and the linked reference pages before relying on them in a pipeline.

Every Bookshelf resource (the catalog-api ECS service, the notifier Lambda function, the bookshelf-uploads-<account-id> bucket, and so on — see AWS (Amazon Web Services)) is reached by something that first has to prove who it is and then be told what it may do. This page is that "who" and "what": IAM as the authorization model, IAM Identity Center for people, roles and STS for everything else, and the default credential chain that lets Bookshelf’s own services authenticate without holding a long-lived key. Account structure and the guardrails above individual accounts are in Accounts and Organizations; the account bootstrap itself is in Getting Started with AWS.

IAM concepts

AWS Identity and Access Management (IAM) is a global service: identities and policies are created once per account and apply in every Region. Every API call is made by a principal and is authenticated, then authorized against the policies that apply to it.

Root user

The identity created with the account, with unrestricted access that no policy can limit. Used for the handful of tasks that require it and protected with MFA. Multi-factor authentication is now required for the root user of every account type, and member accounts of an organization can have no root credentials at all through centralized root access (see Accounts and Organizations).

IAM user

A long-lived identity inside one account with its own password and, optionally, access keys. Older guides and tutorials still set up the CLI with an IAM user and aws configure; current guidance is to avoid long-lived keys for people and use IAM Identity Center (below) instead. IAM users remain for the few cases that cannot federate.

IAM group

A collection of IAM users to which policies are attached. A group is not a principal and cannot be named in a trust or resource policy.

IAM role

An identity with permissions but no credentials of its own. Anyone or anything allowed by the role’s trust policy can assume it and receive temporary credentials from the AWS Security Token Service (STS). Roles are the building block for people (through Identity Center or federation), AWS services (EC2, ECS, Lambda, EKS pods), CI systems and cross-account access.

Federated identity

A person or workload authenticated by an external identity provider (SAML 2.0, OIDC, a corporate directory) that is mapped to a role and receives temporary credentials.

The practical rule for Bookshelf: people sign in through IAM Identity Center, workloads use roles, CI uses OIDC, and there is no IAM user with access keys anywhere.

Policy types

A policy is a JSON document of statements, each with an Effect (Allow or Deny), Action, Resource and an optional Condition; resource-based and trust policies add a Principal. AWS evaluates several kinds of policy together:

Policy type Attached to What it does

Identity-based policy

An IAM user, group or role (AWS managed, customer managed or inline)

Grants permissions to that identity.

Resource-based policy

A resource (S3 bucket, SQS queue, KMS key, Lambda function, role trust policy)

Grants permissions on that resource to named principals, including principals in other accounts.

Permissions boundary

An IAM user or role

Sets the maximum permissions the identity can ever have; grants nothing by itself. Used to let developers create roles without letting them escalate privileges.

Session policy

Passed when assuming a role or federating

Further restricts one session below what the role would otherwise allow.

Service control policy (SCP)

An organization root, OU or account

Sets the maximum permissions for IAM users and roles in the affected member accounts; grants nothing by itself.

Resource control policy (RCP)

An organization root, OU or account

Sets the maximum permissions on supported resources in the affected accounts, whoever the caller is.

Managed policies are reusable and versioned; inline policies are embedded in a single identity and deleted with it. Prefer customer managed policies so they can be reviewed, reused and analysed. A resource-based policy is the only type that can grant access to a principal in another account without that account also delegating it.

A least-privilege identity policy for the Bookshelf catalog-api task role, allowing only the uploads bucket:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadWriteUploads",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::bookshelf-uploads-111122223333/*"
    },
    {
      "Sid": "ListUploads",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::bookshelf-uploads-111122223333",
      "Condition": {
        "StringLike": { "s3:prefix": ["covers/*"] }
      }
    }
  ]
}

Policy variables make one policy reusable across identities; this one lets each person manage only their own object prefix (variables appear only inside the JSON):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:*",
      "Resource": "arn:aws:s3:::bookshelf-uploads-111122223333/home/${aws:username}/*"
    }
  ]
}

Policy evaluation logic

By default every request is denied. For a request within one account, AWS evaluates all applicable policies and reaches a decision in this order of precedence:

  1. An explicit deny in any applicable policy (identity, resource, boundary, session, SCP, RCP) ends evaluation with deny.

  2. If the account belongs to an organization, applicable RCPs and SCPs must each allow the action; otherwise the result is an implicit deny.

  3. A resource-based policy that allows the calling principal is enough on its own for a same-account caller (an IAM user or role in the same account), except when the resource policy names a session ARN.

  4. An identity-based policy must allow the action, and any permissions boundary and session policy must also allow it.

  5. Anything else is an implicit deny.

flowchart TD A[Request] --> B{Explicit Deny in any policy?} B -- yes --> D[Deny] B -- no --> C{Organization RCP and SCP allow?} C -- no --> D C -- yes --> E{Resource-based policy allows principal?} E -- yes, same account --> G[Allow] E -- no or cross-account --> F{Identity-based policy allows?} F -- no --> D F -- yes --> H{Boundary and session policy allow?} H -- no --> D H -- yes --> G

Cross-account access needs both sides: the caller’s identity policy must allow the action and the resource’s resource-based policy must allow the caller. The IAM Policy Simulator and the policy-simulation API let you test a policy against actions and resources without making the calls:

aws iam simulate-principal-policy \
  --policy-source-arn "arn:aws:iam::${ACCOUNT_ID}:role/bookshelf-catalog-api-task" \
  --action-names s3:PutObject s3:DeleteObject \
  --resource-arns "arn:aws:s3:::bookshelf-uploads-${ACCOUNT_ID}/covers/1.png" \
  --query 'EvaluationResults[].[EvalActionName,EvalDecision]' --output table

Roles and trust policies

A role has two policy attachments with different jobs. The trust policy (a resource-based policy on the role) says who may assume it; the permissions policies say what the role may do once assumed. Assuming the wrong role fails at the trust policy; a role that is assumed but cannot act fails at the permissions policy.

In the AWS Management Console

  1. Open the IAM console, make sure the account is correct (IAM has no Region selector), and select Roles → Create role.

  2. Choose the Trusted entity type: AWS service (for example Elastic Container Service Task), AWS account, Web identity or SAML 2.0 federation.

  3. Select the permissions policies to attach (customer managed policies from the previous section are ideal), and optionally a permissions boundary.

  4. Name the role (bookshelf-catalog-api-task), review the trust policy JSON, and select Create role.

With the AWS CLI

export ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)"

cat > ecs-tasks-trust.json <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "ecs-tasks.amazonaws.com" },
    "Action": "sts:AssumeRole"
  }]
}
EOF

aws iam create-role \
  --role-name bookshelf-catalog-api-task \
  --assume-role-policy-document file://ecs-tasks-trust.json

# inline policy from the least-privilege JSON above, saved as uploads-policy.json
aws iam put-role-policy \
  --role-name bookshelf-catalog-api-task \
  --policy-name uploads-access \
  --policy-document file://uploads-policy.json

# or attach a managed policy
aws iam attach-role-policy \
  --role-name bookshelf-catalog-api-task \
  --policy-arn arn:aws:iam::aws:policy/AmazonSQSReadOnlyAccess

Roles for AWS services

The same mechanism backs every compute service; only the trusted service principal and the name of the thing you attach differ.

Service Role concept Notes

Amazon EC2

Instance profile: a container that passes an IAM role to an instance

The console creates the instance profile for you; with the CLI use aws iam create-instance-profile and add-role-to-instance-profile. Credentials arrive through the instance metadata service; require IMDSv2. See Amazon EC2 and Auto Scaling.

Amazon ECS

Task role (what your containers can do) and task execution role (what the ECS agent needs to pull images, write logs and fetch injected secrets)

Keep them separate. See ECS and Fargate.

AWS Lambda

Execution role (what the function can do), plus a resource-based policy (who may invoke it)

See AWS Lambda.

Amazon EKS

EKS Pod Identity (recommended) or IAM roles for service accounts (IRSA) per Kubernetes service account

IRSA remains for cases Pod Identity does not cover, such as Fargate pods. See EKS Clusters and Deploying Microservices on EKS.

STS, assume-role and temporary credentials

The AWS Security Token Service issues temporary credentials made of an access key ID, a secret access key and a session token, which expire (by default after one hour for AssumeRole; the role’s maximum session duration bounds the request). Because they expire on their own, a leaked temporary credential has a bounded blast radius.

sequenceDiagram participant U as Developer or workload participant STS as AWS STS participant IAM as IAM (role and trust policy) participant S3 as AWS service (S3) U->>STS: AssumeRole(role ARN, session name, optional session policy) STS->>IAM: Is the caller allowed by the role trust policy? IAM-->>STS: Trust policy permits caller STS-->>U: Temporary credentials (key ID, secret, session token, expiry) U->>S3: Signed request with temporary credentials S3->>IAM: Evaluate role permissions, boundary and session policy IAM-->>S3: Allow or deny S3-->>U: Response

For an assume-role call to succeed the caller needs two things: permission in its own identity policy to call sts:AssumeRole on the role, and a trust policy on the role that names the caller (or its account). Within an account either side can be sufficient depending on how the principal is named; across accounts both are required.

With the AWS CLI

A developer role in the same account, requiring MFA to assume it (Bookshelf bookshelf-developer):

cat > developer-trust.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "AWS": "arn:aws:iam::${ACCOUNT_ID}:root" },
    "Action": "sts:AssumeRole",
    "Condition": { "Bool": { "aws:MultiFactorAuthPresent": "true" } }
  }]
}
EOF

aws iam create-role --role-name bookshelf-developer \
  --assume-role-policy-document file://developer-trust.json \
  --max-session-duration 3600

# who am I right now?
aws sts get-caller-identity

# assume the role and capture temporary credentials
CREDS="$(aws sts assume-role \
  --role-arn "arn:aws:iam::${ACCOUNT_ID}:role/bookshelf-developer" \
  --role-session-name alberto-dev \
  --query Credentials --output json)"

export AWS_ACCESS_KEY_ID="$(echo "$CREDS" | jq -r .AccessKeyId)"
export AWS_SECRET_ACCESS_KEY="$(echo "$CREDS" | jq -r .SecretAccessKey)"
export AWS_SESSION_TOKEN="$(echo "$CREDS" | jq -r .SessionToken)"
aws sts get-caller-identity   # now shows the assumed-role ARN

Exporting credentials by hand is shown only to make the mechanism visible. The CLI can do the exchange and caching for you with a profile that sets role_arn and source_profile in ~/.aws/config, and the console offers Switch role from the account menu.

IAM Identity Center for people

IAM Identity Center (the service called AWS SSO in older material; renamed in 2022) is the recommended way to give people access to one or many accounts. You create users and groups in its identity store (or connect an external identity provider), define permission sets (collections of policies), and assign a group plus a permission set to an account. Identity Center then provisions a role in that account and users sign in to an access portal and receive short-lived credentials for the CLI. Enabling it for an organization is covered in Getting Started with AWS.

In the AWS Management Console

  1. Open the IAM Identity Center console in your home Region and choose Enable (an organization instance is the default for multi-account use).

  2. Select Groups → Create group (bookshelf-admins) and add users under Users → Add user.

  3. Select Permission sets → Create permission set, choose a predefined set such as PowerUserAccess or a custom one, and set the session duration.

  4. Select AWS accounts, choose the account, then Assign users or groups and pick the group and permission set.

  5. Share the AWS access portal URL with users; they sign in, enrol MFA and pick Access keys or the CLI profile instructions.

With the AWS CLI

# instance ARN and identity store of the Identity Center instance
INSTANCE_ARN="$(aws sso-admin list-instances --query 'Instances[0].InstanceArn' --output text)"
STORE_ID="$(aws sso-admin list-instances --query 'Instances[0].IdentityStoreId' --output text)"

PS_ARN="$(aws sso-admin create-permission-set \
  --instance-arn "$INSTANCE_ARN" \
  --name BookshelfDeveloper \
  --session-duration PT8H \
  --query PermissionSet.PermissionSetArn --output text)"

aws sso-admin attach-managed-policy-to-permission-set \
  --instance-arn "$INSTANCE_ARN" \
  --permission-set-arn "$PS_ARN" \
  --managed-policy-arn arn:aws:iam::aws:policy/PowerUserAccess

GROUP_ID="$(aws identitystore list-groups --identity-store-id "$STORE_ID" \
  --filters AttributePath=DisplayName,AttributeValue=bookshelf-admins \
  --query 'Groups[0].GroupId' --output text)"

aws sso-admin create-account-assignment \
  --instance-arn "$INSTANCE_ARN" \
  --target-id "$ACCOUNT_ID" --target-type AWS_ACCOUNT \
  --permission-set-arn "$PS_ARN" \
  --principal-type GROUP --principal-id "$GROUP_ID"

On the developer’s machine the CLI side is one-time configuration followed by a browser sign-in; the profile name below is a placeholder:

aws configure sso          # prompts for the start URL, Region, account and permission set
aws sso login --profile bookshelf-dev
aws sts get-caller-identity --profile bookshelf-dev

Derived from Configuring IAM Identity Center authentication with the AWS CLI. Console-credential sign-in with aws login (CLI 2.32 and later) is an alternative for individual developers, and CloudShell needs no local credentials at all; neither is meant for CI.

The default credential chain

SDKs and the CLI look for credentials in a fixed order and use the first source that yields some, so the same code runs on a laptop, in CI and on AWS. For the AWS SDK for Java 2.x, DefaultCredentialsProvider checks, in order: Java system properties, environment variables, a web identity token (EKS IRSA and OIDC), the shared credentials/config profile files (including IAM Identity Center and role_arn profiles), container credentials (ECS task roles and EKS Pod Identity) and finally the EC2 instance profile through IMDS. The CLI follows a similar order that starts with command-line options. The consequence is practical: write no credential code, set no keys in a container, and attach the right role to the compute.

package com.example.bookshelf;

import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.ListObjectsV2Request;

public class UploadsLister {
    public static void main(String[] args) {
        try (S3Client s3 = S3Client.builder()
                .region(Region.US_EAST_1)
                // optional: the default chain is already used when omitted
                .credentialsProvider(DefaultCredentialsProvider.create())
                .build()) {
            s3.listObjectsV2Paginator(ListObjectsV2Request.builder()
                            .bucket(System.getenv("BUCKET"))
                            .prefix("covers/")
                            .build())
                    .contents()
                    .forEach(o -> System.out.println(o.key()));
        }
    }
}

To assume a role explicitly (for example a cross-account role) use the STS credentials provider, which refreshes the session before it expires (add the sts module to the build):

StsClient sts = StsClient.builder().region(Region.US_EAST_1).build();

StsAssumeRoleCredentialsProvider provider = StsAssumeRoleCredentialsProvider.builder()
        .stsClient(sts)
        .refreshRequest(r -> r
                .roleArn("arn:aws:iam::111122223333:role/bookshelf-developer")
                .roleSessionName("catalog-api"))
        .build();

S3Client s3 = S3Client.builder()
        .region(Region.US_EAST_1)
        .credentialsProvider(provider)
        .build();

Retired and legacy to avoid: the AWS SDK for Java 1.x reached end of support on 2025-12-31, and the AWS CLI v1 is heading to end of support in 2027; use SDK 2.x and CLI v2.

Federation and OIDC for GitHub Actions

Workload federation replaces stored keys with a trust relationship. An IAM OIDC identity provider in your account trusts an external issuer, a role’s trust policy limits which tokens may use it, and AssumeRoleWithWebIdentity exchanges the issuer’s signed JWT for temporary credentials. SAML 2.0 federation works the same way for corporate directories, and IAM Roles Anywhere extends the idea to on-premises workloads holding an X.509 certificate.

For GitHub Actions the issuer is https://token.actions.githubusercontent.com, the audience is sts.amazonaws.com, and the sub claim identifies the repository and the ref or environment of the run. GitHub documents that repositories created after 2026-07-15 emit an immutable sub that embeds the owner and repository IDs (for example repo:octo-org@123456/octo-repo@456789:ref:refs/heads/main), while older repositories keep the name-only format shown below unless an administrator opts in, so check which format your repository emits before copying a trust condition; see Developer Tools and DevOps for the details. The workflow mechanics — the id-token: write permission, and how the credentials step fits with build and push steps — are covered on Docker CI/CD with GitHub Actions; this page covers the AWS side.

sequenceDiagram participant GH as GitHub Actions job participant OIDC as GitHub OIDC provider participant STS as AWS STS participant AWS as AWS services (ECR, ECS) GH->>OIDC: Request ID token (id-token write permission) OIDC-->>GH: Signed JWT (iss, aud, sub claims) GH->>STS: AssumeRoleWithWebIdentity(role ARN, JWT) STS->>STS: Verify signature, aud and role trust policy conditions on sub STS-->>GH: Temporary credentials GH->>AWS: Push image and deploy with the role

The trust policy is the control point. Match aud exactly and scope sub as narrowly as the workflow allows (repo:<org>/<repo>:ref:refs/heads/main, repo:<org>/<repo>:environment:<name> or repo:<org>/<repo>:pull_request); a wildcard on the organization alone lets any repository there assume the role.

In the AWS Management Console

  1. In the IAM console select Identity providers → Add provider → OpenID Connect.

  2. Enter https://token.actions.githubusercontent.com as the provider URL and sts.amazonaws.com as the audience, then Add provider. AWS validates GitHub’s certificate through its trusted root CAs, so you no longer need to manage a thumbprint.

  3. Select Roles → Create role → Web identity, choose that provider and audience, and enter the GitHub organization, repository and branch.

  4. Attach a policy limited to what the pipeline deploys (ECR push and the ECS update actions for Bookshelf), name the role bookshelf-github-actions-deploy, and create it.

  5. Copy the role ARN into the workflow (it is an identifier, not a secret).

With the AWS CLI

aws iam create-open-id-connect-provider \
  --url https://token.actions.githubusercontent.com \
  --client-id-list sts.amazonaws.com

cat > github-trust.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "Federated": "arn:aws:iam::${ACCOUNT_ID}:oidc-provider/token.actions.githubusercontent.com"
    },
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringEquals": {
        "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
      },
      "StringLike": {
        "token.actions.githubusercontent.com:sub": "repo:<org>/bookshelf-catalog-api:ref:refs/heads/main"
      }
    }
  }]
}
EOF

aws iam create-role --role-name bookshelf-github-actions-deploy \
  --assume-role-policy-document file://github-trust.json

aws iam attach-role-policy --role-name bookshelf-github-actions-deploy \
  --policy-arn arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser

The workflow side, for reference, uses the official action:

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v6
        with:
          role-to-assume: arn:aws:iam::111122223333:role/bookshelf-github-actions-deploy
          aws-region: us-east-1
      - run: aws sts get-caller-identity

Amazon Cognito

Amazon Cognito is for the customers of your application, not for your engineers. A user pool is a user directory and OIDC/OAuth 2.0 identity provider: sign-up and sign-in, MFA and passkeys, social and SAML sign-in, and JWTs your API can validate. An identity pool exchanges a token from a user pool or another provider for temporary AWS credentials scoped by a role, which is useful when a mobile or browser client must call AWS (an S3 upload, for instance) directly. Bookshelf’s catalog-api would validate user-pool JWTs as an OAuth 2.0 resource server; the protocol side is covered in Social Login and Federation, Spring Security, Authorization Server & Social Login and, for Quarkus, JWT, OIDC and Keycloak. Cognito as an API Gateway authorizer appears in API Gateway.

Least privilege with IAM Access Analyzer

Start with broad permissions in a development account, then tighten using evidence. IAM Access Analyzer adds three tools to that loop:

  • Policy generation builds a least-privilege policy from the CloudTrail activity of a role over a chosen period.

  • External access findings report resources (buckets, roles, KMS keys, queues) shared outside your zone of trust, and unused access findings report unused roles, keys and permissions.

  • Policy validation and custom checks lint a policy before you deploy it.

Credential reports and the "last accessed" data on each identity are the manual equivalents.

In the AWS Management Console

  1. Open the IAM console and select Access Analyzer.

  2. Choose Create analyzer, select External access or Unused access and the zone of trust (this account or the organization), and create it.

  3. To generate a policy, open the role, select the Permissions tab and, under Generate policy based on CloudTrail events, choose Generate policy; review the generated actions and save the result as a customer managed policy.

  4. Review Findings regularly and archive intended ones.

With the AWS CLI

aws accessanalyzer create-analyzer --analyzer-name bookshelf-external --type ACCOUNT

aws accessanalyzer list-findings-v2 \
  --analyzer-arn "arn:aws:access-analyzer:us-east-1:${ACCOUNT_ID}:analyzer/bookshelf-external" \
  --query 'findings[].[resourceType,resource,status]' --output table

# credential report: find old keys and users without MFA
aws iam generate-credential-report
aws iam get-credential-report --query Content --output text | base64 -d

Permissions boundaries and guardrails

A permissions boundary lets you delegate role creation safely: a developer may create roles, but only if the new role carries the boundary, and the boundary caps what any such role can do.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:*", "sqs:*", "dynamodb:*", "logs:*"],
      "Resource": "*"
    },
    {
      "Effect": "Deny",
      "Action": ["iam:*", "organizations:*", "account:*"],
      "Resource": "*"
    }
  ]
}

Attach it with aws iam put-role-permissions-boundary --role-name <name> --permissions-boundary <policy-arn>. An SCP applies the same idea at account level: a deny-list SCP such as "deny everything outside approved Regions" is attached in the organization (see Accounts and Organizations) and cannot be overridden by anything inside the account, root user included.

Passwords, MFA and the root user

  • Enable MFA on the root user (a passkey, security key or authenticator app) and register more than one device; use the root user only for tasks that need it.

  • Prefer passkeys and security keys for human users; IAM Identity Center enforces MFA centrally.

  • If you still have IAM users, set an account password policy and delete unused access keys.

  • In an organization, use centralized root access so that member accounts have no root credentials to protect.

aws iam update-account-password-policy \
  --minimum-password-length 14 --require-symbols --require-numbers \
  --max-password-age 90 --password-reuse-prevention 24

Pricing and limits

IAM, IAM Identity Center, STS and Access Analyzer’s external-access analyzers carry no direct charge; unused-access analysis and Cognito are billed by usage (Cognito by monthly active users and feature tier), so check the dated Cognito pricing and Access Analyzer pricing pages. IAM has quotas on the size and number of policies, roles and attached managed policies per identity, and on role session duration, that can be raised in some cases; see IAM and AWS STS quotas.

Clean up

Remove what this page created in dependency order: detach managed policies and delete inline policies from each role, remove roles from instance profiles, then delete the roles, the OIDC provider and the analyzer. Delete the Identity Center assignment before its permission set.

aws iam detach-role-policy --role-name bookshelf-github-actions-deploy \
  --policy-arn arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser
aws iam delete-role --role-name bookshelf-github-actions-deploy

aws iam delete-role-policy --role-name bookshelf-catalog-api-task --policy-name uploads-access
aws iam detach-role-policy --role-name bookshelf-catalog-api-task \
  --policy-arn arn:aws:iam::aws:policy/AmazonSQSReadOnlyAccess
aws iam delete-role --role-name bookshelf-catalog-api-task

aws iam delete-role --role-name bookshelf-developer

aws iam delete-open-id-connect-provider \
  --open-id-connect-provider-arn "arn:aws:iam::${ACCOUNT_ID}:oidc-provider/token.actions.githubusercontent.com"

aws accessanalyzer delete-analyzer --analyzer-name bookshelf-external

aws sso-admin delete-account-assignment \
  --instance-arn "$INSTANCE_ARN" --target-id "$ACCOUNT_ID" --target-type AWS_ACCOUNT \
  --permission-set-arn "$PS_ARN" --principal-type GROUP --principal-id "$GROUP_ID"
aws sso-admin delete-permission-set --instance-arn "$INSTANCE_ARN" --permission-set-arn "$PS_ARN"