Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions docs/summary-schedule/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,152 @@ do. The one rule this construct adds is that the question builds its `WHERE` cla
because that is what writes the window the job fills in. A query that reaches Athena without one is
refused before it is sent, since Athena would take it and read every partition the table projects.

## Permissions for a scoped deploy role

Skippable on an account whose CloudFormation execution role holds `AdministratorAccess`.

This construct creates more kinds of resource than the rest of the pipeline. A deploy of the
defaults writes ten schedules, one function, one log group, two roles with an inline policy each,
and a bucket with a bucket policy. `scheduler:` is the prefix to check first. A role that has
deployed anything else in the account usually holds the rest already.

```typescript
import { PolicyStatement } from "aws-cdk-lib/aws-iam";

new PolicyStatement({
sid: "TheRainlyticsSummarySchedules",
actions: [
"scheduler:CreateSchedule",
"scheduler:GetSchedule",
"scheduler:UpdateSchedule",
"scheduler:DeleteSchedule",
],
resources: [
`arn:aws:scheduler:${region}:${account}:schedule/default/${schedulePrefix}*`,
],
});
```

`default` is the schedule group every schedule here goes in, and the names after it begin with
`schedulePrefix` (`rainlytics-` unless it was passed). The wildcard therefore covers one
deployment's schedules and leaves whatever else the account has scheduled alone.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

It matches on the prefix and nothing else. A second deployment that called itself `rainlytics-docs-`
would sit inside a statement quoting `rainlytics-*`, and the first deployment's role could update
and delete its schedules. Give the second one a prefix that is not the opening of the first one's.

The function, its log group and the two roles:

```typescript
new PolicyStatement({
sid: "TheRainlyticsSummaryFunction",
actions: [
"lambda:CreateFunction",
"lambda:GetFunction",
"lambda:UpdateFunctionCode",
"lambda:UpdateFunctionConfiguration",
"lambda:DeleteFunction",
],
resources: [`arn:aws:lambda:${region}:${account}:function:${stackName}-*`],
});

new PolicyStatement({
sid: "TheRainlyticsSummaryLogs",
actions: [
"logs:CreateLogGroup",
"logs:PutRetentionPolicy",
"logs:DeleteLogGroup",
],
resources: [`arn:aws:logs:${region}:${account}:log-group:${stackName}-*`],
});

new PolicyStatement({
sid: "TheRainlyticsSummaryLogGroups",
actions: ["logs:DescribeLogGroups"],
resources: ["*"],
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

new PolicyStatement({
sid: "TheRainlyticsSummaryRoles",
actions: [
"iam:CreateRole",
"iam:GetRole",
"iam:DeleteRole",
"iam:PutRolePolicy",
"iam:GetRolePolicy",
"iam:DeleteRolePolicy",
"iam:AttachRolePolicy",
"iam:DetachRolePolicy",
"iam:PassRole",
],
resources: [`arn:aws:iam::${account}:role/${stackName}-*`],
});
```

`logs:DescribeLogGroups` is the one that has to be `*`. CloudFormation reads a log group back with
it, and IAM gives the action no resource type at all, so a statement naming a log group authorises
it for no request. The other three actions are scoped to the group.

`iam:PassRole` is the one that looks like surplus. `lambda:CreateFunction` hands Lambda the role the
function runs as, and `scheduler:CreateSchedule` hands Scheduler the role it invokes through. A
policy that creates both roles and stops there fails on the first of those two calls.
`iam:AttachRolePolicy` is for `AWSLambdaBasicExecutionRole`, the managed policy CDK attaches to
every function's role.

The function, its log group and both roles are left unnamed, and CloudFormation names each of them
after the stack and the logical id. That is where `${stackName}-*` comes from above, and the
truncation trap on the [log bucket](../log-bucket/) page applies to all three. Check the prefix
against the names a deploy created rather than against the stack name alone.

The created bucket takes the S3 permissions on that same page against its own ARN, its bucket policy
included (`enforceSSL` writes one). A deployment passing `summariesBucket` creates no bucket and
needs none of them.

The function's code goes up under a different role. `cdk deploy` uploads the asset with the
bootstrap file publishing role before CloudFormation runs. A deploy that fails on the upload is a
bootstrap question.

Read the whole list as inferred from what the construct creates. The resource counts above come
from synthesising the defaults, and no deploy has run under a role narrower than
`AdministratorAccess`. So the actions have never been tested against the failure a missing one would
cause.

## Permissions for a scheduled run

The deploy role has no part in these. The construct grants them itself, onto the role the function
runs as, and a reader under a narrowed deploy role has no policy to write for them. They are here
because a service control policy denies an unlisted prefix whichever role sent the call, and the
deploy list above is half of what such an account has to allow.

One run sends:

- `athena:StartQueryExecution`, `StopQueryExecution`, `GetQueryExecution` and `GetQueryResults` on
the workgroup, with `athena:GetWorkGroup` alongside them (Athena reads the workgroup's own
configuration on the way to running a query in it).
- `glue:GetDatabase`, `glue:GetTable` and `glue:GetPartitions` on the catalog, the database and the
table.
- `s3:GetObject`, `s3:GetBucketLocation` and `s3:ListBucket` on the log bucket and its objects.
Athena lists the prefixes a partition predicate selected before it reads anything in them.
- On the workgroup's results bucket and its objects, what CDK's `grantReadWrite` writes.
`s3:GetObject*`, `s3:GetBucket*`, `s3:List*`, `s3:DeleteObject*`, `s3:PutObject` with its
`LegalHold`, `Retention`, `Tagging` and `VersionTagging` variants, and `s3:Abort*`. Athena writes
each query's output there as the caller and reads it back to answer `GetQueryResults`.
- The same `PutObject` family and `s3:Abort*` again on the summaries bucket's objects, from
`grantPut`.
- `ssm:GetParameter` on the visitor salt parameter.

Those are the statements on the deployed role read back off a synthesised template, rather than a
list inferred from the code.

An account that allows the deploy prefixes and stops there deploys cleanly and fails on the first
schedule that fires. Nobody is watching that run. The message lands in the function's log group and
the summaries never start appearing. That is the harder of the two failures to attribute.

`ssm:GetParameter` reads a parameter no template creates. The salt is a `SecureString`, and
CloudFormation writes `String` and `StringList` parameters only. Somebody puts it there by hand
before the first run that counts visitors, and [creating the
secret](../visitors/#creating-the-secret) has the command.

<!-- card
```typescript
new RollupSummaries(this, "RainlyticsSummaries", { table, workgroup });
Expand Down