Merge branch 'main' into cursor/openapi-redocly-vpc-7708

This commit is contained in:
Adam Moussa 2026-09-21 19:51:47 -04:00 • committed by GitHub
commit 11cbfb421c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 45 additions and 315 deletions

View file

@ -67,9 +67,11 @@ Example: `/oncall admin holiday add 2026-07-04 2 x2 Independence Day` schedules
- **3CX Integration**: Queue routing updated directly via 3CX Queue XAPI
- **Secrets**: AWS Secrets Manager (`afterhours-shift-manager/*`)
### Lambda Functions
### Leftover zip handlers (packaging only)
| Function | Trigger | Purpose |
Fargate is the live path. These `src/*/app.py` zip sources remain for `scripts/package_lambdas.py` only. Leftover Lambda IAM roles were removed after Paychex dropped AfterhoursWeeklyPostSend (PLAT-218).
| Handler | Former trigger | Purpose |
|---|---|---|
| `afterhours-shift-manager` | API Gateway (POST /slack/events) | Slack bot — handles `/oncall` commands and interactive buttons |
| `afterhours-weekly-post` | EventBridge (Monday 7am ET) | Posts weekly schedule to Slack, sends pay report email |
@ -83,17 +85,17 @@ Example: `/oncall admin holiday add 2026-07-04 2 x2 Independence Day` schedules
```
src/
server/ Flask + gunicorn + SQS worker (Fargate)
slack-bot/ Slack Bolt app; Lambda handler until cutover; ships CHANGELOG.md for App Home
weekly-post/ Monday schedule + pay post
roster-sync/ Daily 3CX roster sync
roster-api/ HTTP PUT/DELETE /roster for identity hire/offboard
portal-api/ Cognito employee/admin shift API for the internal portal
ring-scheduler/ 3CX queue routing updates
holiday-router/ 3CX IVR/queue repoint for holiday day shifts (activate/deactivate)
shared/ Bundled into each function zip and the Fargate image
terraform/ HCP Terraform (Lambda skeletons, ECS/ALB, API, DDB, IAM, schedules)
slack-bot/ Slack Bolt app; leftover zip source for packaging; ships CHANGELOG.md for App Home
weekly-post/ Monday schedule + pay post (leftover zip source for packaging)
roster-sync/ Daily 3CX roster sync (leftover zip source for packaging)
roster-api/ HTTP PUT/DELETE /roster for identity hire/offboard (leftover zip source for packaging)
portal-api/ Cognito employee/admin shift API for the internal portal (leftover zip source for packaging)
ring-scheduler/ 3CX queue routing updates (leftover zip source for packaging)
holiday-router/ 3CX IVR/queue repoint for holiday day shifts (leftover zip source for packaging)
shared/ Bundled into leftover function zips and the Fargate image
terraform/ HCP Terraform (ECS/ALB, API, DDB, IAM, leftover zip packaging locals, schedules)
scripts/ in-package changelog copy sync + cutover
tests/ pytest suite (mirrors src/, one dir per Lambda + shared + server)
tests/ pytest suite (mirrors src/, one dir per leftover zip handler + shared + server)
```
### DynamoDB Schema

View file

@ -115,9 +115,8 @@ and `ecs_schedules_enabled=false` until the Fargate cutover below.
HCP outputs to copy: `slack_request_url`, `api_origin`, `fargate_origin`,
`holiday_scheduler_role_arn`, `github_deploy_role_arn`, `jobs_queue_arn`,
`ecs_task_role_arn`. Add `ecs_task_role_arn` to paychex-checkcomponents
(alongside `afterhours-shift-manager-weekly-post`) before Fargate weekly_post
runs. Dual-run keeps the Lambda principal until zip CD is retired.
`ecs_task_role_arn`. Paychex checkcomponents allows `afterhours-shift-manager-api`;
leftover weekly-post principal is gone.
## 4. Set the Slack Request URL

View file

@ -1,291 +1,3 @@
# Leftover Lambda IAM roles from dual-run. The seven functions are gone;
# weekly-post remains so paychex-checkcomponents can keep that principal
# until a follow-up queue-policy apply drops it.
data "aws_iam_policy_document" "lambda_assume" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}
}
}
locals {
table_arn = aws_dynamodb_table.shifts.arn
lambda_identity = {
slack_bot = [
{
sid = "DdbCrud"
actions = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:DeleteItem", "dynamodb:Query", "dynamodb:Scan", "dynamodb:BatchGetItem", "dynamodb:BatchWriteItem", "dynamodb:DescribeTable", "dynamodb:ConditionCheckItem"]
resources = [local.table_arn, "${local.table_arn}/*"]
condition = null
},
{
sid = "Secrets"
actions = ["secretsmanager:GetSecretValue"]
resources = ["arn:aws:secretsmanager:${var.aws_region}:${local.account_id}:secret:afterhours-shift-manager/*"]
condition = null
},
{
sid = "HolidaySchedules"
actions = ["scheduler:CreateSchedule", "scheduler:DeleteSchedule", "scheduler:GetSchedule"]
resources = ["arn:aws:scheduler:${var.aws_region}:${local.account_id}:schedule/default/holiday-*"]
condition = null
},
{
sid = "PassHolidayScheduler"
actions = ["iam:PassRole"]
resources = [local.holiday_scheduler_role_arn]
condition = {
test = "StringEquals"
variable = "iam:PassedToService"
values = ["scheduler.amazonaws.com"]
}
},
{
sid = "InvokeHolidayRouter"
actions = ["lambda:InvokeFunction"]
resources = [local.holiday_router_arn]
condition = null
},
]
weekly_post = concat(
[
{
sid = "DdbCrud"
actions = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:DeleteItem", "dynamodb:Query", "dynamodb:Scan", "dynamodb:BatchGetItem", "dynamodb:BatchWriteItem", "dynamodb:DescribeTable", "dynamodb:ConditionCheckItem"]
resources = [local.table_arn, "${local.table_arn}/*"]
condition = null
},
{
sid = "Secrets"
actions = ["secretsmanager:GetSecretValue"]
resources = ["arn:aws:secretsmanager:${var.aws_region}:${local.account_id}:secret:afterhours-shift-manager/slack-bot-token-*"]
condition = null
},
],
var.checkcomponents_queue_arn == "" ? [] : [
{
sid = "CheckcomponentsSend"
actions = ["sqs:SendMessage"]
resources = [var.checkcomponents_queue_arn]
condition = null
},
],
),
roster_sync = [
{
sid = "DdbCrud"
actions = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:DeleteItem", "dynamodb:Query", "dynamodb:Scan", "dynamodb:BatchGetItem", "dynamodb:BatchWriteItem", "dynamodb:DescribeTable", "dynamodb:ConditionCheckItem"]
resources = [local.table_arn, "${local.table_arn}/*"]
condition = null
},
{
sid = "Secrets"
actions = ["secretsmanager:GetSecretValue"]
resources = ["arn:aws:secretsmanager:${var.aws_region}:${local.account_id}:secret:afterhours-shift-manager/3cx-*"]
condition = null
},
]
roster_api = [
{
sid = "DdbWrite"
actions = ["dynamodb:UpdateItem", "dynamodb:DeleteItem"]
resources = [local.table_arn]
condition = null
},
{
sid = "Secrets"
actions = ["secretsmanager:GetSecretValue"]
resources = ["arn:aws:secretsmanager:${var.aws_region}:${local.account_id}:secret:afterhours-shift-manager/roster-api-token-*"]
condition = null
},
]
ring_scheduler = [
{
sid = "DdbRead"
actions = ["dynamodb:GetItem", "dynamodb:Query", "dynamodb:Scan", "dynamodb:BatchGetItem", "dynamodb:DescribeTable"]
resources = [local.table_arn, "${local.table_arn}/*"]
condition = null
},
{
sid = "Secrets"
actions = ["secretsmanager:GetSecretValue"]
resources = ["arn:aws:secretsmanager:${var.aws_region}:${local.account_id}:secret:afterhours-shift-manager/3cx-*"]
condition = null
},
]
holiday_router = [
{
sid = "DdbCrud"
actions = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:DeleteItem", "dynamodb:Query", "dynamodb:Scan", "dynamodb:BatchGetItem", "dynamodb:BatchWriteItem", "dynamodb:DescribeTable", "dynamodb:ConditionCheckItem"]
resources = [local.table_arn, "${local.table_arn}/*"]
condition = null
},
{
sid = "Secrets"
actions = ["secretsmanager:GetSecretValue"]
resources = ["arn:aws:secretsmanager:${var.aws_region}:${local.account_id}:secret:afterhours-shift-manager/3cx-*"]
condition = null
},
]
portal_api = [
{
sid = "DdbCrud"
actions = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:DeleteItem", "dynamodb:Query", "dynamodb:Scan", "dynamodb:BatchGetItem", "dynamodb:BatchWriteItem", "dynamodb:DescribeTable", "dynamodb:ConditionCheckItem"]
resources = [local.table_arn, "${local.table_arn}/*"]
condition = null
},
{
sid = "Secrets"
actions = ["secretsmanager:GetSecretValue"]
resources = ["arn:aws:secretsmanager:${var.aws_region}:${local.account_id}:secret:afterhours-shift-manager/*"]
condition = null
},
{
sid = "HolidaySchedules"
actions = ["scheduler:CreateSchedule", "scheduler:DeleteSchedule", "scheduler:GetSchedule"]
resources = ["arn:aws:scheduler:${var.aws_region}:${local.account_id}:schedule/default/holiday-*"]
condition = null
},
{
sid = "PassHolidayScheduler"
actions = ["iam:PassRole"]
resources = [local.holiday_scheduler_role_arn]
condition = {
test = "StringEquals"
variable = "iam:PassedToService"
values = ["scheduler.amazonaws.com"]
}
},
{
sid = "InvokeHolidayRouter"
actions = ["lambda:InvokeFunction"]
resources = [local.holiday_router_arn]
condition = null
},
]
}
lambda_env = {
slack_bot = {
SHIFT_TABLE = aws_dynamodb_table.shifts.name
SLACK_BOT_TOKEN_SECRET = "afterhours-shift-manager/slack-bot-token"
SLACK_SIGNING_SECRET = "afterhours-shift-manager/slack-signing-secret"
SHIFT_CHANNEL = var.shift_channel
TCX_SECRET_PREFIX = "afterhours-shift-manager/3cx-"
QUEUE_NUMBER = var.queue_number
TZ = var.timezone
HOLIDAY_ROUTER_ARN = local.holiday_router_arn
HOLIDAY_SCHEDULER_ROLE_ARN = local.holiday_scheduler_role_arn
SENTRY_DSN = var.sentry_dsn
}
weekly_post = {
SHIFT_TABLE = aws_dynamodb_table.shifts.name
SLACK_BOT_TOKEN_SECRET = "afterhours-shift-manager/slack-bot-token"
SHIFT_CHANNEL = var.shift_channel
PAY_REPORT_USER = var.pay_report_user
TZ = var.timezone
CHECKCOMPONENTS_QUEUE_URL = var.checkcomponents_queue_url
SENTRY_DSN = var.sentry_dsn
}
roster_sync = {
SHIFT_TABLE = aws_dynamodb_table.shifts.name
TCX_SECRET_PREFIX = "afterhours-shift-manager/3cx-"
SYNC_GROUP = "DEFAULT"
TZ = var.timezone
SENTRY_DSN = var.sentry_dsn
}
roster_api = {
SHIFT_TABLE = aws_dynamodb_table.shifts.name
ROSTER_API_TOKEN_SECRET = "afterhours-shift-manager/roster-api-token"
TZ = var.timezone
SENTRY_DSN = var.sentry_dsn
}
ring_scheduler = {
SHIFT_TABLE = aws_dynamodb_table.shifts.name
TCX_SECRET_PREFIX = "afterhours-shift-manager/3cx-"
QUEUE_NUMBER = var.queue_number
TZ = var.timezone
SENTRY_DSN = var.sentry_dsn
}
holiday_router = {
SHIFT_TABLE = aws_dynamodb_table.shifts.name
TCX_SECRET_PREFIX = "afterhours-shift-manager/3cx-"
TZ = var.timezone
SENTRY_DSN = var.sentry_dsn
}
portal_api = {
SHIFT_TABLE = aws_dynamodb_table.shifts.name
SLACK_BOT_TOKEN_SECRET = "afterhours-shift-manager/slack-bot-token"
SHIFT_CHANNEL = var.shift_channel
TCX_SECRET_PREFIX = "afterhours-shift-manager/3cx-"
QUEUE_NUMBER = var.queue_number
TZ = var.timezone
HOLIDAY_ROUTER_ARN = local.holiday_router_arn
HOLIDAY_SCHEDULER_ROLE_ARN = local.holiday_scheduler_role_arn
SENTRY_DSN = var.sentry_dsn
PORTAL_COGNITO_ISSUER = var.portal_cognito_issuer
PORTAL_COGNITO_AUDIENCE = var.portal_cognito_audience
PORTAL_COGNITO_TRUST = jsonencode(concat(
var.portal_cognito_issuer != "" && var.portal_cognito_audience != "" ? [{ issuer = var.portal_cognito_issuer, audience = var.portal_cognito_audience }] : [],
var.portal_cognito_extra_trust,
))
}
}
}
resource "aws_iam_role" "lambda" {
for_each = local.functions
name = each.value.role_name
path = "/tf-managed/"
description = "Lambda execution role for ${each.value.function_name}"
assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
permissions_boundary = aws_iam_policy.lambda_boundary.arn
}
data "aws_iam_policy_document" "lambda" {
for_each = local.functions
dynamic "statement" {
for_each = local.lambda_identity[each.key]
content {
sid = statement.value.sid
effect = "Allow"
actions = statement.value.actions
resources = statement.value.resources
dynamic "condition" {
for_each = try(statement.value.condition, null) == null ? [] : [statement.value.condition]
content {
test = condition.value.test
variable = condition.value.variable
values = condition.value.values
}
}
}
}
}
resource "aws_iam_role_policy" "lambda" {
for_each = local.functions
name = each.key
role = aws_iam_role.lambda[each.key].id
policy = data.aws_iam_policy_document.lambda[each.key].json
}
resource "aws_iam_role_policy_attachment" "lambda_basic" {
for_each = local.functions
role = aws_iam_role.lambda[each.key].name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
# Leftover Lambda execution roles were removed after Paychex dropped
# AfterhoursWeeklyPostSend (PLAT-218). Zip handlers in src/*/app.py remain for
# packaging via scripts/package_lambdas.py.

View file

@ -44,7 +44,7 @@ output "jobs_queue_arn" {
}
output "ecs_task_role_arn" {
description = "ECS task role. paychex-checkcomponents queue policy must allow this ARN before Fargate weekly_post."
description = "ECS task role. Paychex already allows this ARN."
value = aws_iam_role.ecs_task.arn
}

View file

@ -4,13 +4,16 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
TERRAFORM = ROOT / "terraform"
LAMBDA_TF = (TERRAFORM / "lambda.tf").read_text()
HCP_IAM = (TERRAFORM / "hcp_iam.tf").read_text()
CI = (ROOT / ".github" / "workflows" / "ci.yaml").read_text()
LOCALS = (TERRAFORM / "locals.tf").read_text()
VARIABLES = (TERRAFORM / "variables.tf").read_text()
def _tf_without_comments(text: str) -> str:
return "\n".join(line.split("#", 1)[0] for line in text.splitlines())
def test_sam_template_removed():
assert not (ROOT / "template.yaml").exists()
assert not (ROOT / "samconfig.toml.example").exists()
@ -18,7 +21,8 @@ def test_sam_template_removed():
def test_zip_cd_removed():
assert not (ROOT / ".github" / "workflows" / "deploy.yaml").exists()
assert 'resource "aws_lambda_function"' not in LAMBDA_TF
for path in TERRAFORM.glob("*.tf"):
assert 'resource "aws_lambda_function"' not in path.read_text()
assert not (TERRAFORM / "apigateway.tf").exists()
assert not (TERRAFORM / "events.tf").exists()
@ -135,7 +139,8 @@ def test_openapi_uses_redocly_recommended():
def test_checkcomponents_queue_arn_variable_matches_iam_references():
assert 'variable "checkcomponents_queue_arn"' in VARIABLES
assert "var.checkcomponents_queue_arn" in LAMBDA_TF
iam = (TERRAFORM / "iam.tf").read_text()
assert "var.checkcomponents_queue_arn" in iam
boundary = (TERRAFORM / "lambda_boundary.tf").read_text()
assert "var.checkcomponents_queue_arn" in boundary
data_tf = (TERRAFORM / "data.tf").read_text()
@ -143,7 +148,22 @@ def test_checkcomponents_queue_arn_variable_matches_iam_references():
assert "checkcomponents_pair" in data_tf
def test_seven_function_names_still_on_leftover_roles():
def test_leftover_lambda_iam_roles_removed():
for path in TERRAFORM.glob("*.tf"):
body = _tf_without_comments(path.read_text())
assert 'resource "aws_iam_role" "lambda"' not in body
assert 'resource "aws_iam_role_policy" "lambda"' not in body
assert 'resource "aws_iam_role_policy_attachment" "lambda_basic"' not in body
assert 'data "aws_iam_policy_document" "lambda_assume"' not in body
def test_lambda_boundary_policy_remains():
boundary = (TERRAFORM / "lambda_boundary.tf").read_text()
assert 'resource "aws_iam_policy" "lambda_boundary"' in boundary
assert "afterhours-shift-manager-lambda-boundary" in boundary
def test_local_functions_still_lists_packaging_keys():
for name in (
"afterhours-shift-manager",
"afterhours-weekly-post",
@ -155,10 +175,7 @@ def test_seven_function_names_still_on_leftover_roles():
):
assert name in LOCALS
assert "afterhours-release-notifier" not in LOCALS
def test_weekly_post_role_is_tf_managed_name():
assert 'role_name = "afterhours-shift-manager-weekly-post"' in LOCALS
assert "weekly_post" in LOCALS
def test_github_deploy_trust_covers_image_only():