OperatorProvisioningPermissionMatrix.java

package com.cloudforgeci.api.core.iam;

import com.cloudforge.core.enums.IAMProfile;

import java.util.List;
import java.util.Map;

/**
 * Permission matrix for the <b>operator provisioning</b> layer — the AWS actions CloudForge
 * Manager's own task role needs, acting as the calling principal, to actually create/manage the
 * infrastructure a {@code deploy:create} target application's synthesized CloudFormation template
 * describes (VPC, EFS, ALB, ECS cluster/service/task definition).
 *
 * <p><b>This is a different layer than {@link PermissionMatrix}.</b> {@code PermissionMatrix}
 * defines what a <i>deployed app's own task role</i> can do once it's running (pull its image,
 * read SSM params, put CloudWatch metrics). This class defines what <i>Manager's own role</i>
 * needs to bring that app's infrastructure into existence and tear it back down in the first
 * place — CloudFormation issues every one of these calls under Manager's identity, not the
 * deployed app's. A stack's own workload permissions being perfectly correct (which {@link
 * PermissionMatrix} already ensures) says nothing about whether Manager was ever allowed to
 * create that stack's VPC/EFS/ALB/ECS resources at all.</p>
 *
 * <p>Tiered by the same {@link IAMProfile} enum {@link PermissionMatrix} uses, not a parallel
 * concept — MINIMAL is read-only (inventory/troubleshooting, no condition since Describe- and
 * List-family actions don't accept a Tags parameter to condition on), STANDARD is full lifecycle
 * for the
 * single Fargate+ALB+EFS shape every catalog app in this platform actually deploys today, and
 * EXTENDED adds the NAT/EIP/flow-log surface a private-with-egress network topology needs.
 * {@link ManagerOperatorIamSupport#deployStatements} currently bakes in EXTENDED unconditionally
 * for Manager's own task role, since there is no per-user AWS-level distinction yet — see the
 * design note below for where that's headed.</p>
 *
 * <p><b>Future direction, not built yet:</b> Manager already has a real, working per-user RBAC
 * policy catalog ({@code ManagerPolicyCatalog}, {@code manager_user_policy}) with a {@code
 * deploy:create} capability gating this exact feature today, and a real {@code sts:AssumeRole}
 * pathway ({@code AssumeRoleOperations}/{@code StsAssumeRoleService}) already built for
 * cross-account connections. The intended seam: instead of a {@code deploy:create} request
 * running directly under Manager's task role credentials, Manager assumes its own (or a
 * dedicated operator) role with a session policy scoped to the {@link IAMProfile} tier this
 * matrix says that request needs — so a Manager user without the {@code deploy:create} RBAC
 * capability can never reach AWS-level infrastructure-creation capability even if something
 * upstream misbehaves, because the session policy would never carry those actions to begin with.
 * This class is written so that seam only ever needs one source of truth on the AWS side — never
 * two independently-maintained action lists drifting apart.</p>
 */
public final class OperatorProvisioningPermissionMatrix {
    private OperatorProvisioningPermissionMatrix() {
    }

    /**
     * {@link com.cloudforgeci.api.network.VpcFactory}'s {@code Vpc} L2 construct — every
     * underlying EC2 networking resource type it can synthesize depending on subnet
     * configuration (VpcFactory always creates public+private-with-egress subnet pairs across
     * 2 AZs for every app in this catalog, hence NAT/EIP/route-table actions live at STANDARD,
     * not EXTENDED, despite the class javadoc's general EXTENDED-adds-NAT framing above -- see
     * {@link #EXTENDED_ONLY_VPC_PERMISSIONS} for what genuinely is EXTENDED-only: flow logs and
     * custom network ACLs, neither of which VpcFactory enables by default).
     */
    public static final Map<IAMProfile, List<String>> VPC_PERMISSIONS = Map.of(
        IAMProfile.MINIMAL, List.of(
            "ec2:DescribeVpcs",
            "ec2:DescribeSubnets",
            "ec2:DescribeRouteTables",
            "ec2:DescribeInternetGateways",
            "ec2:DescribeNatGateways",
            "ec2:DescribeSecurityGroups",
            "ec2:DescribeAvailabilityZones",
            "ec2:DescribeAddresses",
            "ec2:DescribeTags",
            // Every EFS mount target IS an ENI under the hood -- CreateMountTarget/
            // DeleteMountTarget call these EC2 APIs on the caller's own identity to actually
            // create/tear it down, AWS's own documented required permissions for those two EFS
            // actions. Missing here surfaces as a generic EFS-side 403 ("User is not authorized to
            // perform that action on the specified resource") with no EC2 action named anywhere
            // in the error, since it's EFS relaying an authorization failure from its own
            // downstream EC2 call, not this app's own EC2 call.
            "ec2:DescribeNetworkInterfaces"
        ),
        IAMProfile.STANDARD, List.of(
            "ec2:CreateVpc",
            "ec2:DeleteVpc",
            "ec2:ModifyVpcAttribute",
            "ec2:CreateSubnet",
            "ec2:DeleteSubnet",
            "ec2:ModifySubnetAttribute",
            "ec2:CreateInternetGateway",
            "ec2:DeleteInternetGateway",
            "ec2:AttachInternetGateway",
            "ec2:DetachInternetGateway",
            "ec2:CreateRouteTable",
            "ec2:DeleteRouteTable",
            "ec2:CreateRoute",
            "ec2:DeleteRoute",
            "ec2:AssociateRouteTable",
            "ec2:DisassociateRouteTable",
            "ec2:CreateNatGateway",
            "ec2:DeleteNatGateway",
            "ec2:AllocateAddress",
            "ec2:ReleaseAddress",
            "ec2:AssociateAddress",
            "ec2:DisassociateAddress",
            "ec2:CreateSecurityGroup",
            "ec2:DeleteSecurityGroup",
            "ec2:AuthorizeSecurityGroupIngress",
            "ec2:AuthorizeSecurityGroupEgress",
            "ec2:RevokeSecurityGroupIngress",
            "ec2:RevokeSecurityGroupEgress",
            "ec2:CreateTags",
            "ec2:DeleteTags",
            // See the MINIMAL tier's ec2:DescribeNetworkInterfaces comment above -- the create/
            // delete/modify half of the same EFS-mount-target-is-an-ENI requirement.
            "ec2:CreateNetworkInterface",
            "ec2:DeleteNetworkInterface",
            "ec2:ModifyNetworkInterfaceAttribute"
        )
    );

    /** Flow logs and custom network ACLs -- not part of VpcFactory's default topology, only
     *  relevant for a compliance-driven or hardened network profile. */
    public static final List<String> EXTENDED_ONLY_VPC_PERMISSIONS = List.of(
        "ec2:CreateFlowLogs",
        "ec2:DeleteFlowLogs",
        "ec2:DescribeFlowLogs",
        "ec2:CreateNetworkAcl",
        "ec2:DeleteNetworkAcl",
        "ec2:CreateNetworkAclEntry",
        "ec2:DeleteNetworkAclEntry",
        "ec2:ReplaceNetworkAclAssociation",
        "ec2:DescribeNetworkAcls"
    );

    /** {@link com.cloudforgeci.api.storage.EfsFactory}'s {@code FileSystem}/{@code AccessPoint}
     *  L2 constructs. */
    public static final Map<IAMProfile, List<String>> EFS_PERMISSIONS = Map.of(
        IAMProfile.MINIMAL, List.of(
            "elasticfilesystem:DescribeFileSystems",
            "elasticfilesystem:DescribeAccessPoints",
            "elasticfilesystem:DescribeMountTargets",
            "elasticfilesystem:DescribeMountTargetSecurityGroups",
            "elasticfilesystem:DescribeLifecycleConfiguration",
            "elasticfilesystem:DescribeBackupPolicy",
            // CloudFormation checks for an existing replication configuration as part of
            // DeleteFileSystem's own preconditions, even on a file system that was never
            // replicated -- without this, deleting an EFS file system fails outright.
            "elasticfilesystem:DescribeReplicationConfigurations"
        ),
        IAMProfile.STANDARD, List.of(
            "elasticfilesystem:CreateFileSystem",
            "elasticfilesystem:DeleteFileSystem",
            "elasticfilesystem:UpdateFileSystem",
            "elasticfilesystem:CreateAccessPoint",
            "elasticfilesystem:DeleteAccessPoint",
            "elasticfilesystem:CreateMountTarget",
            "elasticfilesystem:DeleteMountTarget",
            "elasticfilesystem:ModifyMountTargetSecurityGroups",
            "elasticfilesystem:PutLifecycleConfiguration",
            "elasticfilesystem:PutBackupPolicy",
            "elasticfilesystem:TagResource",
            "elasticfilesystem:UntagResource",
            "elasticfilesystem:ListTagsForResource"
        )
    );

    /** {@link com.cloudforgeci.api.ingress.AlbFactory}'s {@code ApplicationLoadBalancer}/
     *  {@code ApplicationTargetGroup} L2 constructs. */
    public static final Map<IAMProfile, List<String>> ALB_PERMISSIONS = Map.of(
        IAMProfile.MINIMAL, List.of(
            "elasticloadbalancing:DescribeLoadBalancers",
            "elasticloadbalancing:DescribeLoadBalancerAttributes",
            "elasticloadbalancing:DescribeTargetGroups",
            "elasticloadbalancing:DescribeTargetGroupAttributes",
            "elasticloadbalancing:DescribeTargetHealth",
            "elasticloadbalancing:DescribeListeners",
            "elasticloadbalancing:DescribeRules",
            "elasticloadbalancing:DescribeTags",
            // The ELB service itself calls this EC2 API under the deploying caller's own identity
            // while handling elasticloadbalancing:CreateLoadBalancer, to check the account's
            // supported EC2-Classic/VPC platforms -- AWS's own documented required-permission for
            // CreateLoadBalancer, not something this app calls directly. Missing here surfaces as
            // "ec2:DescribeAccountAttributes ... (Service: ElasticLoadBalancingV2 ...)" the moment
            // a deploy actually tries to create an ALB, never earlier.
            "ec2:DescribeAccountAttributes"
        ),
        IAMProfile.STANDARD, List.of(
            "elasticloadbalancing:CreateLoadBalancer",
            "elasticloadbalancing:DeleteLoadBalancer",
            "elasticloadbalancing:ModifyLoadBalancerAttributes",
            "elasticloadbalancing:SetSecurityGroups",
            "elasticloadbalancing:SetSubnets",
            "elasticloadbalancing:SetIpAddressType",
            "elasticloadbalancing:CreateTargetGroup",
            "elasticloadbalancing:DeleteTargetGroup",
            "elasticloadbalancing:ModifyTargetGroup",
            "elasticloadbalancing:ModifyTargetGroupAttributes",
            "elasticloadbalancing:RegisterTargets",
            "elasticloadbalancing:DeregisterTargets",
            "elasticloadbalancing:CreateListener",
            "elasticloadbalancing:DeleteListener",
            "elasticloadbalancing:ModifyListener",
            "elasticloadbalancing:CreateRule",
            "elasticloadbalancing:DeleteRule",
            "elasticloadbalancing:ModifyRule",
            "elasticloadbalancing:AddTags",
            "elasticloadbalancing:RemoveTags"
        )
    );

    /** {@link com.cloudforgeci.api.compute.FargateFactory}'s {@code Cluster}/
     *  {@code FargateService}/{@code FargateTaskDefinition} L2 constructs -- registering and
     *  running the task definition, not the workload permissions the running task itself needs
     *  (that's {@link PermissionMatrix}). */
    public static final Map<IAMProfile, List<String>> ECS_PERMISSIONS = Map.of(
        IAMProfile.MINIMAL, List.of(
            "ecs:DescribeClusters",
            "ecs:DescribeServices",
            "ecs:DescribeTaskDefinition",
            "ecs:ListClusters",
            "ecs:ListServices",
            "ecs:ListTagsForResource"
        ),
        IAMProfile.STANDARD, List.of(
            "ecs:CreateCluster",
            "ecs:DeleteCluster",
            "ecs:PutClusterCapacityProviders",
            "ecs:CreateService",
            "ecs:DeleteService",
            "ecs:UpdateService",
            "ecs:RegisterTaskDefinition",
            "ecs:DeregisterTaskDefinition",
            "ecs:TagResource",
            "ecs:UntagResource"
        )
    );

    /** Manager creating the target app's own {@code AWS::Logs::LogGroup} as part of its
     *  infrastructure -- separate from {@link PermissionMatrix#CORE_PERMISSIONS}, which is what
     *  the deployed app's own task role needs to write into that log group at runtime. */
    public static final Map<IAMProfile, List<String>> LOGS_PERMISSIONS = Map.of(
        IAMProfile.MINIMAL, List.of(
            "logs:DescribeLogGroups",
            "logs:ListTagsForResource",
            // The Logs tab's actual event-fetching call (CloudWatchLogsStackOperations
            // #fetchLogEvents) -- every other action here only describes/manages the log group
            // itself, never reads what's actually in it, so this was missing entirely until a
            // live Logs tab request against a real stack surfaced it.
            "logs:FilterLogEvents"
        ),
        IAMProfile.STANDARD, List.of(
            "logs:CreateLogGroup",
            "logs:DeleteLogGroup",
            "logs:PutRetentionPolicy",
            "logs:TagResource",
            "logs:UntagResource"
        )
    );

    /**
     * {@link com.cloudforgeci.api.database.RdsFactory}'s {@code DatabaseInstance}/{@code
     * ParameterGroup}/{@code SubnetGroup} L2 constructs, plus the KMS key and Secrets Manager
     * secret every encrypted instance provisions alongside it -- grouped together, not split into
     * three separate maps, since {@code RdsFactory} always creates the three together for any app
     * whose {@code DatabaseSpec} requests a database (there is no "RDS without its own secret and
     * key" shape in this codebase to scope more narrowly than that). {@code kms:CreateKey}'s own
     * tagging step fails with "UnauthorizedTaggingOperation" without {@code kms:TagResource}
     * granted alongside it (see {@code ManagerOperatorIamSupport}'s {@code iamRoleCreate} for the
     * same CloudFormation error-classification label on a different action -- it denotes any
     * denied create-with-tags call, not a tag-condition mismatch specifically).
     */
    public static final Map<IAMProfile, List<String>> DATABASE_PERMISSIONS = Map.of(
        IAMProfile.MINIMAL, List.of(
            "rds:DescribeDBInstances",
            "rds:DescribeDBSubnetGroups",
            "rds:DescribeDBParameterGroups",
            "rds:DescribeDBParameters",
            // CloudFormation resolves the engine's own default parameter values before applying
            // ParameterGroup's custom overrides, even when every override is explicit -- without
            // this, DBParameterGroup creation fails outright.
            "rds:DescribeEngineDefaultParameters",
            "rds:ListTagsForResource",
            "kms:DescribeKey",
            "kms:ListAliases",
            "kms:GetKeyPolicy",
            "kms:GetKeyRotationStatus",
            "secretsmanager:DescribeSecret",
            "secretsmanager:ListSecrets"
        ),
        IAMProfile.STANDARD, List.of(
            "rds:CreateDBInstance",
            "rds:DeleteDBInstance",
            "rds:ModifyDBInstance",
            "rds:AddTagsToResource",
            "rds:RemoveTagsFromResource",
            "rds:CreateDBSubnetGroup",
            "rds:DeleteDBSubnetGroup",
            "rds:ModifyDBSubnetGroup",
            "rds:CreateDBParameterGroup",
            "rds:DeleteDBParameterGroup",
            "rds:ModifyDBParameterGroup",
            "rds:ResetDBParameterGroup",
            // KMS key lifecycle -- Key.Builder always enables key rotation and a DESTROY removal
            // policy in RdsFactory, so rotation/deletion/policy actions are needed alongside
            // create, not just CreateKey itself.
            "kms:CreateKey",
            "kms:CreateAlias",
            "kms:DeleteAlias",
            "kms:EnableKeyRotation",
            "kms:PutKeyPolicy",
            "kms:TagResource",
            "kms:UntagResource",
            "kms:ScheduleKeyDeletion",
            "kms:CancelKeyDeletion",
            // Secrets Manager -- Secret.Builder's generateSecretString is what actually calls
            // GetRandomPassword server-side; RemovalPolicy.DESTROY means a real DeleteSecret (not
            // just a scheduled deletion) has to work too.
            "secretsmanager:CreateSecret",
            "secretsmanager:DeleteSecret",
            "secretsmanager:GetRandomPassword",
            "secretsmanager:GetSecretValue",
            "secretsmanager:PutSecretValue",
            "secretsmanager:UpdateSecret",
            "secretsmanager:TagResource",
            "secretsmanager:UntagResource"
        )
    );

    /**
     * {@link com.cloudforgeci.api.observability.ComplianceFactory}/{@link
     * com.cloudforgeci.api.observability.GuardDutyFactory}/{@link
     * com.cloudforgeci.api.observability.WafFactory} -- deliberately its own dimension, not a
     * fourth {@link IAMProfile} tier, since compliance mode is an independent boolean toggle on
     * {@code DeploymentConfig} ({@code complianceMode}/{@code awsConfigEnabled}/{@code
     * guardDutyEnabled}), orthogonal to which IAMProfile tier an app's own workload role runs
     * under. AWS Config and GuardDuty are both account-level <i>singletons</i>, and enabling
     * either for the very first time in an account requires
     * {@code iam:CreateServiceLinkedRole} for that service's own service-linked role -- a step
     * with no equivalent in the VPC/EFS/ALB/ECS categories above, easy to miss because it's only
     * needed exactly once per account, not once per deployment.
     */
    public static final Map<IAMProfile, List<String>> COMPLIANCE_PERMISSIONS = Map.of(
        IAMProfile.MINIMAL, List.of(
            "config:DescribeConfigurationRecorders",
            "config:DescribeConfigurationRecorderStatus",
            "config:DescribeConfigRules",
            "config:DescribeConformancePacks",
            "config:DescribeDeliveryChannels",
            "config:DescribeRemediationConfigurations",
            "config:GetComplianceDetailsByConfigRule",
            "guardduty:GetDetector",
            "guardduty:ListDetectors",
            "wafv2:GetWebACL",
            "wafv2:ListWebACLs",
            "wafv2:GetLoggingConfiguration",
            "ssm:DescribeDocument",
            "ssm:GetDocument",
            "ssm:ListDocuments",
            "auditmanager:GetAssessment"
        ),
        IAMProfile.STANDARD, List.of(
            "config:PutConfigurationRecorder",
            "config:DeleteConfigurationRecorder",
            "config:StartConfigurationRecorder",
            "config:StopConfigurationRecorder",
            "config:PutDeliveryChannel",
            "config:DeleteDeliveryChannel",
            "config:PutConfigRule",
            "config:DeleteConfigRule",
            "config:PutConformancePack",
            "config:DeleteConformancePack",
            "config:PutRemediationConfigurations",
            "config:DeleteRemediationConfiguration",
            "config:TagResource",
            "config:UntagResource",
            "guardduty:CreateDetector",
            "guardduty:DeleteDetector",
            "guardduty:UpdateDetector",
            "guardduty:TagResource",
            "guardduty:UntagResource",
            "wafv2:CreateWebACL",
            "wafv2:DeleteWebACL",
            "wafv2:UpdateWebACL",
            "wafv2:PutLoggingConfiguration",
            "wafv2:DeleteLoggingConfiguration",
            "wafv2:AssociateWebACL",
            "wafv2:DisassociateWebACL",
            "wafv2:TagResource",
            "wafv2:UntagResource",
            "ssm:CreateDocument",
            "ssm:DeleteDocument",
            "ssm:AddTagsToResource",
            "auditmanager:CreateAssessment",
            "auditmanager:DeleteAssessment",
            "auditmanager:UpdateAssessment",
            "auditmanager:TagResource",
            // Account-level singleton services (Config, GuardDuty) need their own service-linked
            // role created the first time either is ever enabled in the account -- restricted to
            // exactly those two AWS service names, not a bare iam:CreateServiceLinkedRole grant.
            "iam:CreateServiceLinkedRole",
            "iam:GetServiceLinkedRoleDeletionStatus"
        )
    );

    /**
     * All actions needed at or below the given tier, across every provisioning category, for a
     * single flat action list -- mirrors {@link PermissionMatrix#getRequiredPermissions}'
     * additive-tier shape (STANDARD includes MINIMAL, EXTENDED includes STANDARD). {@code
     * includeCompliance} pulls in {@link #COMPLIANCE_PERMISSIONS} at the same tier, kept as a
     * separate parameter rather than a fourth tier value for the reason documented on that map.
     */
    public static List<String> getRequiredPermissions(IAMProfile tier, boolean includeCompliance) {
        List<String> actions = new java.util.ArrayList<>();
        actions.addAll(VPC_PERMISSIONS.get(IAMProfile.MINIMAL));
        actions.addAll(EFS_PERMISSIONS.get(IAMProfile.MINIMAL));
        actions.addAll(ALB_PERMISSIONS.get(IAMProfile.MINIMAL));
        actions.addAll(ECS_PERMISSIONS.get(IAMProfile.MINIMAL));
        actions.addAll(LOGS_PERMISSIONS.get(IAMProfile.MINIMAL));
        actions.addAll(DATABASE_PERMISSIONS.get(IAMProfile.MINIMAL));
        if (includeCompliance) {
            actions.addAll(COMPLIANCE_PERMISSIONS.get(IAMProfile.MINIMAL));
        }
        if (tier == IAMProfile.MINIMAL) {
            return List.copyOf(actions);
        }
        actions.addAll(VPC_PERMISSIONS.get(IAMProfile.STANDARD));
        actions.addAll(EFS_PERMISSIONS.get(IAMProfile.STANDARD));
        actions.addAll(ALB_PERMISSIONS.get(IAMProfile.STANDARD));
        actions.addAll(ECS_PERMISSIONS.get(IAMProfile.STANDARD));
        actions.addAll(LOGS_PERMISSIONS.get(IAMProfile.STANDARD));
        actions.addAll(DATABASE_PERMISSIONS.get(IAMProfile.STANDARD));
        if (includeCompliance) {
            actions.addAll(COMPLIANCE_PERMISSIONS.get(IAMProfile.STANDARD));
        }
        if (tier == IAMProfile.STANDARD) {
            return List.copyOf(actions);
        }
        actions.addAll(EXTENDED_ONLY_VPC_PERMISSIONS);
        return List.copyOf(actions);
    }

    /**
     * {@link #getRequiredPermissions}'s full action list, split roughly in half by measured JSON
     * byte size rather than by category count -- {@link #VPC_PERMISSIONS}/{@link #ALB_PERMISSIONS}/
     * {@link #EFS_PERMISSIONS} ("network") on one side, {@link #ECS_PERMISSIONS}/{@link
     * #LOGS_PERMISSIONS}/{@link #DATABASE_PERMISSIONS}/{@link #COMPLIANCE_PERMISSIONS}
     * ("compute/data") on the other. Exists because of a hard AWS ceiling: an IAM role's
     * <i>combined</i> inline-policy size across every inline policy document it carries is
     * capped at 10,240 bytes total, not a separate budget per document -- this class's flat list
     * (212 actions, ~6.3KB on its own) exceeds that cap alongside the role's other inline
     * policies. {@link ManagerOperatorIamSupport} attaches each half as its own customer-managed
     * policy instead of inline specifically to sidestep that combined-inline ceiling (a managed
     * policy's size budget is independent of it), so the split point here only needs to keep each
     * half comfortably under a managed policy's own (smaller, ~6,144-byte default) size limit,
     * measured against the real synthesized byte counts per AWS service prefix.
     */
    public static List<String> getNetworkPermissions(IAMProfile tier) {
        List<String> actions = new java.util.ArrayList<>();
        actions.addAll(VPC_PERMISSIONS.get(IAMProfile.MINIMAL));
        actions.addAll(EFS_PERMISSIONS.get(IAMProfile.MINIMAL));
        actions.addAll(ALB_PERMISSIONS.get(IAMProfile.MINIMAL));
        if (tier == IAMProfile.MINIMAL) {
            return List.copyOf(actions);
        }
        actions.addAll(VPC_PERMISSIONS.get(IAMProfile.STANDARD));
        actions.addAll(EFS_PERMISSIONS.get(IAMProfile.STANDARD));
        actions.addAll(ALB_PERMISSIONS.get(IAMProfile.STANDARD));
        if (tier == IAMProfile.STANDARD) {
            return List.copyOf(actions);
        }
        actions.addAll(EXTENDED_ONLY_VPC_PERMISSIONS);
        return List.copyOf(actions);
    }

    /** See {@link #getNetworkPermissions} -- the other half of the same split. */
    public static List<String> getComputeAndDataPermissions(IAMProfile tier, boolean includeCompliance) {
        List<String> actions = new java.util.ArrayList<>();
        actions.addAll(ECS_PERMISSIONS.get(IAMProfile.MINIMAL));
        actions.addAll(LOGS_PERMISSIONS.get(IAMProfile.MINIMAL));
        actions.addAll(DATABASE_PERMISSIONS.get(IAMProfile.MINIMAL));
        if (includeCompliance) {
            actions.addAll(COMPLIANCE_PERMISSIONS.get(IAMProfile.MINIMAL));
        }
        if (tier == IAMProfile.MINIMAL) {
            return List.copyOf(actions);
        }
        actions.addAll(ECS_PERMISSIONS.get(IAMProfile.STANDARD));
        actions.addAll(LOGS_PERMISSIONS.get(IAMProfile.STANDARD));
        actions.addAll(DATABASE_PERMISSIONS.get(IAMProfile.STANDARD));
        if (includeCompliance) {
            actions.addAll(COMPLIANCE_PERMISSIONS.get(IAMProfile.STANDARD));
        }
        return List.copyOf(actions);
    }
}