AWSTemplateFormatVersion: "2010-09-09"
Description: >-
  Real-Time Cloud Cost Control - read-only cross-account access role.
  Creates an IAM role the vendor assumes with a per-customer external ID, a dedicated Athena
  workgroup for CUR 2.0 queries, and (optionally) a results bucket for that workgroup.
  No write access to any workload resource. The only non-read actions are running Athena
  queries in the dedicated workgroup and writing their results to the results prefix.

Parameters:
  VendorAccountId:
    Type: String
    Description: AWS account ID of the vendor (the account that will assume the role).
    AllowedPattern: "^[0-9]{12}$"
  ExternalId:
    Type: String
    Description: Random external ID generated by the vendor for this customer (never reuse across customers).
    MinLength: 16
    NoEcho: true
  EnableCurQueries:
    Type: String
    Default: "true"
    AllowedValues: ["true", "false"]
    Description: true for engagements (CUR bucket read plus a dedicated Athena workgroup); false for the free cost scan (Cost Explorer only).
  CurBucketName:
    Type: String
    Default: ""
    Description: S3 bucket that receives your CUR 2.0 / Data Exports (Parquet, INCLUDE RESOURCES, split cost allocation enabled). Required when EnableCurQueries is true.
  CloudTrailBucketName:
    Type: String
    Default: ""
    Description: Optional. S3 bucket of your organization trail (for Athena queries over CloudTrail beyond the 90-day event history).
  AthenaWorkGroupName:
    Type: String
    Default: cloudcost-detective
    Description: Dedicated Athena workgroup; the role can only run queries here.
  CreateResultsBucket:
    Type: String
    Default: "true"
    AllowedValues: ["true", "false"]
    Description: Create a new bucket for Athena query results (true) or use an existing one (false).
  ExistingResultsBucketName:
    Type: String
    Default: ""
    Description: Only if CreateResultsBucket=false. Existing bucket for Athena results.
  AthenaResultsPrefix:
    Type: String
    Default: cloudcost-detective
    Description: Prefix inside the results bucket the role may write to.
  MaxBytesScannedPerQuery:
    Type: Number
    Default: 107374182400
    Description: Per-query data scan limit for the workgroup in bytes (default 100 GiB) - protects you from runaway queries.

Conditions:
  CurOn: !Equals [!Ref EnableCurQueries, "true"]
  HasTrailBucket: !Not [!Equals [!Ref CloudTrailBucketName, ""]]
  MakeBucket: !And [!Equals [!Ref CreateResultsBucket, "true"], !Equals [!Ref EnableCurQueries, "true"]]

Resources:
  ResultsBucket:
    Type: AWS::S3::Bucket
    Condition: MakeBucket
    DeletionPolicy: Retain
    UpdateReplacePolicy: Retain
    Properties:
      BucketName: !Sub "${AthenaWorkGroupName}-results-${AWS::AccountId}"
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      LifecycleConfiguration:
        Rules:
          - Id: expire-query-results
            Status: Enabled
            ExpirationInDays: 30

  DetectiveWorkGroup:
    Type: AWS::Athena::WorkGroup
    Condition: CurOn
    Properties:
      Name: !Ref AthenaWorkGroupName
      Description: Dedicated workgroup for Real-Time Cloud Cost Control CUR queries (read-only analysis).
      State: ENABLED
      WorkGroupConfiguration:
        EnforceWorkGroupConfiguration: true
        PublishCloudWatchMetricsEnabled: true
        BytesScannedCutoffPerQuery: !Ref MaxBytesScannedPerQuery
        ResultConfiguration:
          OutputLocation: !Sub
            - "s3://${Bucket}/${Prefix}/"
            - Bucket: !If [MakeBucket, !Ref ResultsBucket, !Ref ExistingResultsBucketName]
              Prefix: !Ref AthenaResultsPrefix
          EncryptionConfiguration:
            EncryptionOption: SSE_S3

  CostControlReadOnlyRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: CloudCostControlReadOnly
      Description: Read-only role assumed by the cost control vendor with an external ID.
      MaxSessionDuration: 3600
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              AWS: !Sub "arn:aws:iam::${VendorAccountId}:root"
            Action: sts:AssumeRole
            Condition:
              StringEquals:
                sts:ExternalId: !Ref ExternalId
      Policies:
        - PolicyName: CloudCostControlReadOnly
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Sid: BillingAndCostExplorerRead
                Effect: Allow
                Action:
                  - ce:GetCostAndUsage
                  - ce:GetCostAndUsageWithResources
                  - ce:GetDimensionValues
                  - ce:GetTags
                  - ce:GetCostCategories
                  - ce:ListCostAllocationTags
                  - ce:GetAnomalies
                  - ce:GetAnomalyMonitors
                  - ce:GetAnomalySubscriptions
                  - ce:GetSavingsPlansUtilization
                  - ce:GetSavingsPlansUtilizationDetails
                  - ce:GetSavingsPlansCoverage
                  - ce:GetReservationUtilization
                  - ce:GetReservationCoverage
                  - ce:GetRightsizingRecommendation
                  - ce:GetSavingsPlansPurchaseRecommendation
                  - ce:GetReservationPurchaseRecommendation
                  - ce:GetCostForecast
                  - ce:GetUsageForecast
                  - cur:DescribeReportDefinitions
                  - bcm-data-exports:ListExports
                  - bcm-data-exports:GetExport
                  - bcm-data-exports:ListExecutions
                  - bcm-data-exports:GetExecution
                  - bcm-data-exports:ListTables
                  - bcm-data-exports:GetTable
                  - savingsplans:DescribeSavingsPlans
                  - budgets:ViewBudget
                  - pricing:GetProducts
                  - pricing:DescribeServices
                  - pricing:GetAttributeValues
                  - organizations:DescribeOrganization
                  - organizations:ListAccounts
                  - sts:GetCallerIdentity
                Resource: "*"
              - Sid: RecommendationsRead
                Effect: Allow
                Action:
                  - cost-optimization-hub:ListRecommendations
                  - cost-optimization-hub:GetRecommendation
                  - cost-optimization-hub:ListRecommendationSummaries
                  - cost-optimization-hub:GetPreferences
                  - cost-optimization-hub:ListEnrollmentStatuses
                  - compute-optimizer:Get*
                  - compute-optimizer:Describe*
                Resource: "*"
              - Sid: IdentityAndChangeEvidenceRead
                Effect: Allow
                Action:
                  - cloudtrail:LookupEvents
                  - cloudtrail:DescribeTrails
                  - cloudtrail:GetTrailStatus
                  - cloudtrail:GetEventSelectors
                  - cloudtrail:ListTrails
                  - config:GetResourceConfigHistory
                  - config:ListDiscoveredResources
                  - config:DescribeConfigurationRecorders
                  - config:DescribeConfigurationRecorderStatus
                  - config:SelectResourceConfig
                  - iam:ListRoles
                  - iam:GetRole
                  - iam:ListRoleTags
                  - iam:ListAttachedRolePolicies
                  - iam:ListOpenIDConnectProviders
                  - iam:GetOpenIDConnectProvider
                  - events:ListRules
                  - events:DescribeRule
                  - events:ListTargetsByRule
                  - tag:GetResources
                  - tag:GetTagKeys
                  - tag:GetTagValues
                Resource: "*"
              - Sid: UsageSignalsRead
                Effect: Allow
                Action:
                  - cloudwatch:GetMetricData
                  - cloudwatch:GetMetricStatistics
                  - cloudwatch:ListMetrics
                  - cloudwatch:DescribeAlarms
                  - logs:DescribeLogGroups
                  - logs:DescribeQueries
                  - logs:DescribeQueryDefinitions
                  - logs:DescribeMetricFilters
                  - logs:DescribeSubscriptionFilters
                  - logs:ListTagsForResource
                  - athena:ListWorkGroups
                  - athena:GetWorkGroup
                  - athena:ListQueryExecutions
                  - athena:GetQueryExecution
                  - athena:BatchGetQueryExecution
                  - athena:ListDataCatalogs
                  - athena:ListDatabases
                  - athena:ListTableMetadata
                  - athena:GetTableMetadata
                  - ec2:Describe*
                  - eks:ListClusters
                  - eks:DescribeCluster
                  - eks:ListNodegroups
                  - eks:DescribeNodegroup
                  - eks:ListAddons
                  - eks:ListPodIdentityAssociations
                  - eks:DescribePodIdentityAssociation
                  - dynamodb:ListTables
                  - dynamodb:DescribeTable
                  - dynamodb:DescribeTimeToLive
                  - dynamodb:ListTagsOfResource
                  - application-autoscaling:Describe*
                  - elasticache:Describe*
                  - elasticache:ListTagsForResource
                  - rds:Describe*
                  - rds:ListTagsForResource
                  - lambda:ListFunctions
                  - lambda:GetFunctionConfiguration
                  - lambda:GetFunctionConcurrency
                  - lambda:ListEventSourceMappings
                  - lambda:ListTags
                  - s3:ListAllMyBuckets
                  - s3:GetBucketLocation
                  - s3:GetBucketLogging
                  - s3:GetLifecycleConfiguration
                  - s3:GetBucketVersioning
                  - s3:GetIntelligentTieringConfiguration
                  - s3:GetBucketTagging
                  - s3:GetMetricsConfiguration
                  - s3:GetBucketPolicyStatus
                  - s3:ListStorageLensConfigurations
                  - s3:GetStorageLensConfiguration
                  - bedrock:ListFoundationModels
                  - bedrock:GetModelInvocationLoggingConfiguration
                Resource: "*"
              - !If
                - CurOn
                - Sid: CurBucketRead
                  Effect: Allow
                  Action:
                    - s3:ListBucket
                    - s3:GetObject
                  Resource:
                    - !Sub "arn:aws:s3:::${CurBucketName}"
                    - !Sub "arn:aws:s3:::${CurBucketName}/*"
                - !Ref AWS::NoValue
              - !If
                - HasTrailBucket
                - Sid: TrailBucketRead
                  Effect: Allow
                  Action:
                    - s3:ListBucket
                    - s3:GetObject
                  Resource:
                    - !Sub "arn:aws:s3:::${CloudTrailBucketName}"
                    - !Sub "arn:aws:s3:::${CloudTrailBucketName}/*"
                - !Ref AWS::NoValue
              - !If
                - CurOn
                - Sid: CurQueriesInDedicatedWorkgroupOnly
                  Effect: Allow
                  Action:
                    - athena:StartQueryExecution
                    - athena:StopQueryExecution
                    - athena:GetQueryResults
                    - athena:GetQueryResultsStream
                  Resource: !Sub "arn:aws:athena:*:${AWS::AccountId}:workgroup/${AthenaWorkGroupName}"
                - !Ref AWS::NoValue
              - Sid: GlueCatalogReadForCur
                Effect: Allow
                Action:
                  - glue:GetDatabase
                  - glue:GetDatabases
                  - glue:GetTable
                  - glue:GetTables
                  - glue:GetPartition
                  - glue:GetPartitions
                Resource: "*"
              - !If
                - CurOn
                - Sid: AthenaResultsPrefixOnly
                  Effect: Allow
                  Action:
                    - s3:GetObject
                    - s3:PutObject
                    - s3:AbortMultipartUpload
                    - s3:ListBucket
                    - s3:GetBucketLocation
                  Resource:
                    - !Sub
                      - "arn:aws:s3:::${Bucket}"
                      - Bucket: !If [MakeBucket, !Ref ResultsBucket, !Ref ExistingResultsBucketName]
                    - !Sub
                      - "arn:aws:s3:::${Bucket}/${Prefix}/*"
                      - Bucket: !If [MakeBucket, !Ref ResultsBucket, !Ref ExistingResultsBucketName]
                        Prefix: !Ref AthenaResultsPrefix
                - !Ref AWS::NoValue

Outputs:
  RoleArn:
    Description: Share this ARN with the vendor.
    Value: !GetAtt CostControlReadOnlyRole.Arn
  WorkGroup:
    Condition: CurOn
    Description: Dedicated Athena workgroup for CUR queries.
    Value: !Ref DetectiveWorkGroup
  ResultsLocation:
    Condition: CurOn
    Description: Athena results location used by the workgroup.
    Value: !Sub
      - "s3://${Bucket}/${Prefix}/"
      - Bucket: !If [MakeBucket, !Ref ResultsBucket, !Ref ExistingResultsBucketName]
        Prefix: !Ref AthenaResultsPrefix
