AWSTemplateFormatVersion: '2010-09-09'

# GENERATED by scripts/marketplace/generate-iam-prerequisites.py — do not edit by hand.
# The role definitions are extracted from autonoma-secure-quicklaunch.yaml at build time,
# so they cannot drift from the roles the product actually uses. Re-run the generator.

Description: >-
  Autonoma Quick Launch — IAM roles, for accounts that do not permit the product template
  to create them. Deploy this ONCE with an identity that can create IAM roles, then pass
  the seven ARNs it outputs to the matching Existing*RoleArn parameters on the Autonoma
  Quick Launch template. Use the SAME CustomerId for both stacks: these policies are
  scoped by that name. If your account lets the product template create its own roles,
  you do not need this stack at all.

# READ THIS BEFORE YOUR SECURITY REVIEW DOES.
#
# These roles are the same seven the product template creates, with one difference that
# is deliberate and cannot be avoided: four of them grant on resources the PRODUCT stack
# creates later, which do not exist while this stack runs. Those grants are therefore
# scoped by NAME PATTERN rather than by exact ARN:
#
#     secretsmanager  secret:autonoma-<CustomerId>-*
#     ecs cluster     cluster/autonoma-<CustomerId>
#     ecs taskdef     task-definition/autonoma-<CustomerId>-db-init:*
#     cloudwatch logs log-group:/aws/ecs/autonoma-<CustomerId>/otel-*:*
#     efs             file-system/*, conditioned on tag Name=autonoma-<CustomerId>-efs
#
# A pattern covers resources that do not exist yet, which is the point — and also means
# it covers ones created later under the same name. Every pattern is confined to your own
# autonoma-<CustomerId> prefix. If that is broader than your policy allows, create the
# roles yourself from §2 of the buyer disclosure and tighten the ARNs once the product
# stack exists; the product template accepts any role ARN you pass it.

Parameters:
  CustomerId:
    Type: String
    Description: >-
      The SAME customer id you will give the Autonoma Quick Launch template. These
      policies are scoped by it, so a mismatch produces roles that cannot reach anything.
    AllowedPattern: '^[a-z0-9][a-z0-9-]{0,18}$'
    ConstraintDescription: >-
      1-19 chars, lowercase alphanumeric and hyphens, must start with alphanumeric.

  RolePath:
    Type: String
    Default: /
    AllowedPattern: '^\/([\w+=,.@-]+\/)*$'
    Description: >-
      Optional IAM path for these roles. Must match the RolePath you give the product
      template, or its iam:PassRole grants will not match the ARNs you pass back.

  RolePermissionsBoundary:
    Type: String
    Default: ''
    Description: >-
      Optional permissions boundary ARN applied to all seven roles. If your account
      requires a boundary on role creation, this is where it goes.

  RoleNamePrefix:
    Type: String
    Default: 'autonoma-'
    AllowedPattern: '^[a-zA-Z0-9+=,.@_-]{1,24}$'
    Description: >-
      Must MATCH the value you give the product template — it passes these roles by ARN,
      and the ARN contains the name. 24 characters max: the longest role is
      <prefix><CustomerId>-ecs-task-test-runner and IAM caps role names at 64.

  RoleTagKey1:
    Type: String
    Default: ''
    Description: >-
      Optional tag applied to all seven roles, for accounts that refuse iam:CreateRole
      without required tags. Unlike the product template, this one has no practical limit
      on how many you can add — edit the generated Tags block if you need more than three.
  RoleTagValue1:
    Type: String
    Default: ''
    Description: Value for RoleTagKey1.
  RoleTagKey2:
    Type: String
    Default: ''
    Description: Optional second tag key.
  RoleTagValue2:
    Type: String
    Default: ''
    Description: Value for RoleTagKey2.
  RoleTagKey3:
    Type: String
    Default: ''
    Description: Optional third tag key.
  RoleTagValue3:
    Type: String
    Default: ''
    Description: Value for RoleTagKey3.

  EnablePullThroughCache:
    Type: String
    AllowedValues: ['true', 'false']
    Default: 'true'
    Description: >-
      Must MATCH the value you give the product template. The execution role needs
      ecr:CreateRepository and BatchImportUpstreamImage to materialise the cache on
      first pull; without them the first image pull is denied and presents as a network
      failure. Set false only if you are also setting it false on the product stack.

  PullThroughPrefix:
    Type: String
    AllowedPattern: ^[a-z0-9][a-z0-9._/-]{0,20}$
    Default: 'autonoma-ptc'
    Description: >-
      Must MATCH the value you give the product template. The execution role's cache
      policy is scoped to repositories under this prefix, so a mismatch produces a role
      that cannot pull.

Conditions:
  HasRolePermissionsBoundary: !Not [!Equals [!Ref RolePermissionsBoundary, '']]
  UsePullThroughCache: !Equals [!Ref EnablePullThroughCache, 'true']
  HasRoleTag1: !Not [!Equals [!Ref RoleTagKey1, '']]
  HasRoleTag2: !Not [!Equals [!Ref RoleTagKey2, '']]
  HasRoleTag3: !Not [!Equals [!Ref RoleTagKey3, '']]
  HasAnyRoleTag: !Or [!Condition HasRoleTag1, !Condition HasRoleTag2, !Condition HasRoleTag3]

Resources:
  ExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      PermissionsBoundary: !If [HasRolePermissionsBoundary, !Ref RolePermissionsBoundary, !Ref 'AWS::NoValue']
      Tags: !If
        - HasAnyRoleTag
        - - !If [HasRoleTag1, {Key: !Ref RoleTagKey1, Value: !Ref RoleTagValue1}, !Ref 'AWS::NoValue']
          - !If [HasRoleTag2, {Key: !Ref RoleTagKey2, Value: !Ref RoleTagValue2}, !Ref 'AWS::NoValue']
          - !If [HasRoleTag3, {Key: !Ref RoleTagKey3, Value: !Ref RoleTagValue3}, !Ref 'AWS::NoValue']
        - !Ref 'AWS::NoValue'
      Path: !Ref 'RolePath'
      RoleName: !Sub '${RoleNamePrefix}${CustomerId}-ecs-execution'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal: { Service: ecs-tasks.amazonaws.com }
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
      Policies:
        # WITHOUT THESE THE FIRST PULL FAILS ON IAM, NOT ON NETWORK.
        # AmazonECSTaskExecutionRolePolicy grants GetAuthorizationToken,
        # BatchGetImage, BatchCheckLayerAvailability and GetDownloadUrlForLayer —
        # enough to pull an image that is ALREADY cached, and not enough to
        # create it. A pull-through cache materialises the repository on first
        # use, which needs CreateRepository and BatchImportUpstreamImage.
        # Verified 2026-09-01: with the stock policy alone the first pull is
        # denied, which presents as a pull failure and reads like a network
        # problem.
        - !If
          - UsePullThroughCache
          - PolicyName: !Sub 'autonoma-${CustomerId}-image-cache'
            PolicyDocument:
              Version: '2012-10-17'
              Statement:
                - Sid: PopulateTheImageCacheOnFirstPull
                  Effect: Allow
                  Action:
                    - ecr:CreateRepository
                    - ecr:BatchImportUpstreamImage
                  # Scoped to the cache prefix: this must not become a general
                  # create-any-repository grant on the buyer's registry.
                  Resource: !Sub 'arn:${AWS::Partition}:ecr:${AWS::Region}:${AWS::AccountId}:repository/${PullThroughPrefix}/*'
          - !Ref AWS::NoValue
        - PolicyName: !Sub 'autonoma-${CustomerId}-secrets-read'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action: [secretsmanager:GetSecretValue]
                Resource:
                  - !Sub 'arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:autonoma-${CustomerId}-*'
                  # Every valueFrom secret needs its own grant here or the task
                  # crash-loops on ResourceInitializationError before it logs anything.
                  - !Sub 'arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:autonoma-${CustomerId}-*'

  TaskRole:
    Type: AWS::IAM::Role
    Properties:
      PermissionsBoundary: !If [HasRolePermissionsBoundary, !Ref RolePermissionsBoundary, !Ref 'AWS::NoValue']
      Tags: !If
        - HasAnyRoleTag
        - - !If [HasRoleTag1, {Key: !Ref RoleTagKey1, Value: !Ref RoleTagValue1}, !Ref 'AWS::NoValue']
          - !If [HasRoleTag2, {Key: !Ref RoleTagKey2, Value: !Ref RoleTagValue2}, !Ref 'AWS::NoValue']
          - !If [HasRoleTag3, {Key: !Ref RoleTagKey3, Value: !Ref RoleTagValue3}, !Ref 'AWS::NoValue']
        - !Ref 'AWS::NoValue'
      Path: !Ref 'RolePath'
      RoleName: !Sub '${RoleNamePrefix}${CustomerId}-ecs-task'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal: { Service: ecs-tasks.amazonaws.com }
            Action: sts:AssumeRole
      Policies:
        - PolicyName: !Sub 'autonoma-${CustomerId}-efs-access'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - elasticfilesystem:ClientMount
                  - elasticfilesystem:ClientWrite
                  - elasticfilesystem:ClientRootAccess
                Resource: !Sub 'arn:${AWS::Partition}:elasticfilesystem:${AWS::Region}:${AWS::AccountId}:file-system/*'
                # The file system does not exist yet, so it cannot be named. A bare
                # wildcard here would grant ClientRootAccess on EVERY file system in the
                # account; the tag the main stack applies narrows it back to this
                # deployment's own. All three actions support aws:ResourceTag on the
                # file-system resource type.
                Condition:
                  StringEquals:
                    aws:ResourceTag/Name: !Sub 'autonoma-${CustomerId}-efs'
        # Scale-from-zero. The orchestrator wakes an idle agent before routing a step to
        # it (AgentScaler.EnsureAgentRunning); without these it logged
        #   AccessDeniedException: ... ecs:DescribeServices on .../autonoma-req-ai
        # and fell through to "attempting publish anyway", which only worked because the
        # agent happened to be running. An agent that had actually scaled to zero would
        # never wake, and the workflow would hang rather than error (observed 2026-08-12).
        #
        # Scoped to THIS deployment's cluster: the condition is what keeps a compromised
        # task from reading or scaling services in any other cluster in the buyer's
        # account. UpdateService is the write half - DescribeServices alone can observe
        # that an agent is down but not start it.
        - PolicyName: !Sub 'autonoma-${CustomerId}-agent-scaling'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - ecs:DescribeServices
                  - ecs:UpdateService
                Resource: !Sub 'arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:service/autonoma-${CustomerId}/*'
                Condition:
                  ArnEquals:
                    'ecs:cluster': !Sub 'arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:cluster/autonoma-${CustomerId}'
        - PolicyName: !Sub 'autonoma-${CustomerId}-forwarding-read'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action: [secretsmanager:GetSecretValue]
                Resource: !Sub 'arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:autonoma-${CustomerId}-*'
        - PolicyName: !Sub 'autonoma-${CustomerId}-otel-export'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              # otel-collector awsemf/awscloudwatchlogs exporters write to the
              # otel-metrics/otel-logs groups using the TASK role (not the exec
              # role, which only covers container stdout). Without this the
              # collector floods AccessDeniedException on logs:PutLogEvents.
              - Effect: Allow
                Action:
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                  - logs:DescribeLogStreams
                Resource:
                  - !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/ecs/autonoma-${CustomerId}/otel-metrics:*'
                  - !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/ecs/autonoma-${CustomerId}/otel-metrics:*'
                  - !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/ecs/autonoma-${CustomerId}/otel-logs:*'
                  - !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/ecs/autonoma-${CustomerId}/otel-logs:*'
              - Effect: Allow
                Action:
                  - xray:PutTraceSegments
                  - xray:PutTelemetryRecords
                  - xray:GetSamplingRules
                  - xray:GetSamplingTargets
                Resource: '*'
              # security-ai imports its findings into THIS account's Security Hub.
              #
              # The resource is pinned to ${AWS::AccountId} — the BUYER's account —
              # so the grant cannot be used to write findings anywhere else. That
              # pinning is the integration's privacy property expressed in IAM:
              # findings stay in the buyer's account and never transit our control
              # plane, because the only Security Hub this role can write to is the
              # buyer's own.
              #
              # BatchImportFindings only. Not GetFindings, not UpdateFindings, not
              # BatchUpdateFindings: the agent produces findings, it does not read
              # or resolve the buyer's existing ones.
              #
              # Verified against the live API 2026-08-10 with exactly this action
              # and resource shape, running as an ECS task role: 106 findings
              # imported, 0 rejected.
              - Effect: Allow
                Action:
                  - securityhub:BatchImportFindings
                Resource:
                  - !Sub 'arn:${AWS::Partition}:securityhub:${AWS::Region}:${AWS::AccountId}:product/${AWS::AccountId}/default'
        # The scan-worker's grant to LAUNCH the one-shot test-runner task (D7.15).
        # Modelled on DbInitFunctionRole/run-db-init below — same ArnEquals
        # ecs:cluster condition, same PassRole scoped to two named roles — because
        # that is the pattern this stack already proves, and on the control plane
        # the equivalent lives in infrastructure/aws/iam/test-runner/
        # caller-runtask-policy.json.
        #
        # SCOPE, STATED HONESTLY: this stack gives EVERY task one shared task role
        # (TaskRole), so this grant lands on the agents too, not on scan-worker
        # alone. scan-worker is the only process that calls RunTask, but the
        # blast radius of the grant is the whole fleet. Narrowing it means giving
        # scan-worker its own role and duplicating the three policies above onto
        # it; tracked rather than done silently.
        #
        # ecs:StopTask is NOT optional. FargateRunner.await stops the task on
        # deadline or context cancellation; without this action a hung suite
        # leaves a 2 vCPU / 4 GiB Fargate task running and billing by the second
        # with nothing left watching it.
        #
        # iam:PassRole is the statement to scope tightly — unscoped it lets the
        # holder start a task as ANY role in the account. The PassedToService
        # condition is not decoration.
        - PolicyName: !Sub 'autonoma-${CustomerId}-test-runner-launch'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Sid: LaunchTestRunnerTasksOnly
                Effect: Allow
                Action: [ecs:RunTask]
                Resource: !Sub 'arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:task-definition/autonoma-${CustomerId}-test-runner:*'
                Condition:
                  ArnEquals: { 'ecs:cluster': !Sub 'arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:cluster/autonoma-${CustomerId}' }
              - Sid: ObserveAndStopLaunchedTasks
                Effect: Allow
                Action: [ecs:DescribeTasks, ecs:StopTask]
                Resource: !Sub 'arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:task/autonoma-${CustomerId}/*'
                Condition:
                  ArnEquals: { 'ecs:cluster': !Sub 'arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:cluster/autonoma-${CustomerId}' }
              - Sid: PassOnlyTheTwoRolesThisTaskUses
                Effect: Allow
                Action: [iam:PassRole]
                Resource:
                  - !GetAtt ExecutionRole.Arn
                  - !GetAtt TestRunnerTaskRole.Arn
                Condition:
                  StringEquals: { 'iam:PassedToService': ecs-tasks.amazonaws.com }

  # ===========================================================================
  # Task role for autonoma-test-runner — GRANTS NOTHING, ON PURPOSE (D7.15).
  #
  # This task executes an arbitrary repository's build and test suite. `npm ci`,
  # `pip install` and `go mod download` all run third-party code, and the suite
  # itself is code nobody reviewed. Reusing TaskRole above would hand that code
  # the deployment's EFS client rights, its forwarding-secret read and (with the
  # launch policy just added) the ability to start further ECS tasks.
  #
  # Mirrors infrastructure/aws/iam/test-runner/ on the control plane:
  #   trust-policy.json      -> AssumeRolePolicyDocument, conditioned on this
  #                             account and this partition/region's ECS
  #   task-role-policy.json  -> the deny-credential-access inline policy
  #
  # WHY NO elasticfilesystem:Client* GRANT, even though the task mounts /workspace:
  # the EFS volume below is configured WITHOUT AuthorizationConfig, exactly like
  # every other task definition in this template, so ECS mounts it with the NFS
  # client rather than with IAM authorization and this role's identity is never
  # consulted. This template also creates no AWS::EFS::FileSystemPolicy, so the
  # file system applies its default (any client that can reach a mount target).
  # THE DAY SOMEONE ADDS A FILE-SYSTEM POLICY OR SETS Iam: ENABLED ON THE VOLUME,
  # this task loses its mount unless it is named there or granted ClientMount +
  # ClientWrite here. That is a standing dependency, not a permanent fact.
  #
  # The explicit Deny is worth carrying even though the Allow set is empty: an
  # empty policy that someone later "just adds one thing" to is how these roles
  # grow. ecr:GetAuthorizationToken is denied safely — the image pull runs under
  # the EXECUTION role, not this one.
  # ===========================================================================

  TestRunnerTaskRole:
    Type: AWS::IAM::Role
    Properties:
      PermissionsBoundary: !If [HasRolePermissionsBoundary, !Ref RolePermissionsBoundary, !Ref 'AWS::NoValue']
      Tags: !If
        - HasAnyRoleTag
        - - !If [HasRoleTag1, {Key: !Ref RoleTagKey1, Value: !Ref RoleTagValue1}, !Ref 'AWS::NoValue']
          - !If [HasRoleTag2, {Key: !Ref RoleTagKey2, Value: !Ref RoleTagValue2}, !Ref 'AWS::NoValue']
          - !If [HasRoleTag3, {Key: !Ref RoleTagKey3, Value: !Ref RoleTagValue3}, !Ref 'AWS::NoValue']
        - !Ref 'AWS::NoValue'
      Path: !Ref 'RolePath'
      RoleName: !Sub '${RoleNamePrefix}${CustomerId}-ecs-task-test-runner'
      Description: Task role for autonoma-test-runner. Runs untrusted third-party code. Grants nothing by design.
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: ECSTasksOnly
            Effect: Allow
            Principal: { Service: ecs-tasks.amazonaws.com }
            Action: sts:AssumeRole
            Condition:
              StringEquals: { 'aws:SourceAccount': !Ref AWS::AccountId }
              ArnLike: { 'aws:SourceArn': !Sub 'arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:*' }
      Policies:
        - PolicyName: !Sub 'autonoma-${CustomerId}-deny-credential-access'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Sid: DenyCredentialAndIdentityAccess
                Effect: Deny
                Action:
                  - secretsmanager:*
                  - ssm:GetParameter
                  - ssm:GetParameters
                  - ssm:GetParametersByPath
                  - sts:AssumeRole
                  - sts:GetFederationToken
                  - iam:*
                  - ecr:GetAuthorizationToken
                Resource: '*'
        - PolicyName: !Sub 'autonoma-${CustomerId}-package-mirror-read'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              # READ ONLY, AND THAT IS LOad-BEARING. The runner hands this token
              # to the installer, and the installer hands it to whatever
              # postinstall script it just downloaded — there is no arrangement
              # where the installer authenticates and untrusted code cannot see
              # how. The token is therefore not protected, it is made worthless:
              # read-only here, and requested with a 15-minute lifetime by
              # test-runner/package_mirror.go. Granting PublishPackageVersion or
              # any codeartifact:*Delete* here would turn a read credential into
              # a supply-chain write primitive.
              - Sid: ReadPackagesFromTheMirrorOnly
                Effect: Allow
                Action:
                  - codeartifact:GetAuthorizationToken
                  - codeartifact:GetRepositoryEndpoint
                  - codeartifact:ReadFromRepository
                Resource:
                  - !Sub 'arn:${AWS::Partition}:codeartifact:${AWS::Region}:${AWS::AccountId}:domain/autonoma-${CustomerId}'
                  - !Sub 'arn:${AWS::Partition}:codeartifact:${AWS::Region}:${AWS::AccountId}:repository/autonoma-${CustomerId}/*'
                  - !Sub 'arn:${AWS::Partition}:codeartifact:${AWS::Region}:${AWS::AccountId}:package/autonoma-${CustomerId}/*'
              # GetAuthorizationToken is refused without this, and it takes no
              # resource ARN of its own. The condition is what keeps it from
              # being a general bearer-token grant; the role's Deny statement
              # above covers sts:AssumeRole and sts:GetFederationToken but not
              # this action, so the condition is the only thing scoping it.
              - Sid: BearerTokenForCodeArtifactOnly
                Effect: Allow
                Action: sts:GetServiceBearerToken
                Resource: '*'
                Condition:
                  StringEquals: { 'sts:AWSServiceName': codeartifact.amazonaws.com }

  # ===========================================================================
  # Service discovery
  # ===========================================================================

  QuotaCheckFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      PermissionsBoundary: !If [HasRolePermissionsBoundary, !Ref RolePermissionsBoundary, !Ref 'AWS::NoValue']
      Tags: !If
        - HasAnyRoleTag
        - - !If [HasRoleTag1, {Key: !Ref RoleTagKey1, Value: !Ref RoleTagValue1}, !Ref 'AWS::NoValue']
          - !If [HasRoleTag2, {Key: !Ref RoleTagKey2, Value: !Ref RoleTagValue2}, !Ref 'AWS::NoValue']
          - !If [HasRoleTag3, {Key: !Ref RoleTagKey3, Value: !Ref RoleTagValue3}, !Ref 'AWS::NoValue']
        - !Ref 'AWS::NoValue'
      Path: !Ref 'RolePath'
      RoleName: !Sub '${RoleNamePrefix}${CustomerId}-quota-check-fn'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal: { Service: lambda.amazonaws.com }
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: read-fargate-quota
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              # Read-only, and the quota APIs do not accept a resource ARN - the
              # actions are account-scoped by design, so "*" here is the API's
              # shape, not a widened grant. GetServiceQuota returns an
              # account-raised value; GetAWSDefaultServiceQuota is the fallback
              # when the account has never been raised (the new-account case this
              # check exists for).
              - Effect: Allow
                Action:
                  - servicequotas:GetServiceQuota
                  - servicequotas:GetAWSDefaultServiceQuota
                Resource: '*'
              - Effect: Allow
                Action:
                  - secretsmanager:GetSecretValue
                Resource: !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:marketplace-deployment!*'
              # DescribePrefixLists takes no resource ARN — account/region scoped
              # by the API's own shape, like the quota calls above. Needed because
              # CloudFormation cannot resolve the AWS-managed S3 prefix list:
              # AWS::EC2::VPCEndpoint returns only CreationTimestamp, DnsEntries,
              # Id and NetworkInterfaceIds. Terraform gets it from the endpoint
              # resource directly and needs no equivalent.
              - Effect: Allow
                Action:
                  - ec2:DescribePrefixLists
                Resource: '*'

  AlbLogsBucketFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      PermissionsBoundary: !If [HasRolePermissionsBoundary, !Ref RolePermissionsBoundary, !Ref 'AWS::NoValue']
      Tags: !If
        - HasAnyRoleTag
        - - !If [HasRoleTag1, {Key: !Ref RoleTagKey1, Value: !Ref RoleTagValue1}, !Ref 'AWS::NoValue']
          - !If [HasRoleTag2, {Key: !Ref RoleTagKey2, Value: !Ref RoleTagValue2}, !Ref 'AWS::NoValue']
          - !If [HasRoleTag3, {Key: !Ref RoleTagKey3, Value: !Ref RoleTagValue3}, !Ref 'AWS::NoValue']
        - !Ref 'AWS::NoValue'
      Path: !Ref 'RolePath'
      RoleName: !Sub '${RoleNamePrefix}${CustomerId}-alb-logs-fn'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal: { Service: lambda.amazonaws.com }
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: adopt-or-create-logs-bucket
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              # Scoped to THIS bucket. No s3:DeleteBucket and no s3:DeleteObject:
              # the function must be incapable of destroying the logs, so a future
              # edit cannot quietly turn it into the empty-on-delete version that
              # would defeat the Retain policy.
              - Effect: Allow
                Action:
                  - s3:CreateBucket
                  - s3:HeadBucket
                  - s3:PutBucketPolicy
                  - s3:PutBucketPublicAccessBlock
                  - s3:PutEncryptionConfiguration
                  - s3:PutLifecycleConfiguration
                  - s3:PutBucketOwnershipControls
                  - s3:PutBucketTagging
                  - s3:GetBucketLocation
                Resource: !Sub 'arn:aws:s3:::autonoma-${CustomerId}-logs-${AWS::AccountId}-${AWS::Region}'

  DbInitFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      PermissionsBoundary: !If [HasRolePermissionsBoundary, !Ref RolePermissionsBoundary, !Ref 'AWS::NoValue']
      Tags: !If
        - HasAnyRoleTag
        - - !If [HasRoleTag1, {Key: !Ref RoleTagKey1, Value: !Ref RoleTagValue1}, !Ref 'AWS::NoValue']
          - !If [HasRoleTag2, {Key: !Ref RoleTagKey2, Value: !Ref RoleTagValue2}, !Ref 'AWS::NoValue']
          - !If [HasRoleTag3, {Key: !Ref RoleTagKey3, Value: !Ref RoleTagValue3}, !Ref 'AWS::NoValue']
        - !Ref 'AWS::NoValue'
      Path: !Ref 'RolePath'
      RoleName: !Sub '${RoleNamePrefix}${CustomerId}-dbinit-fn'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal: { Service: lambda.amazonaws.com }
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: run-db-init
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action: [ecs:RunTask]
                Resource: !Sub 'arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:task-definition/autonoma-${CustomerId}-db-init:*'
                Condition:
                  ArnEquals: { 'ecs:cluster': !Sub 'arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:cluster/autonoma-${CustomerId}' }
              - Effect: Allow
                Action: [ecs:DescribeTasks]
                Resource: '*'
                Condition:
                  ArnEquals: { 'ecs:cluster': !Sub 'arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:cluster/autonoma-${CustomerId}' }
              - Effect: Allow
                Action: [iam:PassRole]
                Resource:
                  - !GetAtt ExecutionRole.Arn
                  - !GetAtt TaskRole.Arn

  RegisterFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      PermissionsBoundary: !If [HasRolePermissionsBoundary, !Ref RolePermissionsBoundary, !Ref 'AWS::NoValue']
      Tags: !If
        - HasAnyRoleTag
        - - !If [HasRoleTag1, {Key: !Ref RoleTagKey1, Value: !Ref RoleTagValue1}, !Ref 'AWS::NoValue']
          - !If [HasRoleTag2, {Key: !Ref RoleTagKey2, Value: !Ref RoleTagValue2}, !Ref 'AWS::NoValue']
          - !If [HasRoleTag3, {Key: !Ref RoleTagKey3, Value: !Ref RoleTagValue3}, !Ref 'AWS::NoValue']
        - !Ref 'AWS::NoValue'
      Path: !Ref 'RolePath'
      RoleName: !Sub '${RoleNamePrefix}${CustomerId}-register-fn'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal: { Service: lambda.amazonaws.com }
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: write-forwarding-secret
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action: [secretsmanager:PutSecretValue, secretsmanager:GetSecretValue]
                Resource: !Sub 'arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:autonoma-${CustomerId}-*'
              - Effect: Allow
                Action:
                  - secretsmanager:GetSecretValue
                Resource: !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:marketplace-deployment!*'

Outputs:
  ExistingEcsExecutionRoleArn:
    Description: Pass this to the Quick Launch template's ExistingEcsExecutionRoleArn parameter (replaces the autonoma-<CustomerId>-ecs-execution role it would otherwise create).
    Value: !GetAtt ExecutionRole.Arn
    Export:
      Name: !Sub '${AWS::StackName}-ExistingEcsExecutionRoleArn'

  ExistingEcsTaskRoleArn:
    Description: Pass this to the Quick Launch template's ExistingEcsTaskRoleArn parameter (replaces the autonoma-<CustomerId>-ecs-task role it would otherwise create).
    Value: !GetAtt TaskRole.Arn
    Export:
      Name: !Sub '${AWS::StackName}-ExistingEcsTaskRoleArn'

  ExistingTestRunnerRoleArn:
    Description: Pass this to the Quick Launch template's ExistingTestRunnerRoleArn parameter (replaces the autonoma-<CustomerId>-ecs-task-test-runner role it would otherwise create).
    Value: !GetAtt TestRunnerTaskRole.Arn
    Export:
      Name: !Sub '${AWS::StackName}-ExistingTestRunnerRoleArn'

  ExistingQuotaCheckRoleArn:
    Description: Pass this to the Quick Launch template's ExistingQuotaCheckRoleArn parameter (replaces the autonoma-<CustomerId>-quota-check-fn role it would otherwise create).
    Value: !GetAtt QuotaCheckFunctionRole.Arn
    Export:
      Name: !Sub '${AWS::StackName}-ExistingQuotaCheckRoleArn'

  ExistingAlbLogsRoleArn:
    Description: Pass this to the Quick Launch template's ExistingAlbLogsRoleArn parameter (replaces the autonoma-<CustomerId>-alb-logs-fn role it would otherwise create).
    Value: !GetAtt AlbLogsBucketFunctionRole.Arn
    Export:
      Name: !Sub '${AWS::StackName}-ExistingAlbLogsRoleArn'

  ExistingDbInitRoleArn:
    Description: Pass this to the Quick Launch template's ExistingDbInitRoleArn parameter (replaces the autonoma-<CustomerId>-dbinit-fn role it would otherwise create).
    Value: !GetAtt DbInitFunctionRole.Arn
    Export:
      Name: !Sub '${AWS::StackName}-ExistingDbInitRoleArn'

  ExistingRegisterRoleArn:
    Description: Pass this to the Quick Launch template's ExistingRegisterRoleArn parameter (replaces the autonoma-<CustomerId>-register-fn role it would otherwise create).
    Value: !GetAtt RegisterFunctionRole.Arn
    Export:
      Name: !Sub '${AWS::StackName}-ExistingRegisterRoleArn'
