aws / aws/modern-data-architecture-accelerator
`dataops-project`: `projectExecutionRoles` cannot reference a cross-account role, and a bare SSM ARN fails with a misleading error
- Dominant language
- TypeScript
- Stars
- 80
- Forks
- 29
- PR merge metrics
- No merged PRs in 30d
Description
### Environment
- MDAA version: `1.8.0`
- Module: `@aws-mdaa/dataops-project`
- Node: v22
- Single region, two accounts in the same partition
### Summary
A data lake in a **producer** account exposes a Glue database whose tables are
written by an ETL role that lives in a separate **enterprise ingestion** account. To let that
external role create Iceberg table pointers, it needs
`DATA_LOCATION_ACCESS` on the registered S3 prefix.
In `dataops-project` that grant is only emitted for entries in
`projectExecutionRoles` — see `createDatabaseLakeFormationConstruct` in
`@aws-mdaa/dataops-project-l3-construct/lib/dataops-project-l3-construct.js`,
which iterates `this.projectExecutionRoles` and creates a
`CfnPrincipalPermissions` with a `dataLocation` resource. There is no other
configuration surface that produces a data-location grant: the `grants` /
`principalArns` block under `databases..lakeFormation` only produces
database and table permissions.
But `projectExecutionRoles` cannot hold a cross-account role, so the capability
is unreachable from configuration.
### Reproduction
```yaml
# dataops-project module config
projectExecutionRoles:
- id: ssm-org:/mydomain/generated-role/glue-etl/id # local, works
- arn: arn:aws:iam::222222222222:role/enterprise-glue-etl-role # cross-account
databases:
shared_db:
locationBucketName: ssm-org:/mydomain/datalake/bucket/transformed/name
locationPrefix: shared_db
lakeFormation:
createReadWriteGrantsForProjectExecutionRoles: true
```
Deploy into producer account `111111111111`.
**Expected:** a `DATA_LOCATION_ACCESS` grant is created for the cross-account role on
`arn:aws:s3:::/shared_db`. Granting this permission directly to an IAM
principal in an external account is
[supported by Lake Formation](https://docs.aws.amazon.com/lake-formation/latest/dg/granting-location-permissions-external.html)
when the cross-account version setting is 3 or above.
**Actual:** synth always succeeds. The deploy then fails in one of two different
places depending on which anchor property is used — there is no third option, so
the feature is unreachable.
With `id:` — fails in the role-resolution custom resource:
```
CREATE_FAILED | AWS::CloudFormation::CustomResource | RoleResProjectExRoles1
Received response status [FAILED] from custom resource.
Message returned: Error: Failed to resolve role: {'refId': 'ProjectExRoles-1', ...}
```
With `arn:` — gets past role resolution, then fails on the generated
`catalog-key-access` managed policy:
```
UPDATE_FAILED | AWS::IAM::ManagedPolicy | catalogkeyaccesspolicy
Resource handler returned message: "The role with name enterprise-glue-etl-role
cannot be found. (Service: Iam, Status Code: 404 ...)" (HandlerErrorCode: NotFound)
```
The role does exist — in account `222222222222`. IAM is being asked to attach a
managed policy to a role it does not own.
### Root cause
`MdaaResolvableRole` (`@aws-mdaa/iam-role-helper/lib/resolvablerole.js`) creates
the resolution CR lazily, only for properties the config did not supply. That
lazy behaviour is why `principalArns` grants work cross-account — they set
`role: { arn }`, so `arn()` short-circuits and no CR is created.
`dataops-project-l3-construct.js` defeats that for project execution roles by
requesting all three properties of every entry:
- `createDatabaseLakeFormationConstruct` builds principals as
`{ arn: x.arn(), id: x.id(), name: x.name() }`
- `createProjectBucket` maps `this.projectExecutionRoles` through `x.id()` to
build `aws:userId` bucket-policy conditions
**Failure 1 (`id:`)** — `arn()` and `name()` are unpopulated, so a CR is created.
The resolver
(`@aws-mdaa/iam-role-helper/src/python/resolve_role/resolve_role.py`) constructs
a plain `boto3.client('iam')` with no `sts:AssumeRole`, and its execution role is
granted only `iam:ListRoles` and `iam:GetRole` (see `createResolveRoleProvider`
in `rolehelper.js`). It therefore only ever sees IAM in the deploying account:
- by `id` — the role ID cannot appear in a local `list_roles`
- by `arn` — `fetch_role_from_iam` splits the role name off the ARN and calls
`get_role(RoleName=...)` locally, yielding `NoSuchEntity`
- by `name` — same local `get_role`
**Failure 2 (`arn:`)** — resolution is skipped for `arn()`, but the constructor's
Glue-catalog-KMS block builds the `catalog-key-access` managed policy with
`roles: projectExecutionRoles`, each imported via
`MdaaRole.fromRoleArn(..., x.arn())`. The imported role's *name* lands in the
`Roles` property of `AWS::IAM::ManagedPolicy`, and IAM cannot attach a managed
policy to a role owned by another account — producing the 404 above on a role
that exists in the enterprise account.
These two are independent, so no combination of anchor properties works.
### Secondary issue: bare SSM ARNs are silently passed through
Attempting the same thing by pointing at a RAM-shared SSM parameter in the other
account produces a much more confusing failure:
```yaml
projectExecutionRoles:
- id: arn:aws:ssm:us-east-1:222222222222:parameter/myorg/mydomain/generated-role/glue-etl/id
```
`MdaaConfigRefValueTransformer.resolveReference`
(`@aws-mdaa/config/lib/ref-value-transformer.js`) dispatches on the prefixes
`ssm-org:`, `ssm-domain:`, `ssm-env:` and `resolve:ssm:`. Cross-account SSM ARNs
*are* handled — but only inside `getSsmValue`, which is reachable only via
`resolve:ssm:`. A bare `arn:aws:ssm:...` matches no prefix, so it is passed
through as a literal string and handed to the resolver CR as if it were an IAM
role ID. The resulting error reports a failure to resolve a role and shows an SSM
ARN, with nothing indicating that the reference was never resolved in the first
place.
This is easy to hit because the surrounding config uses raw ARNs elsewhere
without any prefix — for example `sagemaker.domainConfigSSMParam` accepts a bare
cross-account SSM ARN — so the inconsistency is not obvious from the schema.
### Suggested fixes
Any one of these would unblock the use case; the first is the smallest:
1. **Expose a data-location grant in the `grants` config surface.** Let a
`grants.` entry request `DATA_LOCATION_ACCESS` alongside its database
and table permissions. This reuses the existing `principalArns` path, which
already handles cross-account principals correctly (the LF access-control L3
construct parses the account out of the principal ARN and supports
`resourceLinks` with `fromAccount`/`targetAccount`), and needs no change to
role resolution.
2. **Make `dataops-project` account-aware for project execution roles.** Two
changes together would make an `arn:`-supplied cross-account entry work end to
end: only call `id()` / `name()` where genuinely needed (the `aws:userId`
bucket-policy conditions are inherently account-local and could skip
cross-account entries), and filter the `catalog-key-access` managed policy's
`roles` to entries whose ARN account matches the deploying account. The
external account grants catalog-key access on its own side.
3. **Fail fast with a clear message.** If a role ref resolves to an ARN in a
different account than the deployment target, raise at synth time explaining
that cross-account roles are unsupported in `projectExecutionRoles`, rather
than failing mid-deploy inside a custom resource.
4. **For the secondary issue**, either treat a value starting with
`arn::ssm:` as a direct SSM reference, or reject it at synth time
with a message pointing at the `{{resolve:ssm:}}` form — anything other
than silently forwarding it as a role identifier.
### Workaround
Remove the cross-account entry from `projectExecutionRoles`, keep the LF
database/table permissions in the `grants.principalArns` block, and apply the
data-location grant out of band:
```bash
aws lakeformation grant-permissions \
--principal DataLakePrincipalIdentifier=arn:aws:iam::222222222222:role/enterprise-glue-etl-role \
--permissions "DATA_LOCATION_ACCESS" \
--resource '{"DataLocation":{"CatalogId":"111111111111","ResourceArn":"arn:aws:s3:::/shared_db"}}'
```
Contributor guide
Assessment
This issue has not been assessed yet.