Skip to content

Commit 8f4a3bb

Browse files
committed
Add user-bot Lambda code, tests and deploy workflows
1 parent 181557e commit 8f4a3bb

21 files changed

Lines changed: 3418 additions & 0 deletions
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
name: Deploy user-bot Lambda
2+
3+
# Ships the user-bot Lambda's code. Terraform (terraform/user-bot.tf) owns everything
4+
# else about the function -- runtime, role, trigger -- and ignores its code, so this
5+
# workflow and terraform-apply.yaml never overwrite each other.
6+
on:
7+
push:
8+
branches:
9+
- main
10+
paths:
11+
- 'lambda/user-bot/**'
12+
- '.github/workflows/user-bot-deploy.yml'
13+
# Recovery path: redeploy without a new commit, e.g. after a failed run. The deploy
14+
# role only trusts refs/heads/main, so dispatching from any other branch fails at
15+
# the credentials step rather than deploying unreviewed code.
16+
workflow_dispatch:
17+
18+
permissions:
19+
id-token: write
20+
contents: read
21+
22+
# Two merges close together must not deploy over each other out of order.
23+
concurrency:
24+
group: user-bot-deploy
25+
cancel-in-progress: false
26+
27+
jobs:
28+
deploy:
29+
name: Build and deploy
30+
runs-on: ubuntu-latest
31+
defaults:
32+
run:
33+
working-directory: lambda/user-bot
34+
35+
steps:
36+
- name: Checkout code
37+
uses: actions/checkout@v5
38+
39+
- name: Set up Node
40+
uses: actions/setup-node@v7
41+
with:
42+
node-version-file: lambda/user-bot/.nvmrc
43+
cache: npm
44+
cache-dependency-path: lambda/user-bot/package-lock.json
45+
46+
- name: Install dependencies
47+
run: npm ci
48+
49+
# Run again here so that exactly what ships has passed.
50+
- name: Typecheck
51+
run: npm run typecheck
52+
53+
- name: Unit tests
54+
run: npm test
55+
56+
- name: Build and package
57+
run: |
58+
npm run build
59+
cd dist && zip -q ../user-bot.zip index.js
60+
61+
- name: Configure AWS Credentials
62+
uses: aws-actions/configure-aws-credentials@v6
63+
with:
64+
role-to-assume: arn:aws:iam::035866691871:role/devops-security-user-bot-deploy
65+
role-session-name: userbotdeploy
66+
# The function lives in us-east-1 because IAM events are only delivered there.
67+
aws-region: us-east-1
68+
69+
- name: Update function code
70+
run: |
71+
aws lambda update-function-code \
72+
--function-name user-bot \
73+
--zip-file fileb://user-bot.zip \
74+
--query '{CodeSha256: CodeSha256, LastUpdateStatus: LastUpdateStatus}'
75+
76+
# function-updated-v2 polls GetFunction, the only read the deploy role is granted.
77+
- name: Wait for the update to finish
78+
run: aws lambda wait function-updated-v2 --function-name user-bot
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: Test user-bot Lambda
2+
3+
# Runs only when the Lambda's code or this workflow changes. Terraform changes go
4+
# through terraform-plan.yaml instead; nothing under lambda/ is a .tf file.
5+
on:
6+
pull_request:
7+
branches:
8+
- main
9+
paths:
10+
- 'lambda/user-bot/**'
11+
- '.github/workflows/user-bot-test.yml'
12+
13+
# No AWS credentials: the tests mock every AWS and Slack call.
14+
permissions:
15+
contents: read
16+
17+
jobs:
18+
test:
19+
name: Typecheck, test and build
20+
runs-on: ubuntu-latest
21+
defaults:
22+
run:
23+
working-directory: lambda/user-bot
24+
25+
steps:
26+
- name: Checkout code
27+
uses: actions/checkout@v5
28+
29+
# Same Node major as the Lambda runtime in terraform/user-bot.tf.
30+
- name: Set up Node
31+
uses: actions/setup-node@v7
32+
with:
33+
node-version-file: lambda/user-bot/.nvmrc
34+
cache: npm
35+
cache-dependency-path: lambda/user-bot/package-lock.json
36+
37+
- name: Install dependencies
38+
run: npm ci
39+
40+
- name: Typecheck
41+
run: npm run typecheck
42+
43+
- name: Unit tests
44+
run: npm test
45+
46+
# Proves the bundle that user-bot-deploy.yml ships still builds.
47+
- name: Build
48+
run: npm run build

‎lambda/user-bot/.gitignore‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules/
2+
dist/
3+
*.zip
4+
coverage/

‎lambda/user-bot/.nvmrc‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
24

‎lambda/user-bot/README.md‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# user-bot
2+
3+
A Lambda that gives each new IAM user a temporary AWS console password and sends it to them as a Slack DM, with sign-in instructions. Tracked in [#209](https://github.com/hackforla/devops-security/issues/209).
4+
5+
> **Not sending DMs yet.** The deployed function uses `StubMessageSender`, which sends nothing and logs who would have been messaged. `SlackMessageSender` is written and tested, but switching to it needs a Slack app, its bot token, a secret the function can read, and permission to read it. None of these exist yet. Until then, the bot resets the password and the new user does not receive it, which is no worse than before: the password Terraform generates was never sent to anyone either.
6+
7+
## When it runs
8+
9+
1. A new user is added to `terraform/aws-users.tf` and merged. `Terraform Apply` creates the user and then its console login profile.
10+
2. The `CreateLoginProfile` call is recorded by CloudTrail, which delivers it to EventBridge in **us-east-1**, the only region IAM events reach.
11+
3. The rule `user-bot-create-login-profile` invokes the `user-bot` function, also in us-east-1. Failed `CreateLoginProfile` calls are filtered out.
12+
13+
The trigger is `CreateLoginProfile`, not `CreateUser`. Terraform calls `CreateUser` a moment before it creates the login profile, so a `CreateUser` trigger could try to update a profile that does not exist yet.
14+
15+
## What it does
16+
17+
It reads the user's tags and **does nothing at all** (no password change, no message) unless **both** of these hold:
18+
19+
| Tag | Required value |
20+
|---|---|
21+
| `managed-by` | exactly `terraform-devops-security`, i.e. the user was created by this repo's Terraform |
22+
| `slack_id` | a Slack member ID such as `U0123456789`, set with the `slack_id` input of the `aws-users` module |
23+
24+
When both hold, it:
25+
26+
1. generates a 20-character password containing all four character classes;
27+
2. sets it with `UpdateLoginProfile` and `PasswordResetRequired: true`, so the user must replace it at first sign-in;
28+
3. sends the user a message with the sign-in page, their IAM user name and the temporary password.
29+
30+
Every skip is logged with its reason and the user name. **The password is never logged**, on any path; the tests assert this.
31+
32+
The `managed-by` check is also enforced by IAM. The execution role may call `UpdateLoginProfile` only on users carrying that tag, so a bug or a hand-crafted invocation still cannot reset anyone else's password, such as an admin or a user tagged `exempt`. If you change the check in `src/handler.ts`, change the policy in `terraform/user-bot.tf` with it.
33+
34+
If sending fails after the password was changed, the function throws. Lambda's asynchronous retry then runs it again from the start, which sets a fresh password and sends again.
35+
36+
## Layout
37+
38+
```
39+
src/
40+
index.ts Lambda entry point; wires the handler to the stub sender
41+
handler.ts the logic above
42+
password.ts password generation
43+
message.ts the message text and sign-in URL
44+
logger.ts JSON-line logger
45+
senders/
46+
types.ts MessageSender interface
47+
slack.ts SlackMessageSender (chat.postMessage)
48+
stub.ts StubMessageSender (logs only)
49+
test/ Vitest unit tests; AWS is mocked with aws-sdk-client-mock, Slack by mocking fetch
50+
```
51+
52+
## Running the tests
53+
54+
Use the Node version in `.nvmrc` (24), the same as the Lambda runtime. A dependency's native binding requires Node `^20.19.0` or `>=22.12.0`, and an older Node skips installing it.
55+
56+
```bash
57+
cd lambda/user-bot
58+
npm ci
59+
npm run typecheck
60+
npm test
61+
npm run build # writes dist/index.js, the file that is deployed
62+
```
63+
64+
None of these needs AWS credentials or a Slack token.
65+
66+
## How it is deployed
67+
68+
Two halves, each with its own workflow:
69+
70+
| | Managed by | Runs when |
71+
|---|---|---|
72+
| The function's configuration, execution role, log group, EventBridge rule, and the deploy role | `terraform/user-bot.tf` and `terraform/aws-gha-oidc-providers.tf`, via the existing `terraform-plan.yaml` / `terraform-apply.yaml` | a `.tf` file changes |
73+
| The function's **code** | `.github/workflows/user-bot-deploy.yml` | a change under `lambda/user-bot/` merges to `main` |
74+
75+
Pull requests touching `lambda/user-bot/` run `.github/workflows/user-bot-test.yml`, which typechecks, tests and builds with no AWS credentials.
76+
77+
Terraform created the function from a do-nothing placeholder and ignores changes to its code, so a deploy never shows up as drift and Terraform never reverts one.
78+
79+
The deploy workflow assumes `devops-security-user-bot-deploy`. That role only trusts runs on `main` and can only update this one function's code. To redeploy without a new commit, run **Deploy user-bot Lambda** from the Actions tab with `main` selected. Dispatching from any other branch fails at the credentials step by design.
80+
81+
## Logs
82+
83+
CloudWatch log group `/aws/lambda/user-bot` in **us-east-1**, kept for 90 days. Each line is JSON with `level`, `message` and fields such as `userName`, `slackId` and `reason`.

0 commit comments

Comments
 (0)