|
| 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