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 task role needs, as the calling principal, to create and manage the infrastructure a
* {@code deploy:create} target application's 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 task role</i> can do at runtime (pull its image, read SSM
* parameters, put CloudWatch metrics). This class defines what <i>Manager's role</i> needs to
* create and delete that app's infrastructure, because CloudFormation issues those calls under
* Manager's identity.</p>
*
* <p>Tiered by the same {@link IAMProfile} enum as {@link PermissionMatrix}: MINIMAL is
* read-only (inventory and troubleshooting; Describe/List actions accept no Tags parameter to
* condition on), STANDARD is the full lifecycle for the Fargate + ALB + EFS shape catalog apps
* deploy, and EXTENDED adds flow logs and custom network ACLs.
* {@link ManagerOperatorIamSupport#deployStatements} currently uses EXTENDED for Manager's task
* role because there is no per-user AWS-level distinction yet.</p>
*
* <p><b>Planned direction:</b> rather than running {@code deploy:create} requests directly under
* Manager's task role, Manager would assume an operator role with a session policy scoped to the
* {@link IAMProfile} tier a request needs, using its existing RBAC catalog
* ({@code ManagerPolicyCatalog}) and {@code sts:AssumeRole} support. Users without the
* {@code deploy:create} capability could then never obtain infrastructure-creation permissions.
* This class is intended to remain the single source of truth for those action lists.</p>
*/
public final class OperatorProvisioningPermissionMatrix {
private OperatorProvisioningPermissionMatrix() {
}
/**
* {@link com.cloudforgeci.api.network.VpcFactory}'s {@code Vpc} L2 construct: every EC2
* networking resource type it can synthesize. VpcFactory always creates public and
* private-with-egress subnet pairs across two AZs, so NAT/EIP/route-table actions are at
* STANDARD. {@link #EXTENDED_ONLY_VPC_PERMISSIONS} holds the EXTENDED-only actions (flow logs
* and custom network ACLs, which VpcFactory does not enable 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",
// EFS mount targets are ENIs: CreateMountTarget/DeleteMountTarget call these EC2 APIs
// with the caller's identity (documented AWS requirement). When missing, EFS returns a
// generic 403 that does not name the EC2 action.
"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",
// Create/delete/modify half of the EFS mount-target ENI requirement (see MINIMAL above).
"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 a replication configuration before DeleteFileSystem, even
// for file systems that were never replicated; deletion fails without this.
"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",
// ELB calls this EC2 API with the caller's identity during CreateLoadBalancer (a
// documented AWS requirement). When missing, ALB creation fails with
// "ec2:DescribeAccountAttributes ... (Service: ElasticLoadBalancingV2 ...)".
"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.core.runtime.FargateRuntimeConfiguration}'s public ACM {@code
* Certificate} L2 construct (SSL with a custom domain, DNS-validated against the app's Route 53
* hosted zone). CloudFormation's {@code AWS::CertificateManager::Certificate} handler calls
* these actions with the caller's identity. Grouped with the network category
* ({@link #getNetworkPermissions}) because the certificate backs the ALB's HTTPS listener.
* The private-CA fallback ({@code acm-pca:*}) is a separate category; see
* {@link #ACM_PCA_PERMISSIONS}.
*/
public static final Map<IAMProfile, List<String>> ACM_PERMISSIONS = Map.of(
IAMProfile.MINIMAL, List.of(
"acm:DescribeCertificate",
"acm:ListCertificates",
"acm:ListTagsForCertificate"
),
IAMProfile.STANDARD, List.of(
"acm:RequestCertificate",
"acm:DeleteCertificate",
"acm:AddTagsToCertificate",
"acm:RemoveTagsFromCertificate"
)
);
/**
* {@link com.cloudforgeci.api.network.DomainFactory}'s {@code HostedZone} L2 construct.
* MINIMAL covers the {@code ListHostedZonesByName} lookup in
* {@code CloudForgeSynthesizer#seedHostedZoneContext} (which runs under this operator role)
* and the read-only calls {@code HostedZone.fromLookup} needs; STANDARD covers the hosted-zone
* lifecycle for {@code createZone=true} and the record sets that point at the app's ALB or
* CloudFront distribution.
*/
public static final Map<IAMProfile, List<String>> ROUTE53_PERMISSIONS = Map.of(
IAMProfile.MINIMAL, List.of(
"route53:ListHostedZonesByName",
"route53:GetHostedZone",
"route53:ListResourceRecordSets",
"route53:ListTagsForResource"
),
IAMProfile.STANDARD, List.of(
"route53:CreateHostedZone",
"route53:DeleteHostedZone",
"route53:ChangeTagsForResource",
"route53:ChangeResourceRecordSets",
"route53:GetChange"
)
);
/** {@link com.cloudforgeci.api.core.runtime.FargateRuntimeConfiguration}'s private-CA
* fallback ({@code CfnCertificateAuthority}/{@code CfnCertificateAuthorityActivation}), used
* when SSL is enabled without a custom domain. Separate from {@link #ACM_PERMISSIONS} because
* {@code acm-pca:*} is a distinct action prefix. */
public static final Map<IAMProfile, List<String>> ACM_PCA_PERMISSIONS = Map.of(
IAMProfile.MINIMAL, List.of(
"acm-pca:DescribeCertificateAuthority",
"acm-pca:GetCertificateAuthorityCertificate",
"acm-pca:GetCertificate",
"acm-pca:ListTags"
),
IAMProfile.STANDARD, List.of(
"acm-pca:CreateCertificateAuthority",
"acm-pca:DeleteCertificateAuthority",
"acm-pca:UpdateCertificateAuthority",
"acm-pca:IssueCertificate",
"acm-pca:ImportCertificateAuthorityCertificate",
"acm-pca:TagCertificateAuthority",
"acm-pca:UntagCertificateAuthority"
)
);
/** {@link com.cloudforgeci.api.core.topology.CmsCdnConfiguration}/{@link
* com.cloudforgeci.api.core.topology.S3WebsiteTopologyConfiguration}'s {@code Distribution}
* L2 construct — CDN in front of an app's ALB origin or an S3-hosted static site. */
public static final Map<IAMProfile, List<String>> CLOUDFRONT_PERMISSIONS = Map.of(
IAMProfile.MINIMAL, List.of(
"cloudfront:GetDistribution",
"cloudfront:GetDistributionConfig",
"cloudfront:ListDistributions",
"cloudfront:ListTagsForResource",
// OriginAccessControl (CachePolicy/origin config needs this to describe an existing
// S3-origin OAC) — the read half of the create/manage pair below.
"cloudfront:GetOriginAccessControl",
"cloudfront:ListOriginAccessControls"
),
IAMProfile.STANDARD, List.of(
"cloudfront:CreateDistribution",
"cloudfront:UpdateDistribution",
"cloudfront:DeleteDistribution",
"cloudfront:TagResource",
"cloudfront:UntagResource",
"cloudfront:CreateOriginAccessControl",
"cloudfront:DeleteOriginAccessControl",
"cloudfront:UpdateOriginAccessControl",
"cloudfront:CreateInvalidation"
)
);
/** {@link com.cloudforgeci.api.core.topology.S3WebsiteTopologyConfiguration}'s {@code Bucket}
* L2 construct (a static-site origin) — distinct from {@code ManagerOperatorIamSupport}'s
* own S3 grants, which are scoped to Manager's own fixed-prefix template/CDK-asset buckets,
* never an arbitrary bucket a deployed app's own template declares. */
public static final Map<IAMProfile, List<String>> S3_APP_BUCKET_PERMISSIONS = Map.of(
IAMProfile.MINIMAL, List.of(
"s3:GetBucketLocation",
"s3:GetBucketPolicy",
"s3:GetEncryptionConfiguration",
"s3:GetBucketPublicAccessBlock",
"s3:ListBucket"
),
IAMProfile.STANDARD, List.of(
"s3:CreateBucket",
"s3:DeleteBucket",
"s3:PutBucketPolicy",
"s3:DeleteBucketPolicy",
"s3:PutEncryptionConfiguration",
"s3:PutBucketPublicAccessBlock",
"s3:PutBucketOwnershipControls",
"s3:PutBucketAcl",
"s3:PutObject",
"s3:DeleteObject",
"s3:PutBucketTagging"
)
);
/** {@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"
)
);
/** Creating the target app's {@code AWS::Logs::LogGroup}. Separate from
* {@link PermissionMatrix#CORE_PERMISSIONS}, which covers the deployed app's task role writing
* to that log group at runtime. */
public static final Map<IAMProfile, List<String>> LOGS_PERMISSIONS = Map.of(
IAMProfile.MINIMAL, List.of(
"logs:DescribeLogGroups",
"logs:ListTagsForResource",
// Reads log events for Manager's Logs tab (CloudWatchLogsStackOperations
// #fetchLogEvents); the other actions here only describe or manage the log group.
"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 reads the engine's default parameter values before applying
// ParameterGroup overrides; DBParameterGroup creation fails without this.
"rds:DescribeEngineDefaultParameters",
"rds:ListTagsForResource",
"kms:DescribeKey",
"kms:ListAliases",
"kms:GetKeyPolicy",
"kms:GetKeyRotationStatus",
"secretsmanager:DescribeSecret",
"secretsmanager:ListSecrets",
// RdsFactory's own StringParameter (a distroless app with no shell can't do variable
// substitution, so its datasource URL -- including dynamic references CloudFormation
// resolves -- is stored here instead, see ContainerFactory's mattermost-* case) and
// SharedResourceRegistry's own general-purpose parameter storage. Distinct from
// COMPLIANCE_PERMISSIONS' ssm:*Document actions (SSM Documents, a different resource
// type under the same service prefix) and ManagerOperatorIamSupport's own
// ssm:GetParameters grant (fixed to the CDK bootstrap-version parameter specifically).
"ssm:GetParameter",
"ssm:GetParameters",
"ssm:DescribeParameters",
"ssm:ListTagsForResource"
),
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: generateSecretString calls GetRandomPassword server-side, and
// RemovalPolicy.DESTROY requires DeleteSecret.
"secretsmanager:CreateSecret",
"secretsmanager:DeleteSecret",
"secretsmanager:GetRandomPassword",
"secretsmanager:GetSecretValue",
"secretsmanager:PutSecretValue",
"secretsmanager:UpdateSecret",
"secretsmanager:TagResource",
"secretsmanager:UntagResource",
"ssm:PutParameter",
"ssm:DeleteParameter",
"ssm:AddTagsToResource",
"ssm:RemoveTagsFromResource"
)
);
/**
* {@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",
// ComplianceFactory's own CloudTrail Trail -- same "compliance mode toggle" grouping
// as Config/GuardDuty/WAF above, not a category of its own.
"cloudtrail:GetTrailStatus",
"cloudtrail:GetEventSelectors",
"cloudtrail:ListTags"
),
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",
"cloudtrail:CreateTrail",
"cloudtrail:DeleteTrail",
"cloudtrail:UpdateTrail",
"cloudtrail:PutEventSelectors",
"cloudtrail:StartLogging",
"cloudtrail:StopLogging",
"cloudtrail:AddTags",
"cloudtrail:RemoveTags",
// 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"
)
);
/** {@link com.cloudforgeci.api.scaling.ScalingFactory}'s Fargate service {@code
* scaleOnCpuUtilization}/{@code EnableScalingProps} and {@link
* com.cloudforgeci.api.compute.Ec2Factory}'s {@code AutoScalingGroup#scaleOnCpuUtilization} --
* two distinct AWS services (Application Auto Scaling registers the ECS service as a
* scalable target; EC2 Auto Scaling owns the ASG directly), kept as one category rather than
* two small ones. */
public static final Map<IAMProfile, List<String>> SCALING_PERMISSIONS = Map.of(
IAMProfile.MINIMAL, List.of(
"application-autoscaling:DescribeScalableTargets",
"application-autoscaling:DescribeScalingPolicies",
"autoscaling:DescribeAutoScalingGroups",
"autoscaling:DescribeScalingActivities",
"autoscaling:DescribePolicies"
),
IAMProfile.STANDARD, List.of(
"application-autoscaling:RegisterScalableTarget",
"application-autoscaling:DeregisterScalableTarget",
"application-autoscaling:PutScalingPolicy",
"application-autoscaling:DeleteScalingPolicy",
"autoscaling:CreateAutoScalingGroup",
"autoscaling:DeleteAutoScalingGroup",
"autoscaling:UpdateAutoScalingGroup",
"autoscaling:PutScalingPolicy",
"autoscaling:DeletePolicy",
"autoscaling:CreateOrUpdateTags",
"autoscaling:DeleteTags"
)
);
/** {@link com.cloudforgeci.api.security.CognitoAuthenticationFactory}/{@link
* com.cloudforgeci.api.security.CognitoSamlFactory}'s {@code UserPool}/{@code
* UserPoolClient}/{@code UserPoolDomain} L2 constructs (application-oidc's own identity
* provider) -- the {@code AwsCustomResource} calls both classes also make (SAML IdP config,
* client-secret retrieval) carry their own dedicated, narrowly-scoped IAM policy per AWS CDK's
* own custom-resource convention, so those don't need anything added here. */
public static final Map<IAMProfile, List<String>> COGNITO_PERMISSIONS = Map.of(
IAMProfile.MINIMAL, List.of(
"cognito-idp:DescribeUserPool",
"cognito-idp:DescribeUserPoolClient",
"cognito-idp:DescribeUserPoolDomain",
"cognito-idp:DescribeIdentityProvider",
"cognito-idp:GetUserPoolMfaConfig",
"cognito-idp:ListTagsForResource"
),
IAMProfile.STANDARD, List.of(
"cognito-idp:CreateUserPool",
"cognito-idp:DeleteUserPool",
"cognito-idp:UpdateUserPool",
"cognito-idp:CreateUserPoolClient",
"cognito-idp:DeleteUserPoolClient",
"cognito-idp:UpdateUserPoolClient",
"cognito-idp:CreateUserPoolDomain",
"cognito-idp:DeleteUserPoolDomain",
"cognito-idp:UpdateUserPoolDomain",
"cognito-idp:CreateIdentityProvider",
"cognito-idp:DeleteIdentityProvider",
"cognito-idp:UpdateIdentityProvider",
"cognito-idp:SetUserPoolMfaConfig",
"cognito-idp:CreateGroup",
"cognito-idp:DeleteGroup",
"cognito-idp:TagResource",
"cognito-idp:UntagResource"
)
);
/** {@link com.cloudforgeci.api.observability.AlarmFactory}/{@link
* com.cloudforgeci.api.observability.SecurityMonitoringFactory}'s {@code Alarm}/{@code
* Metric}/{@code Topic} constructs -- CloudWatch alarms and their SNS notification target,
* one category since neither is useful without the other here (every alarm this platform
* creates wires an {@code SnsAction}). Distinct from {@link #LOGS_PERMISSIONS}: CloudWatch
* Logs and CloudWatch metrics/alarms are different action prefixes ({@code logs:*} vs
* {@code cloudwatch:*}) despite sharing a console. */
public static final Map<IAMProfile, List<String>> MONITORING_PERMISSIONS = Map.of(
IAMProfile.MINIMAL, List.of(
"cloudwatch:DescribeAlarms",
"cloudwatch:GetMetricData",
"cloudwatch:GetMetricStatistics",
"cloudwatch:ListTagsForResource",
"sns:GetTopicAttributes",
"sns:ListTagsForResource",
"sns:ListSubscriptionsByTopic"
),
IAMProfile.STANDARD, List.of(
"cloudwatch:PutMetricAlarm",
"cloudwatch:DeleteAlarms",
"cloudwatch:TagResource",
"cloudwatch:UntagResource",
"sns:CreateTopic",
"sns:DeleteTopic",
"sns:SetTopicAttributes",
"sns:Subscribe",
"sns:Unsubscribe",
"sns:TagResource",
"sns:UntagResource"
)
);
/** {@link com.cloudforgeci.api.storage.BackupFactory}'s {@code BackupVault}/{@code
* BackupPlan}/{@code BackupSelection} L2 constructs -- {@code BackupSelection} also creates
* its own IAM role, explicitly named (see {@code BackupFactory#createSelectionRole}) so
* {@code ManagerOperatorIamSupport#IAM_ROLE_MANAGE_RESOURCES}'s {@code -CfcBackupSelection}
* pattern can match it; that role's own create/manage permissions live there, not here. */
public static final Map<IAMProfile, List<String>> BACKUP_PERMISSIONS = Map.of(
IAMProfile.MINIMAL, List.of(
"backup:DescribeBackupVault",
"backup:GetBackupPlan",
"backup:GetBackupSelection",
"backup:ListTags"
),
IAMProfile.STANDARD, List.of(
"backup:CreateBackupVault",
"backup:DeleteBackupVault",
"backup:PutBackupVaultAccessPolicy",
"backup:CreateBackupPlan",
"backup:DeleteBackupPlan",
"backup:UpdateBackupPlan",
"backup:CreateBackupSelection",
"backup:DeleteBackupSelection",
"backup:TagResource",
"backup:UntagResource"
)
);
/**
* 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(ACM_PERMISSIONS.get(IAMProfile.MINIMAL));
actions.addAll(ACM_PCA_PERMISSIONS.get(IAMProfile.MINIMAL));
actions.addAll(ROUTE53_PERMISSIONS.get(IAMProfile.MINIMAL));
actions.addAll(CLOUDFRONT_PERMISSIONS.get(IAMProfile.MINIMAL));
actions.addAll(S3_APP_BUCKET_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));
actions.addAll(SCALING_PERMISSIONS.get(IAMProfile.MINIMAL));
actions.addAll(COGNITO_PERMISSIONS.get(IAMProfile.MINIMAL));
actions.addAll(MONITORING_PERMISSIONS.get(IAMProfile.MINIMAL));
actions.addAll(BACKUP_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(ACM_PERMISSIONS.get(IAMProfile.STANDARD));
actions.addAll(ACM_PCA_PERMISSIONS.get(IAMProfile.STANDARD));
actions.addAll(ROUTE53_PERMISSIONS.get(IAMProfile.STANDARD));
actions.addAll(CLOUDFRONT_PERMISSIONS.get(IAMProfile.STANDARD));
actions.addAll(S3_APP_BUCKET_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));
actions.addAll(SCALING_PERMISSIONS.get(IAMProfile.STANDARD));
actions.addAll(COGNITO_PERMISSIONS.get(IAMProfile.STANDARD));
actions.addAll(MONITORING_PERMISSIONS.get(IAMProfile.STANDARD));
actions.addAll(BACKUP_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);
}
/**
* The "network" half of {@link #getRequiredPermissions}: VPC, EFS, ALB, ACM, ACM PCA,
* Route 53, CloudFront, and app S3 bucket actions. {@link #getComputeAndDataPermissions}
* returns the rest. The list is split by JSON size, not category count, because the full list
* exceeds a role's combined 10,240-byte inline-policy limit alongside its other policies.
* {@link ManagerOperatorIamSupport} attaches each half as its own customer-managed policy, so
* each half must stay under the 6,144-byte managed-policy limit.
*/
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));
actions.addAll(ACM_PERMISSIONS.get(IAMProfile.MINIMAL));
actions.addAll(ACM_PCA_PERMISSIONS.get(IAMProfile.MINIMAL));
actions.addAll(ROUTE53_PERMISSIONS.get(IAMProfile.MINIMAL));
actions.addAll(CLOUDFRONT_PERMISSIONS.get(IAMProfile.MINIMAL));
actions.addAll(S3_APP_BUCKET_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(ACM_PERMISSIONS.get(IAMProfile.STANDARD));
actions.addAll(ACM_PCA_PERMISSIONS.get(IAMProfile.STANDARD));
actions.addAll(ROUTE53_PERMISSIONS.get(IAMProfile.STANDARD));
actions.addAll(CLOUDFRONT_PERMISSIONS.get(IAMProfile.STANDARD));
actions.addAll(S3_APP_BUCKET_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));
actions.addAll(SCALING_PERMISSIONS.get(IAMProfile.MINIMAL));
actions.addAll(COGNITO_PERMISSIONS.get(IAMProfile.MINIMAL));
actions.addAll(MONITORING_PERMISSIONS.get(IAMProfile.MINIMAL));
actions.addAll(BACKUP_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));
actions.addAll(SCALING_PERMISSIONS.get(IAMProfile.STANDARD));
actions.addAll(COGNITO_PERMISSIONS.get(IAMProfile.STANDARD));
actions.addAll(MONITORING_PERMISSIONS.get(IAMProfile.STANDARD));
actions.addAll(BACKUP_PERMISSIONS.get(IAMProfile.STANDARD));
if (includeCompliance) {
actions.addAll(COMPLIANCE_PERMISSIONS.get(IAMProfile.STANDARD));
}
return List.copyOf(actions);
}
}