afterhours-shift-manager/template.yaml

960 lines
35 KiB
YAML
Raw Normal View History

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: After-Hours Shift Manager — Slack bot for managing on-call shifts with 3CX integration
Parameters:
Timezone:
Type: String
Default: "America/New_York"
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer Move from flat src/ to per-function directories: - src/slack-bot/ — Slack Bolt Lambda handler - src/weekly-post/ — Monday schedule + pay post - src/roster-sync/ — Daily 3CX roster sync - src/shared/ — Lambda Layer with schedule, blocks, three_cx_client Each function has its own requirements.txt and CodeUri. Shared modules are deployed as a SAM Layer (afterhours-shared) importable as `from shared.X import Y`. * Migrate secrets from SSM Parameter Store to Secrets Manager - Slack bot token and signing secret now read from Secrets Manager - 3CX credentials (domain, client-id, client-secret) moved to Secrets Manager under afterhours-shift-manager/3cx-* prefix - Channel ID is now a non-secret CloudFormation parameter (ShiftChannel) - Add shared secrets.py helper for Secrets Manager reads - Remove SSM and KMS IAM policies, add secretsmanager:GetSecretValue * Merge ring-scheduler-3cx as 4th Lambda function - Add afterhours-ring-scheduler Lambda with 4 EventBridge rules (daily 8am EST/EDT + weekend 5pm EST/EDT) for 3CX ring group routing updates - Extract shared ring_scheduler.py module for direct ring group updates from both the scheduled Lambda and the Slack bot - Replace cross-Lambda invoke with direct update_ring_group() call in the Slack bot — eliminates lambda:InvokeFunction dependency - Use RingGroup API (correct) instead of Queue API (was wrong in the original ring-scheduler repo) - Eliminate YAML config fallback — DynamoDB is the sole schedule source - Add RingGroupNumber CloudFormation parameter * Add schedule post live-update and old post deletion (#40, #41) - Store schedule message timestamp in DynamoDB (SCHEDULE_POST record) - Delete previous week's schedule post before posting the new one - Live-update the schedule post via chat_update after any pick/drop/swap/button-pickup so it always reflects current state * Disallow past shifts and add day/night labels (#43, #42) - Reject /oncall pick and /oncall drop for past dates - Show ephemeral error when stale pickup buttons are clicked - Hide pickup buttons for dates in the past - Add explicit "Day (8am-5pm)" and "Night (5pm-8am)" labels to schedule lines, pickup buttons, and shift change notifications * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 The 3CX routing was changed from ring group 800 to queue 801 in a previous PR on ring-scheduler-3cx. Updates all callers and the SAM template parameter default accordingly. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - Ring scheduler: use DynamoDBCrudPolicy (resolve_shift needs Query) - Button pickup: update 3CX for active shift type, not just night - Pick/drop/swap commands: only update 3CX when shift type is active - Swap command: add missing past-date guard - add_roster_entry: reject if extension already exists - Apply ruff formatting * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - Add _find_employee_shift() to check both day/night on weekends - Drop/swap now correctly find and operate on weekend day shifts - Pick finds first available shift type on weekends - Admin override/open/clear update 3CX for same-day active shifts * Fix dependabot directories and admin weekend shift handling Dependabot now scans per-function requirement directories instead of the repo root. Admin override/open/clear commands accept an optional day/night parameter for weekend day shift management. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications Night shift notifications on weekends were missing the type label, making them ambiguous. Also fix schedule post text fallback to use this_monday instead of now for the start date. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
ShiftChannel:
Type: String
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer Move from flat src/ to per-function directories: - src/slack-bot/ — Slack Bolt Lambda handler - src/weekly-post/ — Monday schedule + pay post - src/roster-sync/ — Daily 3CX roster sync - src/shared/ — Lambda Layer with schedule, blocks, three_cx_client Each function has its own requirements.txt and CodeUri. Shared modules are deployed as a SAM Layer (afterhours-shared) importable as `from shared.X import Y`. * Migrate secrets from SSM Parameter Store to Secrets Manager - Slack bot token and signing secret now read from Secrets Manager - 3CX credentials (domain, client-id, client-secret) moved to Secrets Manager under afterhours-shift-manager/3cx-* prefix - Channel ID is now a non-secret CloudFormation parameter (ShiftChannel) - Add shared secrets.py helper for Secrets Manager reads - Remove SSM and KMS IAM policies, add secretsmanager:GetSecretValue * Merge ring-scheduler-3cx as 4th Lambda function - Add afterhours-ring-scheduler Lambda with 4 EventBridge rules (daily 8am EST/EDT + weekend 5pm EST/EDT) for 3CX ring group routing updates - Extract shared ring_scheduler.py module for direct ring group updates from both the scheduled Lambda and the Slack bot - Replace cross-Lambda invoke with direct update_ring_group() call in the Slack bot — eliminates lambda:InvokeFunction dependency - Use RingGroup API (correct) instead of Queue API (was wrong in the original ring-scheduler repo) - Eliminate YAML config fallback — DynamoDB is the sole schedule source - Add RingGroupNumber CloudFormation parameter * Add schedule post live-update and old post deletion (#40, #41) - Store schedule message timestamp in DynamoDB (SCHEDULE_POST record) - Delete previous week's schedule post before posting the new one - Live-update the schedule post via chat_update after any pick/drop/swap/button-pickup so it always reflects current state * Disallow past shifts and add day/night labels (#43, #42) - Reject /oncall pick and /oncall drop for past dates - Show ephemeral error when stale pickup buttons are clicked - Hide pickup buttons for dates in the past - Add explicit "Day (8am-5pm)" and "Night (5pm-8am)" labels to schedule lines, pickup buttons, and shift change notifications * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 The 3CX routing was changed from ring group 800 to queue 801 in a previous PR on ring-scheduler-3cx. Updates all callers and the SAM template parameter default accordingly. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - Ring scheduler: use DynamoDBCrudPolicy (resolve_shift needs Query) - Button pickup: update 3CX for active shift type, not just night - Pick/drop/swap commands: only update 3CX when shift type is active - Swap command: add missing past-date guard - add_roster_entry: reject if extension already exists - Apply ruff formatting * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - Add _find_employee_shift() to check both day/night on weekends - Drop/swap now correctly find and operate on weekend day shifts - Pick finds first available shift type on weekends - Admin override/open/clear update 3CX for same-day active shifts * Fix dependabot directories and admin weekend shift handling Dependabot now scans per-function requirement directories instead of the repo root. Admin override/open/clear commands accept an optional day/night parameter for weekend day shift management. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications Night shift notifications on weekends were missing the type label, making them ambiguous. Also fix schedule post text fallback to use this_monday instead of now for the start date. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
Description: Slack channel ID for schedule posts and shift notifications
QueueNumber:
Type: String
Default: "801"
Description: 3CX queue extension number to update
Globals:
Function:
Runtime: python3.12
Timeout: 30
MemorySize: 1024
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer Move from flat src/ to per-function directories: - src/slack-bot/ — Slack Bolt Lambda handler - src/weekly-post/ — Monday schedule + pay post - src/roster-sync/ — Daily 3CX roster sync - src/shared/ — Lambda Layer with schedule, blocks, three_cx_client Each function has its own requirements.txt and CodeUri. Shared modules are deployed as a SAM Layer (afterhours-shared) importable as `from shared.X import Y`. * Migrate secrets from SSM Parameter Store to Secrets Manager - Slack bot token and signing secret now read from Secrets Manager - 3CX credentials (domain, client-id, client-secret) moved to Secrets Manager under afterhours-shift-manager/3cx-* prefix - Channel ID is now a non-secret CloudFormation parameter (ShiftChannel) - Add shared secrets.py helper for Secrets Manager reads - Remove SSM and KMS IAM policies, add secretsmanager:GetSecretValue * Merge ring-scheduler-3cx as 4th Lambda function - Add afterhours-ring-scheduler Lambda with 4 EventBridge rules (daily 8am EST/EDT + weekend 5pm EST/EDT) for 3CX ring group routing updates - Extract shared ring_scheduler.py module for direct ring group updates from both the scheduled Lambda and the Slack bot - Replace cross-Lambda invoke with direct update_ring_group() call in the Slack bot — eliminates lambda:InvokeFunction dependency - Use RingGroup API (correct) instead of Queue API (was wrong in the original ring-scheduler repo) - Eliminate YAML config fallback — DynamoDB is the sole schedule source - Add RingGroupNumber CloudFormation parameter * Add schedule post live-update and old post deletion (#40, #41) - Store schedule message timestamp in DynamoDB (SCHEDULE_POST record) - Delete previous week's schedule post before posting the new one - Live-update the schedule post via chat_update after any pick/drop/swap/button-pickup so it always reflects current state * Disallow past shifts and add day/night labels (#43, #42) - Reject /oncall pick and /oncall drop for past dates - Show ephemeral error when stale pickup buttons are clicked - Hide pickup buttons for dates in the past - Add explicit "Day (8am-5pm)" and "Night (5pm-8am)" labels to schedule lines, pickup buttons, and shift change notifications * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 The 3CX routing was changed from ring group 800 to queue 801 in a previous PR on ring-scheduler-3cx. Updates all callers and the SAM template parameter default accordingly. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - Ring scheduler: use DynamoDBCrudPolicy (resolve_shift needs Query) - Button pickup: update 3CX for active shift type, not just night - Pick/drop/swap commands: only update 3CX when shift type is active - Swap command: add missing past-date guard - add_roster_entry: reject if extension already exists - Apply ruff formatting * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - Add _find_employee_shift() to check both day/night on weekends - Drop/swap now correctly find and operate on weekend day shifts - Pick finds first available shift type on weekends - Admin override/open/clear update 3CX for same-day active shifts * Fix dependabot directories and admin weekend shift handling Dependabot now scans per-function requirement directories instead of the repo root. Admin override/open/clear commands accept an optional day/night parameter for weekend day shift management. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications Night shift notifications on weekends were missing the type label, making them ambiguous. Also fix schedule post text fallback to use this_monday instead of now for the start date. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
Architectures:
- arm64
PermissionsBoundary: arn:aws:iam::328440206208:policy/seahaven-lambda-execution-boundary
# Access logging + default throttling on the implicit HTTP API (audit M-18).
HttpApi:
AccessLogSettings:
DestinationArn: !GetAtt ApiAccessLogGroup.Arn
Format: '{"requestId":"$context.requestId","ip":"$context.identity.sourceIp","requestTime":"$context.requestTime","method":"$context.httpMethod","routeKey":"$context.routeKey","status":"$context.status","protocol":"$context.protocol","responseLength":"$context.responseLength","integrationError":"$context.integrationErrorMessage"}'
DefaultRouteSettings:
ThrottlingBurstLimit: 50
ThrottlingRateLimit: 100
Resources:
ApiAccessLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: /aws/apigateway/afterhours-shift-manager
RetentionInDays: 90
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer Move from flat src/ to per-function directories: - src/slack-bot/ — Slack Bolt Lambda handler - src/weekly-post/ — Monday schedule + pay post - src/roster-sync/ — Daily 3CX roster sync - src/shared/ — Lambda Layer with schedule, blocks, three_cx_client Each function has its own requirements.txt and CodeUri. Shared modules are deployed as a SAM Layer (afterhours-shared) importable as `from shared.X import Y`. * Migrate secrets from SSM Parameter Store to Secrets Manager - Slack bot token and signing secret now read from Secrets Manager - 3CX credentials (domain, client-id, client-secret) moved to Secrets Manager under afterhours-shift-manager/3cx-* prefix - Channel ID is now a non-secret CloudFormation parameter (ShiftChannel) - Add shared secrets.py helper for Secrets Manager reads - Remove SSM and KMS IAM policies, add secretsmanager:GetSecretValue * Merge ring-scheduler-3cx as 4th Lambda function - Add afterhours-ring-scheduler Lambda with 4 EventBridge rules (daily 8am EST/EDT + weekend 5pm EST/EDT) for 3CX ring group routing updates - Extract shared ring_scheduler.py module for direct ring group updates from both the scheduled Lambda and the Slack bot - Replace cross-Lambda invoke with direct update_ring_group() call in the Slack bot — eliminates lambda:InvokeFunction dependency - Use RingGroup API (correct) instead of Queue API (was wrong in the original ring-scheduler repo) - Eliminate YAML config fallback — DynamoDB is the sole schedule source - Add RingGroupNumber CloudFormation parameter * Add schedule post live-update and old post deletion (#40, #41) - Store schedule message timestamp in DynamoDB (SCHEDULE_POST record) - Delete previous week's schedule post before posting the new one - Live-update the schedule post via chat_update after any pick/drop/swap/button-pickup so it always reflects current state * Disallow past shifts and add day/night labels (#43, #42) - Reject /oncall pick and /oncall drop for past dates - Show ephemeral error when stale pickup buttons are clicked - Hide pickup buttons for dates in the past - Add explicit "Day (8am-5pm)" and "Night (5pm-8am)" labels to schedule lines, pickup buttons, and shift change notifications * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 The 3CX routing was changed from ring group 800 to queue 801 in a previous PR on ring-scheduler-3cx. Updates all callers and the SAM template parameter default accordingly. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - Ring scheduler: use DynamoDBCrudPolicy (resolve_shift needs Query) - Button pickup: update 3CX for active shift type, not just night - Pick/drop/swap commands: only update 3CX when shift type is active - Swap command: add missing past-date guard - add_roster_entry: reject if extension already exists - Apply ruff formatting * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - Add _find_employee_shift() to check both day/night on weekends - Drop/swap now correctly find and operate on weekend day shifts - Pick finds first available shift type on weekends - Admin override/open/clear update 3CX for same-day active shifts * Fix dependabot directories and admin weekend shift handling Dependabot now scans per-function requirement directories instead of the repo root. Admin override/open/clear commands accept an optional day/night parameter for weekend day shift management. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications Night shift notifications on weekends were missing the type label, making them ambiguous. Also fix schedule post text fallback to use this_monday instead of now for the start date. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
# --- Shared Lambda Layer ---
SharedLayer:
Type: AWS::Serverless::LayerVersion
Properties:
LayerName: afterhours-shared
ContentUri: src/shared/
CompatibleRuntimes:
- python3.12
CompatibleArchitectures:
- arm64
Metadata:
BuildMethod: python3.12
BuildArchitecture: arm64
# --- DynamoDB ---
ShiftTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: afterhours-shifts
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: PK
AttributeType: S
- AttributeName: SK
AttributeType: S
KeySchema:
- AttributeName: PK
KeyType: HASH
- AttributeName: SK
KeyType: RANGE
TimeToLiveSpecification:
AttributeName: expires_at
Enabled: true
# --- Slack Bot Lambda ---
SlackBotFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: afterhours-shift-manager
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer Move from flat src/ to per-function directories: - src/slack-bot/ — Slack Bolt Lambda handler - src/weekly-post/ — Monday schedule + pay post - src/roster-sync/ — Daily 3CX roster sync - src/shared/ — Lambda Layer with schedule, blocks, three_cx_client Each function has its own requirements.txt and CodeUri. Shared modules are deployed as a SAM Layer (afterhours-shared) importable as `from shared.X import Y`. * Migrate secrets from SSM Parameter Store to Secrets Manager - Slack bot token and signing secret now read from Secrets Manager - 3CX credentials (domain, client-id, client-secret) moved to Secrets Manager under afterhours-shift-manager/3cx-* prefix - Channel ID is now a non-secret CloudFormation parameter (ShiftChannel) - Add shared secrets.py helper for Secrets Manager reads - Remove SSM and KMS IAM policies, add secretsmanager:GetSecretValue * Merge ring-scheduler-3cx as 4th Lambda function - Add afterhours-ring-scheduler Lambda with 4 EventBridge rules (daily 8am EST/EDT + weekend 5pm EST/EDT) for 3CX ring group routing updates - Extract shared ring_scheduler.py module for direct ring group updates from both the scheduled Lambda and the Slack bot - Replace cross-Lambda invoke with direct update_ring_group() call in the Slack bot — eliminates lambda:InvokeFunction dependency - Use RingGroup API (correct) instead of Queue API (was wrong in the original ring-scheduler repo) - Eliminate YAML config fallback — DynamoDB is the sole schedule source - Add RingGroupNumber CloudFormation parameter * Add schedule post live-update and old post deletion (#40, #41) - Store schedule message timestamp in DynamoDB (SCHEDULE_POST record) - Delete previous week's schedule post before posting the new one - Live-update the schedule post via chat_update after any pick/drop/swap/button-pickup so it always reflects current state * Disallow past shifts and add day/night labels (#43, #42) - Reject /oncall pick and /oncall drop for past dates - Show ephemeral error when stale pickup buttons are clicked - Hide pickup buttons for dates in the past - Add explicit "Day (8am-5pm)" and "Night (5pm-8am)" labels to schedule lines, pickup buttons, and shift change notifications * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 The 3CX routing was changed from ring group 800 to queue 801 in a previous PR on ring-scheduler-3cx. Updates all callers and the SAM template parameter default accordingly. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - Ring scheduler: use DynamoDBCrudPolicy (resolve_shift needs Query) - Button pickup: update 3CX for active shift type, not just night - Pick/drop/swap commands: only update 3CX when shift type is active - Swap command: add missing past-date guard - add_roster_entry: reject if extension already exists - Apply ruff formatting * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - Add _find_employee_shift() to check both day/night on weekends - Drop/swap now correctly find and operate on weekend day shifts - Pick finds first available shift type on weekends - Admin override/open/clear update 3CX for same-day active shifts * Fix dependabot directories and admin weekend shift handling Dependabot now scans per-function requirement directories instead of the repo root. Admin override/open/clear commands accept an optional day/night parameter for weekend day shift management. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications Night shift notifications on weekends were missing the type label, making them ambiguous. Also fix schedule post text fallback to use this_monday instead of now for the start date. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
Handler: handler.handler
CodeUri: src/slack-bot/
Layers:
- !Ref SharedLayer
Environment:
Variables:
SHIFT_TABLE: !Ref ShiftTable
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer Move from flat src/ to per-function directories: - src/slack-bot/ — Slack Bolt Lambda handler - src/weekly-post/ — Monday schedule + pay post - src/roster-sync/ — Daily 3CX roster sync - src/shared/ — Lambda Layer with schedule, blocks, three_cx_client Each function has its own requirements.txt and CodeUri. Shared modules are deployed as a SAM Layer (afterhours-shared) importable as `from shared.X import Y`. * Migrate secrets from SSM Parameter Store to Secrets Manager - Slack bot token and signing secret now read from Secrets Manager - 3CX credentials (domain, client-id, client-secret) moved to Secrets Manager under afterhours-shift-manager/3cx-* prefix - Channel ID is now a non-secret CloudFormation parameter (ShiftChannel) - Add shared secrets.py helper for Secrets Manager reads - Remove SSM and KMS IAM policies, add secretsmanager:GetSecretValue * Merge ring-scheduler-3cx as 4th Lambda function - Add afterhours-ring-scheduler Lambda with 4 EventBridge rules (daily 8am EST/EDT + weekend 5pm EST/EDT) for 3CX ring group routing updates - Extract shared ring_scheduler.py module for direct ring group updates from both the scheduled Lambda and the Slack bot - Replace cross-Lambda invoke with direct update_ring_group() call in the Slack bot — eliminates lambda:InvokeFunction dependency - Use RingGroup API (correct) instead of Queue API (was wrong in the original ring-scheduler repo) - Eliminate YAML config fallback — DynamoDB is the sole schedule source - Add RingGroupNumber CloudFormation parameter * Add schedule post live-update and old post deletion (#40, #41) - Store schedule message timestamp in DynamoDB (SCHEDULE_POST record) - Delete previous week's schedule post before posting the new one - Live-update the schedule post via chat_update after any pick/drop/swap/button-pickup so it always reflects current state * Disallow past shifts and add day/night labels (#43, #42) - Reject /oncall pick and /oncall drop for past dates - Show ephemeral error when stale pickup buttons are clicked - Hide pickup buttons for dates in the past - Add explicit "Day (8am-5pm)" and "Night (5pm-8am)" labels to schedule lines, pickup buttons, and shift change notifications * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 The 3CX routing was changed from ring group 800 to queue 801 in a previous PR on ring-scheduler-3cx. Updates all callers and the SAM template parameter default accordingly. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - Ring scheduler: use DynamoDBCrudPolicy (resolve_shift needs Query) - Button pickup: update 3CX for active shift type, not just night - Pick/drop/swap commands: only update 3CX when shift type is active - Swap command: add missing past-date guard - add_roster_entry: reject if extension already exists - Apply ruff formatting * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - Add _find_employee_shift() to check both day/night on weekends - Drop/swap now correctly find and operate on weekend day shifts - Pick finds first available shift type on weekends - Admin override/open/clear update 3CX for same-day active shifts * Fix dependabot directories and admin weekend shift handling Dependabot now scans per-function requirement directories instead of the repo root. Admin override/open/clear commands accept an optional day/night parameter for weekend day shift management. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications Night shift notifications on weekends were missing the type label, making them ambiguous. Also fix schedule post text fallback to use this_monday instead of now for the start date. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
SLACK_BOT_TOKEN_SECRET: afterhours-shift-manager/slack-bot-token
SLACK_SIGNING_SECRET: afterhours-shift-manager/slack-signing-secret
SHIFT_CHANNEL: !Ref ShiftChannel
TCX_SECRET_PREFIX: afterhours-shift-manager/3cx-
QUEUE_NUMBER: !Ref QueueNumber
TZ: !Ref Timezone
# Holiday scheduling: per-holiday one-off schedules target the router,
# passing the scheduler exec role; inline activation invokes it directly.
HOLIDAY_ROUTER_ARN: !GetAtt HolidayRouterFunction.Arn
HOLIDAY_SCHEDULER_ROLE_ARN: !GetAtt HolidaySchedulerExecutionRole.Arn
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref ShiftTable
- Statement:
- Effect: Allow
Action:
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer Move from flat src/ to per-function directories: - src/slack-bot/ — Slack Bolt Lambda handler - src/weekly-post/ — Monday schedule + pay post - src/roster-sync/ — Daily 3CX roster sync - src/shared/ — Lambda Layer with schedule, blocks, three_cx_client Each function has its own requirements.txt and CodeUri. Shared modules are deployed as a SAM Layer (afterhours-shared) importable as `from shared.X import Y`. * Migrate secrets from SSM Parameter Store to Secrets Manager - Slack bot token and signing secret now read from Secrets Manager - 3CX credentials (domain, client-id, client-secret) moved to Secrets Manager under afterhours-shift-manager/3cx-* prefix - Channel ID is now a non-secret CloudFormation parameter (ShiftChannel) - Add shared secrets.py helper for Secrets Manager reads - Remove SSM and KMS IAM policies, add secretsmanager:GetSecretValue * Merge ring-scheduler-3cx as 4th Lambda function - Add afterhours-ring-scheduler Lambda with 4 EventBridge rules (daily 8am EST/EDT + weekend 5pm EST/EDT) for 3CX ring group routing updates - Extract shared ring_scheduler.py module for direct ring group updates from both the scheduled Lambda and the Slack bot - Replace cross-Lambda invoke with direct update_ring_group() call in the Slack bot — eliminates lambda:InvokeFunction dependency - Use RingGroup API (correct) instead of Queue API (was wrong in the original ring-scheduler repo) - Eliminate YAML config fallback — DynamoDB is the sole schedule source - Add RingGroupNumber CloudFormation parameter * Add schedule post live-update and old post deletion (#40, #41) - Store schedule message timestamp in DynamoDB (SCHEDULE_POST record) - Delete previous week's schedule post before posting the new one - Live-update the schedule post via chat_update after any pick/drop/swap/button-pickup so it always reflects current state * Disallow past shifts and add day/night labels (#43, #42) - Reject /oncall pick and /oncall drop for past dates - Show ephemeral error when stale pickup buttons are clicked - Hide pickup buttons for dates in the past - Add explicit "Day (8am-5pm)" and "Night (5pm-8am)" labels to schedule lines, pickup buttons, and shift change notifications * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 The 3CX routing was changed from ring group 800 to queue 801 in a previous PR on ring-scheduler-3cx. Updates all callers and the SAM template parameter default accordingly. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - Ring scheduler: use DynamoDBCrudPolicy (resolve_shift needs Query) - Button pickup: update 3CX for active shift type, not just night - Pick/drop/swap commands: only update 3CX when shift type is active - Swap command: add missing past-date guard - add_roster_entry: reject if extension already exists - Apply ruff formatting * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - Add _find_employee_shift() to check both day/night on weekends - Drop/swap now correctly find and operate on weekend day shifts - Pick finds first available shift type on weekends - Admin override/open/clear update 3CX for same-day active shifts * Fix dependabot directories and admin weekend shift handling Dependabot now scans per-function requirement directories instead of the repo root. Admin override/open/clear commands accept an optional day/night parameter for weekend day shift management. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications Night shift notifications on weekends were missing the type label, making them ambiguous. Also fix schedule post text fallback to use this_monday instead of now for the start date. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
- secretsmanager:GetSecretValue
Resource:
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer Move from flat src/ to per-function directories: - src/slack-bot/ — Slack Bolt Lambda handler - src/weekly-post/ — Monday schedule + pay post - src/roster-sync/ — Daily 3CX roster sync - src/shared/ — Lambda Layer with schedule, blocks, three_cx_client Each function has its own requirements.txt and CodeUri. Shared modules are deployed as a SAM Layer (afterhours-shared) importable as `from shared.X import Y`. * Migrate secrets from SSM Parameter Store to Secrets Manager - Slack bot token and signing secret now read from Secrets Manager - 3CX credentials (domain, client-id, client-secret) moved to Secrets Manager under afterhours-shift-manager/3cx-* prefix - Channel ID is now a non-secret CloudFormation parameter (ShiftChannel) - Add shared secrets.py helper for Secrets Manager reads - Remove SSM and KMS IAM policies, add secretsmanager:GetSecretValue * Merge ring-scheduler-3cx as 4th Lambda function - Add afterhours-ring-scheduler Lambda with 4 EventBridge rules (daily 8am EST/EDT + weekend 5pm EST/EDT) for 3CX ring group routing updates - Extract shared ring_scheduler.py module for direct ring group updates from both the scheduled Lambda and the Slack bot - Replace cross-Lambda invoke with direct update_ring_group() call in the Slack bot — eliminates lambda:InvokeFunction dependency - Use RingGroup API (correct) instead of Queue API (was wrong in the original ring-scheduler repo) - Eliminate YAML config fallback — DynamoDB is the sole schedule source - Add RingGroupNumber CloudFormation parameter * Add schedule post live-update and old post deletion (#40, #41) - Store schedule message timestamp in DynamoDB (SCHEDULE_POST record) - Delete previous week's schedule post before posting the new one - Live-update the schedule post via chat_update after any pick/drop/swap/button-pickup so it always reflects current state * Disallow past shifts and add day/night labels (#43, #42) - Reject /oncall pick and /oncall drop for past dates - Show ephemeral error when stale pickup buttons are clicked - Hide pickup buttons for dates in the past - Add explicit "Day (8am-5pm)" and "Night (5pm-8am)" labels to schedule lines, pickup buttons, and shift change notifications * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 The 3CX routing was changed from ring group 800 to queue 801 in a previous PR on ring-scheduler-3cx. Updates all callers and the SAM template parameter default accordingly. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - Ring scheduler: use DynamoDBCrudPolicy (resolve_shift needs Query) - Button pickup: update 3CX for active shift type, not just night - Pick/drop/swap commands: only update 3CX when shift type is active - Swap command: add missing past-date guard - add_roster_entry: reject if extension already exists - Apply ruff formatting * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - Add _find_employee_shift() to check both day/night on weekends - Drop/swap now correctly find and operate on weekend day shifts - Pick finds first available shift type on weekends - Admin override/open/clear update 3CX for same-day active shifts * Fix dependabot directories and admin weekend shift handling Dependabot now scans per-function requirement directories instead of the repo root. Admin override/open/clear commands accept an optional day/night parameter for weekend day shift management. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications Night shift notifications on weekends were missing the type label, making them ambiguous. Also fix schedule post text fallback to use this_monday instead of now for the start date. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
- !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:afterhours-shift-manager/*"
# Manage the per-holiday EventBridge Scheduler one-off schedules
# (08:00 activate / 17:00 deactivate of the holiday router).
- Effect: Allow
Action:
- scheduler:CreateSchedule
- scheduler:DeleteSchedule
- scheduler:GetSchedule
# Predictable names (holiday-activate-/holiday-deactivate-<date>)
# in the default group — scope to those rather than all schedules.
Resource:
- !Sub "arn:aws:scheduler:${AWS::Region}:${AWS::AccountId}:schedule/default/holiday-*"
# PassRole only for the holiday scheduler exec role, and only when
# handed to EventBridge Scheduler.
- Effect: Allow
Action: iam:PassRole
Resource: !GetAtt HolidaySchedulerExecutionRole.Arn
Condition:
StringEquals:
iam:PassedToService: scheduler.amazonaws.com
# Inline activation (holiday added mid-window) invokes the router now.
# Unqualified ARN only — we invoke the base function, no alias/version.
- Effect: Allow
Action: lambda:InvokeFunction
Resource: !GetAtt HolidayRouterFunction.Arn
Events:
SlackEvents:
Type: HttpApi
Properties:
Path: /slack/events
Method: POST
# --- Weekly Schedule Post (Monday 7am ET) ---
WeeklyPostFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: afterhours-weekly-post
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer Move from flat src/ to per-function directories: - src/slack-bot/ — Slack Bolt Lambda handler - src/weekly-post/ — Monday schedule + pay post - src/roster-sync/ — Daily 3CX roster sync - src/shared/ — Lambda Layer with schedule, blocks, three_cx_client Each function has its own requirements.txt and CodeUri. Shared modules are deployed as a SAM Layer (afterhours-shared) importable as `from shared.X import Y`. * Migrate secrets from SSM Parameter Store to Secrets Manager - Slack bot token and signing secret now read from Secrets Manager - 3CX credentials (domain, client-id, client-secret) moved to Secrets Manager under afterhours-shift-manager/3cx-* prefix - Channel ID is now a non-secret CloudFormation parameter (ShiftChannel) - Add shared secrets.py helper for Secrets Manager reads - Remove SSM and KMS IAM policies, add secretsmanager:GetSecretValue * Merge ring-scheduler-3cx as 4th Lambda function - Add afterhours-ring-scheduler Lambda with 4 EventBridge rules (daily 8am EST/EDT + weekend 5pm EST/EDT) for 3CX ring group routing updates - Extract shared ring_scheduler.py module for direct ring group updates from both the scheduled Lambda and the Slack bot - Replace cross-Lambda invoke with direct update_ring_group() call in the Slack bot — eliminates lambda:InvokeFunction dependency - Use RingGroup API (correct) instead of Queue API (was wrong in the original ring-scheduler repo) - Eliminate YAML config fallback — DynamoDB is the sole schedule source - Add RingGroupNumber CloudFormation parameter * Add schedule post live-update and old post deletion (#40, #41) - Store schedule message timestamp in DynamoDB (SCHEDULE_POST record) - Delete previous week's schedule post before posting the new one - Live-update the schedule post via chat_update after any pick/drop/swap/button-pickup so it always reflects current state * Disallow past shifts and add day/night labels (#43, #42) - Reject /oncall pick and /oncall drop for past dates - Show ephemeral error when stale pickup buttons are clicked - Hide pickup buttons for dates in the past - Add explicit "Day (8am-5pm)" and "Night (5pm-8am)" labels to schedule lines, pickup buttons, and shift change notifications * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 The 3CX routing was changed from ring group 800 to queue 801 in a previous PR on ring-scheduler-3cx. Updates all callers and the SAM template parameter default accordingly. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - Ring scheduler: use DynamoDBCrudPolicy (resolve_shift needs Query) - Button pickup: update 3CX for active shift type, not just night - Pick/drop/swap commands: only update 3CX when shift type is active - Swap command: add missing past-date guard - add_roster_entry: reject if extension already exists - Apply ruff formatting * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - Add _find_employee_shift() to check both day/night on weekends - Drop/swap now correctly find and operate on weekend day shifts - Pick finds first available shift type on weekends - Admin override/open/clear update 3CX for same-day active shifts * Fix dependabot directories and admin weekend shift handling Dependabot now scans per-function requirement directories instead of the repo root. Admin override/open/clear commands accept an optional day/night parameter for weekend day shift management. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications Night shift notifications on weekends were missing the type label, making them ambiguous. Also fix schedule post text fallback to use this_monday instead of now for the start date. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
Handler: app.handler
CodeUri: src/weekly-post/
Layers:
- !Ref SharedLayer
Environment:
Variables:
SHIFT_TABLE: !Ref ShiftTable
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer Move from flat src/ to per-function directories: - src/slack-bot/ — Slack Bolt Lambda handler - src/weekly-post/ — Monday schedule + pay post - src/roster-sync/ — Daily 3CX roster sync - src/shared/ — Lambda Layer with schedule, blocks, three_cx_client Each function has its own requirements.txt and CodeUri. Shared modules are deployed as a SAM Layer (afterhours-shared) importable as `from shared.X import Y`. * Migrate secrets from SSM Parameter Store to Secrets Manager - Slack bot token and signing secret now read from Secrets Manager - 3CX credentials (domain, client-id, client-secret) moved to Secrets Manager under afterhours-shift-manager/3cx-* prefix - Channel ID is now a non-secret CloudFormation parameter (ShiftChannel) - Add shared secrets.py helper for Secrets Manager reads - Remove SSM and KMS IAM policies, add secretsmanager:GetSecretValue * Merge ring-scheduler-3cx as 4th Lambda function - Add afterhours-ring-scheduler Lambda with 4 EventBridge rules (daily 8am EST/EDT + weekend 5pm EST/EDT) for 3CX ring group routing updates - Extract shared ring_scheduler.py module for direct ring group updates from both the scheduled Lambda and the Slack bot - Replace cross-Lambda invoke with direct update_ring_group() call in the Slack bot — eliminates lambda:InvokeFunction dependency - Use RingGroup API (correct) instead of Queue API (was wrong in the original ring-scheduler repo) - Eliminate YAML config fallback — DynamoDB is the sole schedule source - Add RingGroupNumber CloudFormation parameter * Add schedule post live-update and old post deletion (#40, #41) - Store schedule message timestamp in DynamoDB (SCHEDULE_POST record) - Delete previous week's schedule post before posting the new one - Live-update the schedule post via chat_update after any pick/drop/swap/button-pickup so it always reflects current state * Disallow past shifts and add day/night labels (#43, #42) - Reject /oncall pick and /oncall drop for past dates - Show ephemeral error when stale pickup buttons are clicked - Hide pickup buttons for dates in the past - Add explicit "Day (8am-5pm)" and "Night (5pm-8am)" labels to schedule lines, pickup buttons, and shift change notifications * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 The 3CX routing was changed from ring group 800 to queue 801 in a previous PR on ring-scheduler-3cx. Updates all callers and the SAM template parameter default accordingly. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - Ring scheduler: use DynamoDBCrudPolicy (resolve_shift needs Query) - Button pickup: update 3CX for active shift type, not just night - Pick/drop/swap commands: only update 3CX when shift type is active - Swap command: add missing past-date guard - add_roster_entry: reject if extension already exists - Apply ruff formatting * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - Add _find_employee_shift() to check both day/night on weekends - Drop/swap now correctly find and operate on weekend day shifts - Pick finds first available shift type on weekends - Admin override/open/clear update 3CX for same-day active shifts * Fix dependabot directories and admin weekend shift handling Dependabot now scans per-function requirement directories instead of the repo root. Admin override/open/clear commands accept an optional day/night parameter for weekend day shift management. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications Night shift notifications on weekends were missing the type label, making them ambiguous. Also fix schedule post text fallback to use this_monday instead of now for the start date. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
SLACK_BOT_TOKEN_SECRET: afterhours-shift-manager/slack-bot-token
SHIFT_CHANNEL: !Ref ShiftChannel
SES_SENDER: noreply@seahaven.com
PAYROLL_RECIPIENTS: payroll@seahaven.com
PAY_REPORT_USER: U0A3SC48T47
TZ: !Ref Timezone
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref ShiftTable
- Statement:
INFRA-106: nightly-sweep security remediation (auth/race/IAM) (#125) * Fix auth and race-condition flaws in shift commands Four confirmed findings from the 2026-06-17 security sweep: - register_user let any Slack user overwrite an extension already bound to a different user (account takeover). Add a DynamoDB ConditionExpression so a write only succeeds when the extension is unclaimed or already this user's; raise ExtensionAlreadyRegistered otherwise and surface a clear Slack message. - The `rate` subcommand was routed without the is_admin flag, so any user could set $0 pay rates. Gate _handle_rate on is_admin, matching the admin-command guard. - `/oncall pick` used a plain put_item (TOCTOU): two concurrent picks both won. Use the atomic claim_open_shift conditional claim so the loser gets an "already picked up" message. - swap-accept overwrote a shift independently claimed after the swap was initiated. Add reassign_if_held_by, a conditional write that only applies the swap while the override is still the requester's (or on the weekly fallback), and notify the accepter otherwise. Add tests for the register-ownership guard and the rate admin guard. Refs: INFRA * Scope shift-manager Lambda IAM to least privilege The nightly sweep flagged four over-broad permissions. Scope each to only what the function actually reads (verified against source): - WeeklyPost: secrets to slack-bot-token-* only (was the whole afterhours-shift-manager/* namespace); SES SendEmail to the single noreply@seahaven.com identity (was identity/*). - RosterSync and RingScheduler: secrets to 3cx-* only (was the whole namespace); both read only the 3cx domain/client-id/client-secret. SlackBotFunction and HolidayRouter wildcards are left unchanged — out of scope for this sweep. Refs: INFRA * fix: re-validate shift holder on swap-accept (sh-security-review RIHB-1) reassign_if_held_by trusted 'no override row' as 'still the requester's', but a weekly-held shift also has no override row. An admin clear or weekly edit between swap-init and accept could move the shift to a third party with no override, letting the accept steal it (CWE-367, confirmed HIGH). Re-resolve the current holder at accept and abort if it is no longer the requester. Adds regression test + seeds the holder in existing accept tests. * fix: complete IAM least-privilege sweep (sh-security-review) HolidayRouter secrets scope afterhours-shift-manager/* -> /3cx-* (reads only 3cx secrets); RingScheduler DynamoDBCrudPolicy -> DynamoDBReadPolicy (read-only at runtime). SlackBot wildcard left as-is (reads across all sub-prefixes; verified defensible).
2026-06-18 12:05:25 -04:00
# Least privilege: only the Slack bot token, not the whole namespace.
- Effect: Allow
Action:
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer Move from flat src/ to per-function directories: - src/slack-bot/ — Slack Bolt Lambda handler - src/weekly-post/ — Monday schedule + pay post - src/roster-sync/ — Daily 3CX roster sync - src/shared/ — Lambda Layer with schedule, blocks, three_cx_client Each function has its own requirements.txt and CodeUri. Shared modules are deployed as a SAM Layer (afterhours-shared) importable as `from shared.X import Y`. * Migrate secrets from SSM Parameter Store to Secrets Manager - Slack bot token and signing secret now read from Secrets Manager - 3CX credentials (domain, client-id, client-secret) moved to Secrets Manager under afterhours-shift-manager/3cx-* prefix - Channel ID is now a non-secret CloudFormation parameter (ShiftChannel) - Add shared secrets.py helper for Secrets Manager reads - Remove SSM and KMS IAM policies, add secretsmanager:GetSecretValue * Merge ring-scheduler-3cx as 4th Lambda function - Add afterhours-ring-scheduler Lambda with 4 EventBridge rules (daily 8am EST/EDT + weekend 5pm EST/EDT) for 3CX ring group routing updates - Extract shared ring_scheduler.py module for direct ring group updates from both the scheduled Lambda and the Slack bot - Replace cross-Lambda invoke with direct update_ring_group() call in the Slack bot — eliminates lambda:InvokeFunction dependency - Use RingGroup API (correct) instead of Queue API (was wrong in the original ring-scheduler repo) - Eliminate YAML config fallback — DynamoDB is the sole schedule source - Add RingGroupNumber CloudFormation parameter * Add schedule post live-update and old post deletion (#40, #41) - Store schedule message timestamp in DynamoDB (SCHEDULE_POST record) - Delete previous week's schedule post before posting the new one - Live-update the schedule post via chat_update after any pick/drop/swap/button-pickup so it always reflects current state * Disallow past shifts and add day/night labels (#43, #42) - Reject /oncall pick and /oncall drop for past dates - Show ephemeral error when stale pickup buttons are clicked - Hide pickup buttons for dates in the past - Add explicit "Day (8am-5pm)" and "Night (5pm-8am)" labels to schedule lines, pickup buttons, and shift change notifications * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 The 3CX routing was changed from ring group 800 to queue 801 in a previous PR on ring-scheduler-3cx. Updates all callers and the SAM template parameter default accordingly. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - Ring scheduler: use DynamoDBCrudPolicy (resolve_shift needs Query) - Button pickup: update 3CX for active shift type, not just night - Pick/drop/swap commands: only update 3CX when shift type is active - Swap command: add missing past-date guard - add_roster_entry: reject if extension already exists - Apply ruff formatting * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - Add _find_employee_shift() to check both day/night on weekends - Drop/swap now correctly find and operate on weekend day shifts - Pick finds first available shift type on weekends - Admin override/open/clear update 3CX for same-day active shifts * Fix dependabot directories and admin weekend shift handling Dependabot now scans per-function requirement directories instead of the repo root. Admin override/open/clear commands accept an optional day/night parameter for weekend day shift management. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications Night shift notifications on weekends were missing the type label, making them ambiguous. Also fix schedule post text fallback to use this_monday instead of now for the start date. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
- secretsmanager:GetSecretValue
Resource:
INFRA-106: nightly-sweep security remediation (auth/race/IAM) (#125) * Fix auth and race-condition flaws in shift commands Four confirmed findings from the 2026-06-17 security sweep: - register_user let any Slack user overwrite an extension already bound to a different user (account takeover). Add a DynamoDB ConditionExpression so a write only succeeds when the extension is unclaimed or already this user's; raise ExtensionAlreadyRegistered otherwise and surface a clear Slack message. - The `rate` subcommand was routed without the is_admin flag, so any user could set $0 pay rates. Gate _handle_rate on is_admin, matching the admin-command guard. - `/oncall pick` used a plain put_item (TOCTOU): two concurrent picks both won. Use the atomic claim_open_shift conditional claim so the loser gets an "already picked up" message. - swap-accept overwrote a shift independently claimed after the swap was initiated. Add reassign_if_held_by, a conditional write that only applies the swap while the override is still the requester's (or on the weekly fallback), and notify the accepter otherwise. Add tests for the register-ownership guard and the rate admin guard. Refs: INFRA * Scope shift-manager Lambda IAM to least privilege The nightly sweep flagged four over-broad permissions. Scope each to only what the function actually reads (verified against source): - WeeklyPost: secrets to slack-bot-token-* only (was the whole afterhours-shift-manager/* namespace); SES SendEmail to the single noreply@seahaven.com identity (was identity/*). - RosterSync and RingScheduler: secrets to 3cx-* only (was the whole namespace); both read only the 3cx domain/client-id/client-secret. SlackBotFunction and HolidayRouter wildcards are left unchanged — out of scope for this sweep. Refs: INFRA * fix: re-validate shift holder on swap-accept (sh-security-review RIHB-1) reassign_if_held_by trusted 'no override row' as 'still the requester's', but a weekly-held shift also has no override row. An admin clear or weekly edit between swap-init and accept could move the shift to a third party with no override, letting the accept steal it (CWE-367, confirmed HIGH). Re-resolve the current holder at accept and abort if it is no longer the requester. Adds regression test + seeds the holder in existing accept tests. * fix: complete IAM least-privilege sweep (sh-security-review) HolidayRouter secrets scope afterhours-shift-manager/* -> /3cx-* (reads only 3cx secrets); RingScheduler DynamoDBCrudPolicy -> DynamoDBReadPolicy (read-only at runtime). SlackBot wildcard left as-is (reads across all sub-prefixes; verified defensible).
2026-06-18 12:05:25 -04:00
- !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:afterhours-shift-manager/slack-bot-token-*"
- Effect: Allow
Action:
- ses:SendEmail
Resource:
INFRA-106: nightly-sweep security remediation (auth/race/IAM) (#125) * Fix auth and race-condition flaws in shift commands Four confirmed findings from the 2026-06-17 security sweep: - register_user let any Slack user overwrite an extension already bound to a different user (account takeover). Add a DynamoDB ConditionExpression so a write only succeeds when the extension is unclaimed or already this user's; raise ExtensionAlreadyRegistered otherwise and surface a clear Slack message. - The `rate` subcommand was routed without the is_admin flag, so any user could set $0 pay rates. Gate _handle_rate on is_admin, matching the admin-command guard. - `/oncall pick` used a plain put_item (TOCTOU): two concurrent picks both won. Use the atomic claim_open_shift conditional claim so the loser gets an "already picked up" message. - swap-accept overwrote a shift independently claimed after the swap was initiated. Add reassign_if_held_by, a conditional write that only applies the swap while the override is still the requester's (or on the weekly fallback), and notify the accepter otherwise. Add tests for the register-ownership guard and the rate admin guard. Refs: INFRA * Scope shift-manager Lambda IAM to least privilege The nightly sweep flagged four over-broad permissions. Scope each to only what the function actually reads (verified against source): - WeeklyPost: secrets to slack-bot-token-* only (was the whole afterhours-shift-manager/* namespace); SES SendEmail to the single noreply@seahaven.com identity (was identity/*). - RosterSync and RingScheduler: secrets to 3cx-* only (was the whole namespace); both read only the 3cx domain/client-id/client-secret. SlackBotFunction and HolidayRouter wildcards are left unchanged — out of scope for this sweep. Refs: INFRA * fix: re-validate shift holder on swap-accept (sh-security-review RIHB-1) reassign_if_held_by trusted 'no override row' as 'still the requester's', but a weekly-held shift also has no override row. An admin clear or weekly edit between swap-init and accept could move the shift to a third party with no override, letting the accept steal it (CWE-367, confirmed HIGH). Re-resolve the current holder at accept and abort if it is no longer the requester. Adds regression test + seeds the holder in existing accept tests. * fix: complete IAM least-privilege sweep (sh-security-review) HolidayRouter secrets scope afterhours-shift-manager/* -> /3cx-* (reads only 3cx secrets); RingScheduler DynamoDBCrudPolicy -> DynamoDBReadPolicy (read-only at runtime). SlackBot wildcard left as-is (reads across all sub-prefixes; verified defensible).
2026-06-18 12:05:25 -04:00
# Only the single sending identity (noreply@seahaven.com), not
# every identity in the account.
- !Sub "arn:aws:ses:${AWS::Region}:${AWS::AccountId}:identity/noreply@seahaven.com"
# The sending identity has a default configuration set
# (seahaven-email-events); SES authorizes SendEmail against the
# config-set resource too, so it must be granted alongside the
# identity or the send is denied. Scoped to the known set name.
- !Sub "arn:aws:ses:${AWS::Region}:${AWS::AccountId}:configuration-set/seahaven-email-events"
Events:
# EST: 7am ET = 12:00 UTC (Nov-Mar)
WeeklyPostEST:
Type: Schedule
Properties:
Schedule: cron(0 12 ? * MON *)
Description: "Post weekly schedule Monday 7am EST"
Enabled: true
# EDT: 7am ET = 11:00 UTC (Mar-Nov)
WeeklyPostEDT:
Type: Schedule
Properties:
Schedule: cron(0 11 ? * MON *)
Description: "Post weekly schedule Monday 7am EDT"
Enabled: true
# --- Roster Sync Lambda (daily sync from 3CX) ---
RosterSyncFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: afterhours-roster-sync
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer Move from flat src/ to per-function directories: - src/slack-bot/ — Slack Bolt Lambda handler - src/weekly-post/ — Monday schedule + pay post - src/roster-sync/ — Daily 3CX roster sync - src/shared/ — Lambda Layer with schedule, blocks, three_cx_client Each function has its own requirements.txt and CodeUri. Shared modules are deployed as a SAM Layer (afterhours-shared) importable as `from shared.X import Y`. * Migrate secrets from SSM Parameter Store to Secrets Manager - Slack bot token and signing secret now read from Secrets Manager - 3CX credentials (domain, client-id, client-secret) moved to Secrets Manager under afterhours-shift-manager/3cx-* prefix - Channel ID is now a non-secret CloudFormation parameter (ShiftChannel) - Add shared secrets.py helper for Secrets Manager reads - Remove SSM and KMS IAM policies, add secretsmanager:GetSecretValue * Merge ring-scheduler-3cx as 4th Lambda function - Add afterhours-ring-scheduler Lambda with 4 EventBridge rules (daily 8am EST/EDT + weekend 5pm EST/EDT) for 3CX ring group routing updates - Extract shared ring_scheduler.py module for direct ring group updates from both the scheduled Lambda and the Slack bot - Replace cross-Lambda invoke with direct update_ring_group() call in the Slack bot — eliminates lambda:InvokeFunction dependency - Use RingGroup API (correct) instead of Queue API (was wrong in the original ring-scheduler repo) - Eliminate YAML config fallback — DynamoDB is the sole schedule source - Add RingGroupNumber CloudFormation parameter * Add schedule post live-update and old post deletion (#40, #41) - Store schedule message timestamp in DynamoDB (SCHEDULE_POST record) - Delete previous week's schedule post before posting the new one - Live-update the schedule post via chat_update after any pick/drop/swap/button-pickup so it always reflects current state * Disallow past shifts and add day/night labels (#43, #42) - Reject /oncall pick and /oncall drop for past dates - Show ephemeral error when stale pickup buttons are clicked - Hide pickup buttons for dates in the past - Add explicit "Day (8am-5pm)" and "Night (5pm-8am)" labels to schedule lines, pickup buttons, and shift change notifications * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 The 3CX routing was changed from ring group 800 to queue 801 in a previous PR on ring-scheduler-3cx. Updates all callers and the SAM template parameter default accordingly. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - Ring scheduler: use DynamoDBCrudPolicy (resolve_shift needs Query) - Button pickup: update 3CX for active shift type, not just night - Pick/drop/swap commands: only update 3CX when shift type is active - Swap command: add missing past-date guard - add_roster_entry: reject if extension already exists - Apply ruff formatting * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - Add _find_employee_shift() to check both day/night on weekends - Drop/swap now correctly find and operate on weekend day shifts - Pick finds first available shift type on weekends - Admin override/open/clear update 3CX for same-day active shifts * Fix dependabot directories and admin weekend shift handling Dependabot now scans per-function requirement directories instead of the repo root. Admin override/open/clear commands accept an optional day/night parameter for weekend day shift management. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications Night shift notifications on weekends were missing the type label, making them ambiguous. Also fix schedule post text fallback to use this_monday instead of now for the start date. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
Handler: app.handler
CodeUri: src/roster-sync/
Layers:
- !Ref SharedLayer
Timeout: 60
Environment:
Variables:
SHIFT_TABLE: !Ref ShiftTable
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer Move from flat src/ to per-function directories: - src/slack-bot/ — Slack Bolt Lambda handler - src/weekly-post/ — Monday schedule + pay post - src/roster-sync/ — Daily 3CX roster sync - src/shared/ — Lambda Layer with schedule, blocks, three_cx_client Each function has its own requirements.txt and CodeUri. Shared modules are deployed as a SAM Layer (afterhours-shared) importable as `from shared.X import Y`. * Migrate secrets from SSM Parameter Store to Secrets Manager - Slack bot token and signing secret now read from Secrets Manager - 3CX credentials (domain, client-id, client-secret) moved to Secrets Manager under afterhours-shift-manager/3cx-* prefix - Channel ID is now a non-secret CloudFormation parameter (ShiftChannel) - Add shared secrets.py helper for Secrets Manager reads - Remove SSM and KMS IAM policies, add secretsmanager:GetSecretValue * Merge ring-scheduler-3cx as 4th Lambda function - Add afterhours-ring-scheduler Lambda with 4 EventBridge rules (daily 8am EST/EDT + weekend 5pm EST/EDT) for 3CX ring group routing updates - Extract shared ring_scheduler.py module for direct ring group updates from both the scheduled Lambda and the Slack bot - Replace cross-Lambda invoke with direct update_ring_group() call in the Slack bot — eliminates lambda:InvokeFunction dependency - Use RingGroup API (correct) instead of Queue API (was wrong in the original ring-scheduler repo) - Eliminate YAML config fallback — DynamoDB is the sole schedule source - Add RingGroupNumber CloudFormation parameter * Add schedule post live-update and old post deletion (#40, #41) - Store schedule message timestamp in DynamoDB (SCHEDULE_POST record) - Delete previous week's schedule post before posting the new one - Live-update the schedule post via chat_update after any pick/drop/swap/button-pickup so it always reflects current state * Disallow past shifts and add day/night labels (#43, #42) - Reject /oncall pick and /oncall drop for past dates - Show ephemeral error when stale pickup buttons are clicked - Hide pickup buttons for dates in the past - Add explicit "Day (8am-5pm)" and "Night (5pm-8am)" labels to schedule lines, pickup buttons, and shift change notifications * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 The 3CX routing was changed from ring group 800 to queue 801 in a previous PR on ring-scheduler-3cx. Updates all callers and the SAM template parameter default accordingly. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - Ring scheduler: use DynamoDBCrudPolicy (resolve_shift needs Query) - Button pickup: update 3CX for active shift type, not just night - Pick/drop/swap commands: only update 3CX when shift type is active - Swap command: add missing past-date guard - add_roster_entry: reject if extension already exists - Apply ruff formatting * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - Add _find_employee_shift() to check both day/night on weekends - Drop/swap now correctly find and operate on weekend day shifts - Pick finds first available shift type on weekends - Admin override/open/clear update 3CX for same-day active shifts * Fix dependabot directories and admin weekend shift handling Dependabot now scans per-function requirement directories instead of the repo root. Admin override/open/clear commands accept an optional day/night parameter for weekend day shift management. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications Night shift notifications on weekends were missing the type label, making them ambiguous. Also fix schedule post text fallback to use this_monday instead of now for the start date. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
TCX_SECRET_PREFIX: afterhours-shift-manager/3cx-
SYNC_GROUP: DEFAULT
TZ: !Ref Timezone
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref ShiftTable
- Statement:
INFRA-106: nightly-sweep security remediation (auth/race/IAM) (#125) * Fix auth and race-condition flaws in shift commands Four confirmed findings from the 2026-06-17 security sweep: - register_user let any Slack user overwrite an extension already bound to a different user (account takeover). Add a DynamoDB ConditionExpression so a write only succeeds when the extension is unclaimed or already this user's; raise ExtensionAlreadyRegistered otherwise and surface a clear Slack message. - The `rate` subcommand was routed without the is_admin flag, so any user could set $0 pay rates. Gate _handle_rate on is_admin, matching the admin-command guard. - `/oncall pick` used a plain put_item (TOCTOU): two concurrent picks both won. Use the atomic claim_open_shift conditional claim so the loser gets an "already picked up" message. - swap-accept overwrote a shift independently claimed after the swap was initiated. Add reassign_if_held_by, a conditional write that only applies the swap while the override is still the requester's (or on the weekly fallback), and notify the accepter otherwise. Add tests for the register-ownership guard and the rate admin guard. Refs: INFRA * Scope shift-manager Lambda IAM to least privilege The nightly sweep flagged four over-broad permissions. Scope each to only what the function actually reads (verified against source): - WeeklyPost: secrets to slack-bot-token-* only (was the whole afterhours-shift-manager/* namespace); SES SendEmail to the single noreply@seahaven.com identity (was identity/*). - RosterSync and RingScheduler: secrets to 3cx-* only (was the whole namespace); both read only the 3cx domain/client-id/client-secret. SlackBotFunction and HolidayRouter wildcards are left unchanged — out of scope for this sweep. Refs: INFRA * fix: re-validate shift holder on swap-accept (sh-security-review RIHB-1) reassign_if_held_by trusted 'no override row' as 'still the requester's', but a weekly-held shift also has no override row. An admin clear or weekly edit between swap-init and accept could move the shift to a third party with no override, letting the accept steal it (CWE-367, confirmed HIGH). Re-resolve the current holder at accept and abort if it is no longer the requester. Adds regression test + seeds the holder in existing accept tests. * fix: complete IAM least-privilege sweep (sh-security-review) HolidayRouter secrets scope afterhours-shift-manager/* -> /3cx-* (reads only 3cx secrets); RingScheduler DynamoDBCrudPolicy -> DynamoDBReadPolicy (read-only at runtime). SlackBot wildcard left as-is (reads across all sub-prefixes; verified defensible).
2026-06-18 12:05:25 -04:00
# Least privilege: only the 3cx-* secrets this function reads.
- Effect: Allow
Action:
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer Move from flat src/ to per-function directories: - src/slack-bot/ — Slack Bolt Lambda handler - src/weekly-post/ — Monday schedule + pay post - src/roster-sync/ — Daily 3CX roster sync - src/shared/ — Lambda Layer with schedule, blocks, three_cx_client Each function has its own requirements.txt and CodeUri. Shared modules are deployed as a SAM Layer (afterhours-shared) importable as `from shared.X import Y`. * Migrate secrets from SSM Parameter Store to Secrets Manager - Slack bot token and signing secret now read from Secrets Manager - 3CX credentials (domain, client-id, client-secret) moved to Secrets Manager under afterhours-shift-manager/3cx-* prefix - Channel ID is now a non-secret CloudFormation parameter (ShiftChannel) - Add shared secrets.py helper for Secrets Manager reads - Remove SSM and KMS IAM policies, add secretsmanager:GetSecretValue * Merge ring-scheduler-3cx as 4th Lambda function - Add afterhours-ring-scheduler Lambda with 4 EventBridge rules (daily 8am EST/EDT + weekend 5pm EST/EDT) for 3CX ring group routing updates - Extract shared ring_scheduler.py module for direct ring group updates from both the scheduled Lambda and the Slack bot - Replace cross-Lambda invoke with direct update_ring_group() call in the Slack bot — eliminates lambda:InvokeFunction dependency - Use RingGroup API (correct) instead of Queue API (was wrong in the original ring-scheduler repo) - Eliminate YAML config fallback — DynamoDB is the sole schedule source - Add RingGroupNumber CloudFormation parameter * Add schedule post live-update and old post deletion (#40, #41) - Store schedule message timestamp in DynamoDB (SCHEDULE_POST record) - Delete previous week's schedule post before posting the new one - Live-update the schedule post via chat_update after any pick/drop/swap/button-pickup so it always reflects current state * Disallow past shifts and add day/night labels (#43, #42) - Reject /oncall pick and /oncall drop for past dates - Show ephemeral error when stale pickup buttons are clicked - Hide pickup buttons for dates in the past - Add explicit "Day (8am-5pm)" and "Night (5pm-8am)" labels to schedule lines, pickup buttons, and shift change notifications * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 The 3CX routing was changed from ring group 800 to queue 801 in a previous PR on ring-scheduler-3cx. Updates all callers and the SAM template parameter default accordingly. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - Ring scheduler: use DynamoDBCrudPolicy (resolve_shift needs Query) - Button pickup: update 3CX for active shift type, not just night - Pick/drop/swap commands: only update 3CX when shift type is active - Swap command: add missing past-date guard - add_roster_entry: reject if extension already exists - Apply ruff formatting * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - Add _find_employee_shift() to check both day/night on weekends - Drop/swap now correctly find and operate on weekend day shifts - Pick finds first available shift type on weekends - Admin override/open/clear update 3CX for same-day active shifts * Fix dependabot directories and admin weekend shift handling Dependabot now scans per-function requirement directories instead of the repo root. Admin override/open/clear commands accept an optional day/night parameter for weekend day shift management. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications Night shift notifications on weekends were missing the type label, making them ambiguous. Also fix schedule post text fallback to use this_monday instead of now for the start date. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
- secretsmanager:GetSecretValue
Resource:
INFRA-106: nightly-sweep security remediation (auth/race/IAM) (#125) * Fix auth and race-condition flaws in shift commands Four confirmed findings from the 2026-06-17 security sweep: - register_user let any Slack user overwrite an extension already bound to a different user (account takeover). Add a DynamoDB ConditionExpression so a write only succeeds when the extension is unclaimed or already this user's; raise ExtensionAlreadyRegistered otherwise and surface a clear Slack message. - The `rate` subcommand was routed without the is_admin flag, so any user could set $0 pay rates. Gate _handle_rate on is_admin, matching the admin-command guard. - `/oncall pick` used a plain put_item (TOCTOU): two concurrent picks both won. Use the atomic claim_open_shift conditional claim so the loser gets an "already picked up" message. - swap-accept overwrote a shift independently claimed after the swap was initiated. Add reassign_if_held_by, a conditional write that only applies the swap while the override is still the requester's (or on the weekly fallback), and notify the accepter otherwise. Add tests for the register-ownership guard and the rate admin guard. Refs: INFRA * Scope shift-manager Lambda IAM to least privilege The nightly sweep flagged four over-broad permissions. Scope each to only what the function actually reads (verified against source): - WeeklyPost: secrets to slack-bot-token-* only (was the whole afterhours-shift-manager/* namespace); SES SendEmail to the single noreply@seahaven.com identity (was identity/*). - RosterSync and RingScheduler: secrets to 3cx-* only (was the whole namespace); both read only the 3cx domain/client-id/client-secret. SlackBotFunction and HolidayRouter wildcards are left unchanged — out of scope for this sweep. Refs: INFRA * fix: re-validate shift holder on swap-accept (sh-security-review RIHB-1) reassign_if_held_by trusted 'no override row' as 'still the requester's', but a weekly-held shift also has no override row. An admin clear or weekly edit between swap-init and accept could move the shift to a third party with no override, letting the accept steal it (CWE-367, confirmed HIGH). Re-resolve the current holder at accept and abort if it is no longer the requester. Adds regression test + seeds the holder in existing accept tests. * fix: complete IAM least-privilege sweep (sh-security-review) HolidayRouter secrets scope afterhours-shift-manager/* -> /3cx-* (reads only 3cx secrets); RingScheduler DynamoDBCrudPolicy -> DynamoDBReadPolicy (read-only at runtime). SlackBot wildcard left as-is (reads across all sub-prefixes; verified defensible).
2026-06-18 12:05:25 -04:00
- !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:afterhours-shift-manager/3cx-*"
Events:
# Daily at 6am ET (before the 7am schedule post and 8am 3CX scheduler)
# EST: 6am ET = 11:00 UTC (Nov-Mar)
RosterSyncEST:
Type: Schedule
Properties:
Schedule: cron(0 11 ? * * *)
Description: "Sync roster from 3CX at 6am EST"
Enabled: true
# EDT: 6am ET = 10:00 UTC (Mar-Nov)
RosterSyncEDT:
Type: Schedule
Properties:
Schedule: cron(0 10 ? * * *)
Description: "Sync roster from 3CX at 6am EDT"
Enabled: true
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer Move from flat src/ to per-function directories: - src/slack-bot/ — Slack Bolt Lambda handler - src/weekly-post/ — Monday schedule + pay post - src/roster-sync/ — Daily 3CX roster sync - src/shared/ — Lambda Layer with schedule, blocks, three_cx_client Each function has its own requirements.txt and CodeUri. Shared modules are deployed as a SAM Layer (afterhours-shared) importable as `from shared.X import Y`. * Migrate secrets from SSM Parameter Store to Secrets Manager - Slack bot token and signing secret now read from Secrets Manager - 3CX credentials (domain, client-id, client-secret) moved to Secrets Manager under afterhours-shift-manager/3cx-* prefix - Channel ID is now a non-secret CloudFormation parameter (ShiftChannel) - Add shared secrets.py helper for Secrets Manager reads - Remove SSM and KMS IAM policies, add secretsmanager:GetSecretValue * Merge ring-scheduler-3cx as 4th Lambda function - Add afterhours-ring-scheduler Lambda with 4 EventBridge rules (daily 8am EST/EDT + weekend 5pm EST/EDT) for 3CX ring group routing updates - Extract shared ring_scheduler.py module for direct ring group updates from both the scheduled Lambda and the Slack bot - Replace cross-Lambda invoke with direct update_ring_group() call in the Slack bot — eliminates lambda:InvokeFunction dependency - Use RingGroup API (correct) instead of Queue API (was wrong in the original ring-scheduler repo) - Eliminate YAML config fallback — DynamoDB is the sole schedule source - Add RingGroupNumber CloudFormation parameter * Add schedule post live-update and old post deletion (#40, #41) - Store schedule message timestamp in DynamoDB (SCHEDULE_POST record) - Delete previous week's schedule post before posting the new one - Live-update the schedule post via chat_update after any pick/drop/swap/button-pickup so it always reflects current state * Disallow past shifts and add day/night labels (#43, #42) - Reject /oncall pick and /oncall drop for past dates - Show ephemeral error when stale pickup buttons are clicked - Hide pickup buttons for dates in the past - Add explicit "Day (8am-5pm)" and "Night (5pm-8am)" labels to schedule lines, pickup buttons, and shift change notifications * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 The 3CX routing was changed from ring group 800 to queue 801 in a previous PR on ring-scheduler-3cx. Updates all callers and the SAM template parameter default accordingly. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - Ring scheduler: use DynamoDBCrudPolicy (resolve_shift needs Query) - Button pickup: update 3CX for active shift type, not just night - Pick/drop/swap commands: only update 3CX when shift type is active - Swap command: add missing past-date guard - add_roster_entry: reject if extension already exists - Apply ruff formatting * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - Add _find_employee_shift() to check both day/night on weekends - Drop/swap now correctly find and operate on weekend day shifts - Pick finds first available shift type on weekends - Admin override/open/clear update 3CX for same-day active shifts * Fix dependabot directories and admin weekend shift handling Dependabot now scans per-function requirement directories instead of the repo root. Admin override/open/clear commands accept an optional day/night parameter for weekend day shift management. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications Night shift notifications on weekends were missing the type label, making them ambiguous. Also fix schedule post text fallback to use this_monday instead of now for the start date. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
# --- Ring Scheduler (daily 3CX queue routing updates) ---
RingSchedulerFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: afterhours-ring-scheduler
Handler: app.handler
CodeUri: src/ring-scheduler/
Layers:
- !Ref SharedLayer
Timeout: 60
Environment:
Variables:
SHIFT_TABLE: !Ref ShiftTable
TCX_SECRET_PREFIX: afterhours-shift-manager/3cx-
QUEUE_NUMBER: !Ref QueueNumber
TZ: !Ref Timezone
Policies:
INFRA-106: nightly-sweep security remediation (auth/race/IAM) (#125) * Fix auth and race-condition flaws in shift commands Four confirmed findings from the 2026-06-17 security sweep: - register_user let any Slack user overwrite an extension already bound to a different user (account takeover). Add a DynamoDB ConditionExpression so a write only succeeds when the extension is unclaimed or already this user's; raise ExtensionAlreadyRegistered otherwise and surface a clear Slack message. - The `rate` subcommand was routed without the is_admin flag, so any user could set $0 pay rates. Gate _handle_rate on is_admin, matching the admin-command guard. - `/oncall pick` used a plain put_item (TOCTOU): two concurrent picks both won. Use the atomic claim_open_shift conditional claim so the loser gets an "already picked up" message. - swap-accept overwrote a shift independently claimed after the swap was initiated. Add reassign_if_held_by, a conditional write that only applies the swap while the override is still the requester's (or on the weekly fallback), and notify the accepter otherwise. Add tests for the register-ownership guard and the rate admin guard. Refs: INFRA * Scope shift-manager Lambda IAM to least privilege The nightly sweep flagged four over-broad permissions. Scope each to only what the function actually reads (verified against source): - WeeklyPost: secrets to slack-bot-token-* only (was the whole afterhours-shift-manager/* namespace); SES SendEmail to the single noreply@seahaven.com identity (was identity/*). - RosterSync and RingScheduler: secrets to 3cx-* only (was the whole namespace); both read only the 3cx domain/client-id/client-secret. SlackBotFunction and HolidayRouter wildcards are left unchanged — out of scope for this sweep. Refs: INFRA * fix: re-validate shift holder on swap-accept (sh-security-review RIHB-1) reassign_if_held_by trusted 'no override row' as 'still the requester's', but a weekly-held shift also has no override row. An admin clear or weekly edit between swap-init and accept could move the shift to a third party with no override, letting the accept steal it (CWE-367, confirmed HIGH). Re-resolve the current holder at accept and abort if it is no longer the requester. Adds regression test + seeds the holder in existing accept tests. * fix: complete IAM least-privilege sweep (sh-security-review) HolidayRouter secrets scope afterhours-shift-manager/* -> /3cx-* (reads only 3cx secrets); RingScheduler DynamoDBCrudPolicy -> DynamoDBReadPolicy (read-only at runtime). SlackBot wildcard left as-is (reads across all sub-prefixes; verified defensible).
2026-06-18 12:05:25 -04:00
- DynamoDBReadPolicy:
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer Move from flat src/ to per-function directories: - src/slack-bot/ — Slack Bolt Lambda handler - src/weekly-post/ — Monday schedule + pay post - src/roster-sync/ — Daily 3CX roster sync - src/shared/ — Lambda Layer with schedule, blocks, three_cx_client Each function has its own requirements.txt and CodeUri. Shared modules are deployed as a SAM Layer (afterhours-shared) importable as `from shared.X import Y`. * Migrate secrets from SSM Parameter Store to Secrets Manager - Slack bot token and signing secret now read from Secrets Manager - 3CX credentials (domain, client-id, client-secret) moved to Secrets Manager under afterhours-shift-manager/3cx-* prefix - Channel ID is now a non-secret CloudFormation parameter (ShiftChannel) - Add shared secrets.py helper for Secrets Manager reads - Remove SSM and KMS IAM policies, add secretsmanager:GetSecretValue * Merge ring-scheduler-3cx as 4th Lambda function - Add afterhours-ring-scheduler Lambda with 4 EventBridge rules (daily 8am EST/EDT + weekend 5pm EST/EDT) for 3CX ring group routing updates - Extract shared ring_scheduler.py module for direct ring group updates from both the scheduled Lambda and the Slack bot - Replace cross-Lambda invoke with direct update_ring_group() call in the Slack bot — eliminates lambda:InvokeFunction dependency - Use RingGroup API (correct) instead of Queue API (was wrong in the original ring-scheduler repo) - Eliminate YAML config fallback — DynamoDB is the sole schedule source - Add RingGroupNumber CloudFormation parameter * Add schedule post live-update and old post deletion (#40, #41) - Store schedule message timestamp in DynamoDB (SCHEDULE_POST record) - Delete previous week's schedule post before posting the new one - Live-update the schedule post via chat_update after any pick/drop/swap/button-pickup so it always reflects current state * Disallow past shifts and add day/night labels (#43, #42) - Reject /oncall pick and /oncall drop for past dates - Show ephemeral error when stale pickup buttons are clicked - Hide pickup buttons for dates in the past - Add explicit "Day (8am-5pm)" and "Night (5pm-8am)" labels to schedule lines, pickup buttons, and shift change notifications * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 The 3CX routing was changed from ring group 800 to queue 801 in a previous PR on ring-scheduler-3cx. Updates all callers and the SAM template parameter default accordingly. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - Ring scheduler: use DynamoDBCrudPolicy (resolve_shift needs Query) - Button pickup: update 3CX for active shift type, not just night - Pick/drop/swap commands: only update 3CX when shift type is active - Swap command: add missing past-date guard - add_roster_entry: reject if extension already exists - Apply ruff formatting * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - Add _find_employee_shift() to check both day/night on weekends - Drop/swap now correctly find and operate on weekend day shifts - Pick finds first available shift type on weekends - Admin override/open/clear update 3CX for same-day active shifts * Fix dependabot directories and admin weekend shift handling Dependabot now scans per-function requirement directories instead of the repo root. Admin override/open/clear commands accept an optional day/night parameter for weekend day shift management. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications Night shift notifications on weekends were missing the type label, making them ambiguous. Also fix schedule post text fallback to use this_monday instead of now for the start date. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
TableName: !Ref ShiftTable
- Statement:
INFRA-106: nightly-sweep security remediation (auth/race/IAM) (#125) * Fix auth and race-condition flaws in shift commands Four confirmed findings from the 2026-06-17 security sweep: - register_user let any Slack user overwrite an extension already bound to a different user (account takeover). Add a DynamoDB ConditionExpression so a write only succeeds when the extension is unclaimed or already this user's; raise ExtensionAlreadyRegistered otherwise and surface a clear Slack message. - The `rate` subcommand was routed without the is_admin flag, so any user could set $0 pay rates. Gate _handle_rate on is_admin, matching the admin-command guard. - `/oncall pick` used a plain put_item (TOCTOU): two concurrent picks both won. Use the atomic claim_open_shift conditional claim so the loser gets an "already picked up" message. - swap-accept overwrote a shift independently claimed after the swap was initiated. Add reassign_if_held_by, a conditional write that only applies the swap while the override is still the requester's (or on the weekly fallback), and notify the accepter otherwise. Add tests for the register-ownership guard and the rate admin guard. Refs: INFRA * Scope shift-manager Lambda IAM to least privilege The nightly sweep flagged four over-broad permissions. Scope each to only what the function actually reads (verified against source): - WeeklyPost: secrets to slack-bot-token-* only (was the whole afterhours-shift-manager/* namespace); SES SendEmail to the single noreply@seahaven.com identity (was identity/*). - RosterSync and RingScheduler: secrets to 3cx-* only (was the whole namespace); both read only the 3cx domain/client-id/client-secret. SlackBotFunction and HolidayRouter wildcards are left unchanged — out of scope for this sweep. Refs: INFRA * fix: re-validate shift holder on swap-accept (sh-security-review RIHB-1) reassign_if_held_by trusted 'no override row' as 'still the requester's', but a weekly-held shift also has no override row. An admin clear or weekly edit between swap-init and accept could move the shift to a third party with no override, letting the accept steal it (CWE-367, confirmed HIGH). Re-resolve the current holder at accept and abort if it is no longer the requester. Adds regression test + seeds the holder in existing accept tests. * fix: complete IAM least-privilege sweep (sh-security-review) HolidayRouter secrets scope afterhours-shift-manager/* -> /3cx-* (reads only 3cx secrets); RingScheduler DynamoDBCrudPolicy -> DynamoDBReadPolicy (read-only at runtime). SlackBot wildcard left as-is (reads across all sub-prefixes; verified defensible).
2026-06-18 12:05:25 -04:00
# Least privilege: only the 3cx-* secrets this function reads.
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer Move from flat src/ to per-function directories: - src/slack-bot/ — Slack Bolt Lambda handler - src/weekly-post/ — Monday schedule + pay post - src/roster-sync/ — Daily 3CX roster sync - src/shared/ — Lambda Layer with schedule, blocks, three_cx_client Each function has its own requirements.txt and CodeUri. Shared modules are deployed as a SAM Layer (afterhours-shared) importable as `from shared.X import Y`. * Migrate secrets from SSM Parameter Store to Secrets Manager - Slack bot token and signing secret now read from Secrets Manager - 3CX credentials (domain, client-id, client-secret) moved to Secrets Manager under afterhours-shift-manager/3cx-* prefix - Channel ID is now a non-secret CloudFormation parameter (ShiftChannel) - Add shared secrets.py helper for Secrets Manager reads - Remove SSM and KMS IAM policies, add secretsmanager:GetSecretValue * Merge ring-scheduler-3cx as 4th Lambda function - Add afterhours-ring-scheduler Lambda with 4 EventBridge rules (daily 8am EST/EDT + weekend 5pm EST/EDT) for 3CX ring group routing updates - Extract shared ring_scheduler.py module for direct ring group updates from both the scheduled Lambda and the Slack bot - Replace cross-Lambda invoke with direct update_ring_group() call in the Slack bot — eliminates lambda:InvokeFunction dependency - Use RingGroup API (correct) instead of Queue API (was wrong in the original ring-scheduler repo) - Eliminate YAML config fallback — DynamoDB is the sole schedule source - Add RingGroupNumber CloudFormation parameter * Add schedule post live-update and old post deletion (#40, #41) - Store schedule message timestamp in DynamoDB (SCHEDULE_POST record) - Delete previous week's schedule post before posting the new one - Live-update the schedule post via chat_update after any pick/drop/swap/button-pickup so it always reflects current state * Disallow past shifts and add day/night labels (#43, #42) - Reject /oncall pick and /oncall drop for past dates - Show ephemeral error when stale pickup buttons are clicked - Hide pickup buttons for dates in the past - Add explicit "Day (8am-5pm)" and "Night (5pm-8am)" labels to schedule lines, pickup buttons, and shift change notifications * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 The 3CX routing was changed from ring group 800 to queue 801 in a previous PR on ring-scheduler-3cx. Updates all callers and the SAM template parameter default accordingly. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - Ring scheduler: use DynamoDBCrudPolicy (resolve_shift needs Query) - Button pickup: update 3CX for active shift type, not just night - Pick/drop/swap commands: only update 3CX when shift type is active - Swap command: add missing past-date guard - add_roster_entry: reject if extension already exists - Apply ruff formatting * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - Add _find_employee_shift() to check both day/night on weekends - Drop/swap now correctly find and operate on weekend day shifts - Pick finds first available shift type on weekends - Admin override/open/clear update 3CX for same-day active shifts * Fix dependabot directories and admin weekend shift handling Dependabot now scans per-function requirement directories instead of the repo root. Admin override/open/clear commands accept an optional day/night parameter for weekend day shift management. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications Night shift notifications on weekends were missing the type label, making them ambiguous. Also fix schedule post text fallback to use this_monday instead of now for the start date. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource:
INFRA-106: nightly-sweep security remediation (auth/race/IAM) (#125) * Fix auth and race-condition flaws in shift commands Four confirmed findings from the 2026-06-17 security sweep: - register_user let any Slack user overwrite an extension already bound to a different user (account takeover). Add a DynamoDB ConditionExpression so a write only succeeds when the extension is unclaimed or already this user's; raise ExtensionAlreadyRegistered otherwise and surface a clear Slack message. - The `rate` subcommand was routed without the is_admin flag, so any user could set $0 pay rates. Gate _handle_rate on is_admin, matching the admin-command guard. - `/oncall pick` used a plain put_item (TOCTOU): two concurrent picks both won. Use the atomic claim_open_shift conditional claim so the loser gets an "already picked up" message. - swap-accept overwrote a shift independently claimed after the swap was initiated. Add reassign_if_held_by, a conditional write that only applies the swap while the override is still the requester's (or on the weekly fallback), and notify the accepter otherwise. Add tests for the register-ownership guard and the rate admin guard. Refs: INFRA * Scope shift-manager Lambda IAM to least privilege The nightly sweep flagged four over-broad permissions. Scope each to only what the function actually reads (verified against source): - WeeklyPost: secrets to slack-bot-token-* only (was the whole afterhours-shift-manager/* namespace); SES SendEmail to the single noreply@seahaven.com identity (was identity/*). - RosterSync and RingScheduler: secrets to 3cx-* only (was the whole namespace); both read only the 3cx domain/client-id/client-secret. SlackBotFunction and HolidayRouter wildcards are left unchanged — out of scope for this sweep. Refs: INFRA * fix: re-validate shift holder on swap-accept (sh-security-review RIHB-1) reassign_if_held_by trusted 'no override row' as 'still the requester's', but a weekly-held shift also has no override row. An admin clear or weekly edit between swap-init and accept could move the shift to a third party with no override, letting the accept steal it (CWE-367, confirmed HIGH). Re-resolve the current holder at accept and abort if it is no longer the requester. Adds regression test + seeds the holder in existing accept tests. * fix: complete IAM least-privilege sweep (sh-security-review) HolidayRouter secrets scope afterhours-shift-manager/* -> /3cx-* (reads only 3cx secrets); RingScheduler DynamoDBCrudPolicy -> DynamoDBReadPolicy (read-only at runtime). SlackBot wildcard left as-is (reads across all sub-prefixes; verified defensible).
2026-06-18 12:05:25 -04:00
- !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:afterhours-shift-manager/3cx-*"
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer Move from flat src/ to per-function directories: - src/slack-bot/ — Slack Bolt Lambda handler - src/weekly-post/ — Monday schedule + pay post - src/roster-sync/ — Daily 3CX roster sync - src/shared/ — Lambda Layer with schedule, blocks, three_cx_client Each function has its own requirements.txt and CodeUri. Shared modules are deployed as a SAM Layer (afterhours-shared) importable as `from shared.X import Y`. * Migrate secrets from SSM Parameter Store to Secrets Manager - Slack bot token and signing secret now read from Secrets Manager - 3CX credentials (domain, client-id, client-secret) moved to Secrets Manager under afterhours-shift-manager/3cx-* prefix - Channel ID is now a non-secret CloudFormation parameter (ShiftChannel) - Add shared secrets.py helper for Secrets Manager reads - Remove SSM and KMS IAM policies, add secretsmanager:GetSecretValue * Merge ring-scheduler-3cx as 4th Lambda function - Add afterhours-ring-scheduler Lambda with 4 EventBridge rules (daily 8am EST/EDT + weekend 5pm EST/EDT) for 3CX ring group routing updates - Extract shared ring_scheduler.py module for direct ring group updates from both the scheduled Lambda and the Slack bot - Replace cross-Lambda invoke with direct update_ring_group() call in the Slack bot — eliminates lambda:InvokeFunction dependency - Use RingGroup API (correct) instead of Queue API (was wrong in the original ring-scheduler repo) - Eliminate YAML config fallback — DynamoDB is the sole schedule source - Add RingGroupNumber CloudFormation parameter * Add schedule post live-update and old post deletion (#40, #41) - Store schedule message timestamp in DynamoDB (SCHEDULE_POST record) - Delete previous week's schedule post before posting the new one - Live-update the schedule post via chat_update after any pick/drop/swap/button-pickup so it always reflects current state * Disallow past shifts and add day/night labels (#43, #42) - Reject /oncall pick and /oncall drop for past dates - Show ephemeral error when stale pickup buttons are clicked - Hide pickup buttons for dates in the past - Add explicit "Day (8am-5pm)" and "Night (5pm-8am)" labels to schedule lines, pickup buttons, and shift change notifications * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 The 3CX routing was changed from ring group 800 to queue 801 in a previous PR on ring-scheduler-3cx. Updates all callers and the SAM template parameter default accordingly. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - Ring scheduler: use DynamoDBCrudPolicy (resolve_shift needs Query) - Button pickup: update 3CX for active shift type, not just night - Pick/drop/swap commands: only update 3CX when shift type is active - Swap command: add missing past-date guard - add_roster_entry: reject if extension already exists - Apply ruff formatting * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - Add _find_employee_shift() to check both day/night on weekends - Drop/swap now correctly find and operate on weekend day shifts - Pick finds first available shift type on weekends - Admin override/open/clear update 3CX for same-day active shifts * Fix dependabot directories and admin weekend shift handling Dependabot now scans per-function requirement directories instead of the repo root. Admin override/open/clear commands accept an optional day/night parameter for weekend day shift management. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications Night shift notifications on weekends were missing the type label, making them ambiguous. Also fix schedule post text fallback to use this_monday instead of now for the start date. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
Events:
# Daily at 8am ET — update after-hours routing
DailyScheduleEST:
Type: Schedule
Properties:
Schedule: cron(0 13 ? * * *)
Description: "Update 3CX queue at 8am EST"
Enabled: true
DailyScheduleEDT:
Type: Schedule
Properties:
Schedule: cron(0 12 ? * * *)
Description: "Update 3CX queue at 8am EDT"
Enabled: true
# Weekends at 5pm ET — switch to night shift person
WeekendEveningEST:
Type: Schedule
Properties:
Schedule: cron(0 22 ? * SAT,SUN *)
Description: "Update 3CX queue at 5pm EST weekends"
Enabled: true
WeekendEveningEDT:
Type: Schedule
Properties:
Schedule: cron(0 21 ? * SAT,SUN *)
Description: "Update 3CX queue at 5pm EDT weekends"
Enabled: true
# Lambda error alarm for the ring scheduler. Mirrors the account-wide
# operational convention (Lambda-Errors-<fn>, threshold 1 over one 5-min
# period, Sum, missing=notBreaching) and pages the same site-alerts SNS topic
# → AWS Chatbot → Slack as the other afterhours-* functions.
RingSchedulerErrorAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "Lambda-Errors-${RingSchedulerFunction}"
AlarmDescription: "Ring scheduler Lambda reported one or more errors"
Namespace: AWS/Lambda
MetricName: Errors
Dimensions:
- Name: FunctionName
Value: !Ref RingSchedulerFunction
Statistic: Sum
Period: 300
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching
AlarmActions:
- !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts"
# --- Holiday Router (repoints 3CX IVR/queue for a holiday day-shift) ---
# Invoked with {"action": "activate"|"deactivate", "date": "<YYYY-MM-DD>"} at
# 08:00 ET (activate) and 17:00 ET (deactivate) for each holiday date. Both
# operations are idempotent. No standing schedule here — invocation is driven
# per-holiday-date (the holiday record gates the work; off-days are no-ops).
HolidayRouterFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: afterhours-holiday-router
Handler: app.handler
CodeUri: src/holiday-router/
Layers:
- !Ref SharedLayer
Timeout: 60
Environment:
Variables:
SHIFT_TABLE: !Ref ShiftTable
TCX_SECRET_PREFIX: afterhours-shift-manager/3cx-
TZ: !Ref Timezone
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref ShiftTable
- Statement:
INFRA-106: nightly-sweep security remediation (auth/race/IAM) (#125) * Fix auth and race-condition flaws in shift commands Four confirmed findings from the 2026-06-17 security sweep: - register_user let any Slack user overwrite an extension already bound to a different user (account takeover). Add a DynamoDB ConditionExpression so a write only succeeds when the extension is unclaimed or already this user's; raise ExtensionAlreadyRegistered otherwise and surface a clear Slack message. - The `rate` subcommand was routed without the is_admin flag, so any user could set $0 pay rates. Gate _handle_rate on is_admin, matching the admin-command guard. - `/oncall pick` used a plain put_item (TOCTOU): two concurrent picks both won. Use the atomic claim_open_shift conditional claim so the loser gets an "already picked up" message. - swap-accept overwrote a shift independently claimed after the swap was initiated. Add reassign_if_held_by, a conditional write that only applies the swap while the override is still the requester's (or on the weekly fallback), and notify the accepter otherwise. Add tests for the register-ownership guard and the rate admin guard. Refs: INFRA * Scope shift-manager Lambda IAM to least privilege The nightly sweep flagged four over-broad permissions. Scope each to only what the function actually reads (verified against source): - WeeklyPost: secrets to slack-bot-token-* only (was the whole afterhours-shift-manager/* namespace); SES SendEmail to the single noreply@seahaven.com identity (was identity/*). - RosterSync and RingScheduler: secrets to 3cx-* only (was the whole namespace); both read only the 3cx domain/client-id/client-secret. SlackBotFunction and HolidayRouter wildcards are left unchanged — out of scope for this sweep. Refs: INFRA * fix: re-validate shift holder on swap-accept (sh-security-review RIHB-1) reassign_if_held_by trusted 'no override row' as 'still the requester's', but a weekly-held shift also has no override row. An admin clear or weekly edit between swap-init and accept could move the shift to a third party with no override, letting the accept steal it (CWE-367, confirmed HIGH). Re-resolve the current holder at accept and abort if it is no longer the requester. Adds regression test + seeds the holder in existing accept tests. * fix: complete IAM least-privilege sweep (sh-security-review) HolidayRouter secrets scope afterhours-shift-manager/* -> /3cx-* (reads only 3cx secrets); RingScheduler DynamoDBCrudPolicy -> DynamoDBReadPolicy (read-only at runtime). SlackBot wildcard left as-is (reads across all sub-prefixes; verified defensible).
2026-06-18 12:05:25 -04:00
# Least privilege: only the 3cx-* secrets this function reads.
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource:
INFRA-106: nightly-sweep security remediation (auth/race/IAM) (#125) * Fix auth and race-condition flaws in shift commands Four confirmed findings from the 2026-06-17 security sweep: - register_user let any Slack user overwrite an extension already bound to a different user (account takeover). Add a DynamoDB ConditionExpression so a write only succeeds when the extension is unclaimed or already this user's; raise ExtensionAlreadyRegistered otherwise and surface a clear Slack message. - The `rate` subcommand was routed without the is_admin flag, so any user could set $0 pay rates. Gate _handle_rate on is_admin, matching the admin-command guard. - `/oncall pick` used a plain put_item (TOCTOU): two concurrent picks both won. Use the atomic claim_open_shift conditional claim so the loser gets an "already picked up" message. - swap-accept overwrote a shift independently claimed after the swap was initiated. Add reassign_if_held_by, a conditional write that only applies the swap while the override is still the requester's (or on the weekly fallback), and notify the accepter otherwise. Add tests for the register-ownership guard and the rate admin guard. Refs: INFRA * Scope shift-manager Lambda IAM to least privilege The nightly sweep flagged four over-broad permissions. Scope each to only what the function actually reads (verified against source): - WeeklyPost: secrets to slack-bot-token-* only (was the whole afterhours-shift-manager/* namespace); SES SendEmail to the single noreply@seahaven.com identity (was identity/*). - RosterSync and RingScheduler: secrets to 3cx-* only (was the whole namespace); both read only the 3cx domain/client-id/client-secret. SlackBotFunction and HolidayRouter wildcards are left unchanged — out of scope for this sweep. Refs: INFRA * fix: re-validate shift holder on swap-accept (sh-security-review RIHB-1) reassign_if_held_by trusted 'no override row' as 'still the requester's', but a weekly-held shift also has no override row. An admin clear or weekly edit between swap-init and accept could move the shift to a third party with no override, letting the accept steal it (CWE-367, confirmed HIGH). Re-resolve the current holder at accept and abort if it is no longer the requester. Adds regression test + seeds the holder in existing accept tests. * fix: complete IAM least-privilege sweep (sh-security-review) HolidayRouter secrets scope afterhours-shift-manager/* -> /3cx-* (reads only 3cx secrets); RingScheduler DynamoDBCrudPolicy -> DynamoDBReadPolicy (read-only at runtime). SlackBot wildcard left as-is (reads across all sub-prefixes; verified defensible).
2026-06-18 12:05:25 -04:00
- !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:afterhours-shift-manager/3cx-*"
# Lambda error alarm for the holiday router. Mirrors the account-wide
# operational convention (Lambda-Errors-<fn>, threshold 1 over one 5-min
# period, Sum, missing=notBreaching) and pages the same site-alerts SNS topic
# → AWS Chatbot → Slack as the other afterhours-* functions.
HolidayRouterErrorAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "Lambda-Errors-${HolidayRouterFunction}"
AlarmDescription: "Holiday router Lambda reported one or more errors"
Namespace: AWS/Lambda
MetricName: Errors
Dimensions:
- Name: FunctionName
Value: !Ref HolidayRouterFunction
Statistic: Sum
Period: 300
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching
AlarmActions:
- !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts"
# EventBridge Scheduler execution role. The slack-bot creates one-off
# schedules per holiday date (08:00 activate / 17:00 deactivate); Scheduler
# assumes this role to invoke the holiday router. Auto-named (no RoleName) so
# the deploy's CAPABILITY_IAM suffices — cd-sam does not pass
# CAPABILITY_NAMED_IAM. The permissions boundary is REQUIRED: the scoped
# github-cfn-execution-role gates iam:CreateRole/PutRolePolicy on roles
# carrying exactly this boundary, so the CI deploy is denied without it.
HolidaySchedulerExecutionRole:
Type: AWS::IAM::Role
Properties:
PermissionsBoundary: arn:aws:iam::328440206208:policy/seahaven-lambda-execution-boundary
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: scheduler.amazonaws.com
Action: sts:AssumeRole
Condition:
StringEquals:
aws:SourceAccount: !Ref AWS::AccountId
# Only schedules this stack creates (holiday-* in the default group)
# may assume the role — not any schedule in the account.
ArnLike:
aws:SourceArn: !Sub "arn:aws:scheduler:${AWS::Region}:${AWS::AccountId}:schedule/default/holiday-*"
Policies:
- PolicyName: invoke-holiday-router
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action: lambda:InvokeFunction
Resource: !GetAtt HolidayRouterFunction.Arn
Add changelog-driven releases and App Home tab (#112) * Add changelog-driven releases and App Home tab Version the bot continuously from CHANGELOG.md (the single source of truth for both the version and the staff-readable notes) and surface changes to users in two ways: - A new afterhours-release-notifier Lambda posts a "What's New" message to the shift channel on minor/major releases (patches stay silent). - The bot gains an App Home "About" tab showing what it does, the command list, and the current version's notes. release.yaml runs on Deploy success (not release:published — GITHUB_TOKEN events don't start downstream workflows), checks out the deployed commit, and tags + publishes a GitHub Release + invokes the notifier. It assumes a dedicated, boundary-carrying OIDC role scoped to InvokeFunction on the notifier; the account's cfn role gates role creation on that boundary. The manual Version Bump workflow is retired. A CI guard enforces that a CHANGELOG edit is a clean SemVer bump and that the in-package copy matches. * Harden release workflow and regex against CodeQL findings Address three code-scanning alerts on the PR: - Critical (actions/untrusted-checkout): split release.yaml into a read-only `prepare` job that checks out and runs repo code, and a privileged `publish` job (contents:write + OIDC) that never checks out repo code — it tags, releases, and invokes purely through the GitHub and AWS APIs. Also assert head_branch == main. - High x2 (py/polynomial-redos): rewrite the italic and link regexes in markdown_to_mrkdwn with possessive quantifiers and exclusive character classes so they run in linear time on adversarial input. Adds a regression test. * Move release/announce into Deploy workflow to clear CodeQL The workflow_run-triggered release.yaml kept tripping CodeQL's privileged-context rules (untrusted-checkout, then cache-poisoning) — CodeQL distrusts any workflow_run that checks out a ref, regardless of the main-only guarantee, and there is no autofix. Fold the release job into deploy.yaml gated on `needs: deploy`. A push-to-main run is a trusted context, so checking out and running repo code with write/OIDC is safe there. This still gates on deploy success and serializes via the deploy concurrency group, and removes the separate workflow entirely.
2026-06-11 19:41:31 -04:00
# --- Release Notifier (invoked by release.yaml on minor/major releases) ---
ReleaseNotifierFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: afterhours-release-notifier
Handler: app.handler
CodeUri: src/release-notifier/
Layers:
- !Ref SharedLayer
Environment:
Variables:
SLACK_BOT_TOKEN_SECRET: afterhours-shift-manager/slack-bot-token
SHIFT_CHANNEL: !Ref ShiftChannel
TZ: !Ref Timezone
Policies:
# Least privilege: only the Slack bot token, not the whole namespace.
- Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource:
- !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:afterhours-shift-manager/slack-bot-token-*"
# GitHub-OIDC role assumed by release.yaml to invoke the notifier. Auto-named
# (no RoleName) so the deploy's CAPABILITY_IAM is sufficient — cd-sam does not
# pass CAPABILITY_NAMED_IAM. Trust + permission are scoped to the minimum: this
# repo's main ref + release workflow, and InvokeFunction on the notifier alone.
# Its ARN is surfaced as a stack output and set once as the
# RELEASE_NOTIFY_INVOKE_ROLE_ARN repo variable (see README).
#
# The permissions boundary is REQUIRED, not optional: the scoped
# github-cfn-execution-role's IAM policy gates iam:CreateRole/PutRolePolicy on
# the role carrying exactly this boundary, so the CI deploy is denied without
# it. The boundary itself permits lambda:InvokeFunction (Sid LambdaInvoke), so
# it does not restrict this role's one job.
ReleaseNotifyInvokeRole:
Type: AWS::IAM::Role
Properties:
PermissionsBoundary: arn:aws:iam::328440206208:policy/seahaven-lambda-execution-boundary
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Federated: !Sub "arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com"
Action: sts:AssumeRoleWithWebIdentity
Condition:
StringEquals:
token.actions.githubusercontent.com:aud: sts.amazonaws.com
StringLike:
token.actions.githubusercontent.com:sub: "repo:Sea-Haven-Industries/afterhours-shift-manager:ref:refs/heads/main"
# Defense-in-depth: only the Deploy workflow's release job may assume
# this role, not any workflow running on main. (The release job lives
# in deploy.yaml; this must match that workflow's path.)
token.actions.githubusercontent.com:job_workflow_ref: "Sea-Haven-Industries/afterhours-shift-manager/.github/workflows/deploy.yaml@refs/heads/main"
Add changelog-driven releases and App Home tab (#112) * Add changelog-driven releases and App Home tab Version the bot continuously from CHANGELOG.md (the single source of truth for both the version and the staff-readable notes) and surface changes to users in two ways: - A new afterhours-release-notifier Lambda posts a "What's New" message to the shift channel on minor/major releases (patches stay silent). - The bot gains an App Home "About" tab showing what it does, the command list, and the current version's notes. release.yaml runs on Deploy success (not release:published — GITHUB_TOKEN events don't start downstream workflows), checks out the deployed commit, and tags + publishes a GitHub Release + invokes the notifier. It assumes a dedicated, boundary-carrying OIDC role scoped to InvokeFunction on the notifier; the account's cfn role gates role creation on that boundary. The manual Version Bump workflow is retired. A CI guard enforces that a CHANGELOG edit is a clean SemVer bump and that the in-package copy matches. * Harden release workflow and regex against CodeQL findings Address three code-scanning alerts on the PR: - Critical (actions/untrusted-checkout): split release.yaml into a read-only `prepare` job that checks out and runs repo code, and a privileged `publish` job (contents:write + OIDC) that never checks out repo code — it tags, releases, and invokes purely through the GitHub and AWS APIs. Also assert head_branch == main. - High x2 (py/polynomial-redos): rewrite the italic and link regexes in markdown_to_mrkdwn with possessive quantifiers and exclusive character classes so they run in linear time on adversarial input. Adds a regression test. * Move release/announce into Deploy workflow to clear CodeQL The workflow_run-triggered release.yaml kept tripping CodeQL's privileged-context rules (untrusted-checkout, then cache-poisoning) — CodeQL distrusts any workflow_run that checks out a ref, regardless of the main-only guarantee, and there is no autofix. Fold the release job into deploy.yaml gated on `needs: deploy`. A push-to-main run is a trusted context, so checking out and running repo code with write/OIDC is safe there. This still gates on deploy success and serializes via the deploy concurrency group, and removes the separate workflow entirely.
2026-06-11 19:41:31 -04:00
Policies:
- PolicyName: invoke-release-notifier
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action: lambda:InvokeFunction
Resource: !GetAtt ReleaseNotifierFunction.Arn
Add CloudWatch alarm coverage for all functions, table, and HTTP API (#126) * Add CloudWatch alarm coverage for all functions, table, and HTTP API Extend the in-template Lambda-<Metric>-<fn> alarm convention to full coverage: - Errors (Sum, >=1/5min) for roster-sync and release-notifier, plus orphan adoption of slack-bot and weekly-post (live alarms of those exact names already exist outside the stack and must be deleted before deploy). - Duration (Maximum, ~80% of timeout, 2-of-3) for all six functions. - Throttles (Sum, >=1/5min) for all six functions. - DynamoDB ThrottledRequests (Sum, >=1/5min) on afterhours-shifts. SystemErrors omitted: AWS emits it only per-Operation, so a TableName-only alarm would sit permanently in INSUFFICIENT_DATA. - API Gateway v2 4xx (>=5), 5xx (>=1), and p99 Latency (~3000ms, 2-of-3) on the implicit ServerlessHttpApi. All alarms page the shared site-alerts SNS topic, no OKActions, TreatMissingData notBreaching. README updated with a Monitoring & Alarms section. Duration and API latency thresholds pending sign-off. * Fix DynamoDB throttle alarm metric: use Read/WriteThrottleEvents ThrottledRequests is not emitted at the TableName-only dimension (only TableName+Operation), so the table-level alarm would sit permanently in INSUFFICIENT_DATA and never fire. Replace with ReadThrottleEvents and WriteThrottleEvents, which AWS/DynamoDB emits at the TableName dimension. * Correct DynamoDB alarm docs and drop sign-off wording README DynamoDB section now lists the alarms actually shipped (DDB-ReadThrottle / DDB-WriteThrottle on Read/WriteThrottleEvents) instead of the stale ThrottledRequests alarm. Thresholds are owner-approved, so remove PENDING ADAM SIGN-OFF wording from template.yaml comments.
2026-06-17 14:45:47 -04:00
# ===========================================================================
# CloudWatch alarm coverage (Wave 1 PR A). All alarms page the same
# site-alerts SNS topic → AWS Chatbot → Slack as the existing Errors alarms.
# No OKActions by design (the existing Errors alarms have none either).
# Naming follows the in-template convention: Lambda-<Metric>-${Fn}.
# ===========================================================================
# --- Lambda Errors alarms (clone of HolidayRouterErrorAlarm) ---
# Errors for ring-scheduler + holiday-router already exist above.
RosterSyncErrorAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "Lambda-Errors-${RosterSyncFunction}"
AlarmDescription: "Roster sync Lambda reported one or more errors"
Namespace: AWS/Lambda
MetricName: Errors
Dimensions:
- Name: FunctionName
Value: !Ref RosterSyncFunction
Statistic: Sum
Period: 300
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching
AlarmActions:
- !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts"
ReleaseNotifierErrorAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "Lambda-Errors-${ReleaseNotifierFunction}"
AlarmDescription: "Release notifier Lambda reported one or more errors"
Namespace: AWS/Lambda
MetricName: Errors
Dimensions:
- Name: FunctionName
Value: !Ref ReleaseNotifierFunction
Statistic: Sum
Period: 300
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching
AlarmActions:
- !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts"
# ORPHAN ADOPTION: live alarms named Lambda-Errors-afterhours-shift-manager
# and Lambda-Errors-afterhours-weekly-post already exist OUTSIDE the stack.
# They MUST be deleted immediately before this stack deploys, or CloudFormation
# will fail to create these resources (AlarmName collision). See PR body.
SlackBotErrorAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "Lambda-Errors-${SlackBotFunction}"
AlarmDescription: "Slack bot Lambda reported one or more errors"
Namespace: AWS/Lambda
MetricName: Errors
Dimensions:
- Name: FunctionName
Value: !Ref SlackBotFunction
Statistic: Sum
Period: 300
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching
AlarmActions:
- !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts"
WeeklyPostErrorAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "Lambda-Errors-${WeeklyPostFunction}"
AlarmDescription: "Weekly post Lambda reported one or more errors"
Namespace: AWS/Lambda
MetricName: Errors
Dimensions:
- Name: FunctionName
Value: !Ref WeeklyPostFunction
Statistic: Sum
Period: 300
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching
AlarmActions:
- !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts"
# --- Lambda Duration alarms (Maximum, ms; 2-of-3 evaluation) ---
# Thresholds set to ~80% of each function's timeout, with 2-of-3 evaluation.
# Timeouts: slack-bot/weekly-post/release-notifier = 30s (global default);
# roster-sync/ring-scheduler/holiday-router = 60s.
SlackBotDurationAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "Lambda-Duration-${SlackBotFunction}"
AlarmDescription: "Slack bot Lambda duration approaching its 30s timeout (>=24s)"
Namespace: AWS/Lambda
MetricName: Duration
Dimensions:
- Name: FunctionName
Value: !Ref SlackBotFunction
Statistic: Maximum
Period: 300
EvaluationPeriods: 3
DatapointsToAlarm: 2
Threshold: 24000
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching
AlarmActions:
- !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts"
WeeklyPostDurationAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "Lambda-Duration-${WeeklyPostFunction}"
AlarmDescription: "Weekly post Lambda duration approaching its 30s timeout (>=24s)"
Namespace: AWS/Lambda
MetricName: Duration
Dimensions:
- Name: FunctionName
Value: !Ref WeeklyPostFunction
Statistic: Maximum
Period: 300
EvaluationPeriods: 3
DatapointsToAlarm: 2
Threshold: 24000
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching
AlarmActions:
- !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts"
RosterSyncDurationAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "Lambda-Duration-${RosterSyncFunction}"
AlarmDescription: "Roster sync Lambda duration approaching its 60s timeout (>=48s)"
Namespace: AWS/Lambda
MetricName: Duration
Dimensions:
- Name: FunctionName
Value: !Ref RosterSyncFunction
Statistic: Maximum
Period: 300
EvaluationPeriods: 3
DatapointsToAlarm: 2
Threshold: 48000
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching
AlarmActions:
- !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts"
RingSchedulerDurationAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "Lambda-Duration-${RingSchedulerFunction}"
AlarmDescription: "Ring scheduler Lambda duration approaching its 60s timeout (>=48s)"
Namespace: AWS/Lambda
MetricName: Duration
Dimensions:
- Name: FunctionName
Value: !Ref RingSchedulerFunction
Statistic: Maximum
Period: 300
EvaluationPeriods: 3
DatapointsToAlarm: 2
Threshold: 48000
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching
AlarmActions:
- !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts"
HolidayRouterDurationAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "Lambda-Duration-${HolidayRouterFunction}"
AlarmDescription: "Holiday router Lambda duration approaching its 60s timeout (>=48s)"
Namespace: AWS/Lambda
MetricName: Duration
Dimensions:
- Name: FunctionName
Value: !Ref HolidayRouterFunction
Statistic: Maximum
Period: 300
EvaluationPeriods: 3
DatapointsToAlarm: 2
Threshold: 48000
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching
AlarmActions:
- !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts"
ReleaseNotifierDurationAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "Lambda-Duration-${ReleaseNotifierFunction}"
AlarmDescription: "Release notifier Lambda duration approaching its 30s timeout (>=24s)"
Namespace: AWS/Lambda
MetricName: Duration
Dimensions:
- Name: FunctionName
Value: !Ref ReleaseNotifierFunction
Statistic: Maximum
Period: 300
EvaluationPeriods: 3
DatapointsToAlarm: 2
Threshold: 24000
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching
AlarmActions:
- !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts"
# --- Lambda Throttles alarms (Sum; threshold 1 over one 5-min period) ---
SlackBotThrottlesAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "Lambda-Throttles-${SlackBotFunction}"
AlarmDescription: "Slack bot Lambda was throttled (concurrency limit hit)"
Namespace: AWS/Lambda
MetricName: Throttles
Dimensions:
- Name: FunctionName
Value: !Ref SlackBotFunction
Statistic: Sum
Period: 300
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching
AlarmActions:
- !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts"
WeeklyPostThrottlesAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "Lambda-Throttles-${WeeklyPostFunction}"
AlarmDescription: "Weekly post Lambda was throttled (concurrency limit hit)"
Namespace: AWS/Lambda
MetricName: Throttles
Dimensions:
- Name: FunctionName
Value: !Ref WeeklyPostFunction
Statistic: Sum
Period: 300
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching
AlarmActions:
- !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts"
RosterSyncThrottlesAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "Lambda-Throttles-${RosterSyncFunction}"
AlarmDescription: "Roster sync Lambda was throttled (concurrency limit hit)"
Namespace: AWS/Lambda
MetricName: Throttles
Dimensions:
- Name: FunctionName
Value: !Ref RosterSyncFunction
Statistic: Sum
Period: 300
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching
AlarmActions:
- !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts"
RingSchedulerThrottlesAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "Lambda-Throttles-${RingSchedulerFunction}"
AlarmDescription: "Ring scheduler Lambda was throttled (concurrency limit hit)"
Namespace: AWS/Lambda
MetricName: Throttles
Dimensions:
- Name: FunctionName
Value: !Ref RingSchedulerFunction
Statistic: Sum
Period: 300
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching
AlarmActions:
- !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts"
HolidayRouterThrottlesAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "Lambda-Throttles-${HolidayRouterFunction}"
AlarmDescription: "Holiday router Lambda was throttled (concurrency limit hit)"
Namespace: AWS/Lambda
MetricName: Throttles
Dimensions:
- Name: FunctionName
Value: !Ref HolidayRouterFunction
Statistic: Sum
Period: 300
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching
AlarmActions:
- !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts"
ReleaseNotifierThrottlesAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "Lambda-Throttles-${ReleaseNotifierFunction}"
AlarmDescription: "Release notifier Lambda was throttled (concurrency limit hit)"
Namespace: AWS/Lambda
MetricName: Throttles
Dimensions:
- Name: FunctionName
Value: !Ref ReleaseNotifierFunction
Statistic: Sum
Period: 300
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching
AlarmActions:
- !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts"
# --- DynamoDB alarms (afterhours-shifts table) ---
# ReadThrottleEvents / WriteThrottleEvents are the table-level throttle
# signals: AWS/DynamoDB emits them at the TableName dimension, so these
# alarms transition normally. (ThrottledRequests and SystemErrors are NOT
# emitted at TableName-only granularity — only at TableName+Operation — so
# alarms on them sit permanently in INSUFFICIENT_DATA and never fire.)
ShiftTableReadThrottleAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "DDB-ReadThrottle-${ShiftTable}"
AlarmDescription: "afterhours-shifts table had one or more read throttle events"
Namespace: AWS/DynamoDB
MetricName: ReadThrottleEvents
Dimensions:
- Name: TableName
Value: !Ref ShiftTable
Statistic: Sum
Period: 300
EvaluationPeriods: 1
Threshold: 0
ComparisonOperator: GreaterThanThreshold
TreatMissingData: notBreaching
AlarmActions:
- !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts"
ShiftTableWriteThrottleAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "DDB-WriteThrottle-${ShiftTable}"
AlarmDescription: "afterhours-shifts table had one or more write throttle events"
Namespace: AWS/DynamoDB
MetricName: WriteThrottleEvents
Dimensions:
- Name: TableName
Value: !Ref ShiftTable
Statistic: Sum
Period: 300
EvaluationPeriods: 1
Threshold: 0
ComparisonOperator: GreaterThanThreshold
TreatMissingData: notBreaching
AlarmActions:
- !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts"
# --- API Gateway v2 (HTTP API) alarms on the implicit ServerlessHttpApi ---
# AWS::ApiGatewayV2 metric names: 4xx, 5xx, Latency; dimension ApiId.
ApiGateway4xxAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "ApiGateway-4xx-${ServerlessHttpApi}"
AlarmDescription: "Elevated 4xx responses on the Slack events HTTP API"
Namespace: AWS/ApiGateway
MetricName: 4xx
Dimensions:
- Name: ApiId
Value: !Ref ServerlessHttpApi
Statistic: Sum
Period: 300
EvaluationPeriods: 1
Threshold: 5
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching
AlarmActions:
- !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts"
ApiGateway5xxAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "ApiGateway-5xx-${ServerlessHttpApi}"
AlarmDescription: "5xx responses on the Slack events HTTP API"
Namespace: AWS/ApiGateway
MetricName: 5xx
Dimensions:
- Name: ApiId
Value: !Ref ServerlessHttpApi
Statistic: Sum
Period: 300
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching
AlarmActions:
- !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts"
# Latency p99 via ExtendedStatistic. ~3000ms target chosen alongside the
# Lambda Duration thresholds (Slack requires a fast 3s ack).
ApiGatewayLatencyAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "ApiGateway-Latency-${ServerlessHttpApi}"
AlarmDescription: "p99 latency on the Slack events HTTP API exceeded 3s"
Namespace: AWS/ApiGateway
MetricName: Latency
Dimensions:
- Name: ApiId
Value: !Ref ServerlessHttpApi
ExtendedStatistic: p99
Period: 300
EvaluationPeriods: 3
DatapointsToAlarm: 2
Threshold: 3000
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching
AlarmActions:
- !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:site-alerts"
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer Move from flat src/ to per-function directories: - src/slack-bot/ — Slack Bolt Lambda handler - src/weekly-post/ — Monday schedule + pay post - src/roster-sync/ — Daily 3CX roster sync - src/shared/ — Lambda Layer with schedule, blocks, three_cx_client Each function has its own requirements.txt and CodeUri. Shared modules are deployed as a SAM Layer (afterhours-shared) importable as `from shared.X import Y`. * Migrate secrets from SSM Parameter Store to Secrets Manager - Slack bot token and signing secret now read from Secrets Manager - 3CX credentials (domain, client-id, client-secret) moved to Secrets Manager under afterhours-shift-manager/3cx-* prefix - Channel ID is now a non-secret CloudFormation parameter (ShiftChannel) - Add shared secrets.py helper for Secrets Manager reads - Remove SSM and KMS IAM policies, add secretsmanager:GetSecretValue * Merge ring-scheduler-3cx as 4th Lambda function - Add afterhours-ring-scheduler Lambda with 4 EventBridge rules (daily 8am EST/EDT + weekend 5pm EST/EDT) for 3CX ring group routing updates - Extract shared ring_scheduler.py module for direct ring group updates from both the scheduled Lambda and the Slack bot - Replace cross-Lambda invoke with direct update_ring_group() call in the Slack bot — eliminates lambda:InvokeFunction dependency - Use RingGroup API (correct) instead of Queue API (was wrong in the original ring-scheduler repo) - Eliminate YAML config fallback — DynamoDB is the sole schedule source - Add RingGroupNumber CloudFormation parameter * Add schedule post live-update and old post deletion (#40, #41) - Store schedule message timestamp in DynamoDB (SCHEDULE_POST record) - Delete previous week's schedule post before posting the new one - Live-update the schedule post via chat_update after any pick/drop/swap/button-pickup so it always reflects current state * Disallow past shifts and add day/night labels (#43, #42) - Reject /oncall pick and /oncall drop for past dates - Show ephemeral error when stale pickup buttons are clicked - Hide pickup buttons for dates in the past - Add explicit "Day (8am-5pm)" and "Night (5pm-8am)" labels to schedule lines, pickup buttons, and shift change notifications * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 The 3CX routing was changed from ring group 800 to queue 801 in a previous PR on ring-scheduler-3cx. Updates all callers and the SAM template parameter default accordingly. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - Ring scheduler: use DynamoDBCrudPolicy (resolve_shift needs Query) - Button pickup: update 3CX for active shift type, not just night - Pick/drop/swap commands: only update 3CX when shift type is active - Swap command: add missing past-date guard - add_roster_entry: reject if extension already exists - Apply ruff formatting * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - Add _find_employee_shift() to check both day/night on weekends - Drop/swap now correctly find and operate on weekend day shifts - Pick finds first available shift type on weekends - Admin override/open/clear update 3CX for same-day active shifts * Fix dependabot directories and admin weekend shift handling Dependabot now scans per-function requirement directories instead of the repo root. Admin override/open/clear commands accept an optional day/night parameter for weekend day shift management. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications Night shift notifications on weekends were missing the type label, making them ambiguous. Also fix schedule post text fallback to use this_monday instead of now for the start date. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
# --- CloudWatch Log Groups (explicit 60-day retention) ---
SlackBotLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub "/aws/lambda/${SlackBotFunction}"
RetentionInDays: 60
WeeklyPostLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub "/aws/lambda/${WeeklyPostFunction}"
RetentionInDays: 60
RosterSyncLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub "/aws/lambda/${RosterSyncFunction}"
RetentionInDays: 60
RingSchedulerLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub "/aws/lambda/${RingSchedulerFunction}"
RetentionInDays: 60
HolidayRouterLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub "/aws/lambda/${HolidayRouterFunction}"
RetentionInDays: 60
Add changelog-driven releases and App Home tab (#112) * Add changelog-driven releases and App Home tab Version the bot continuously from CHANGELOG.md (the single source of truth for both the version and the staff-readable notes) and surface changes to users in two ways: - A new afterhours-release-notifier Lambda posts a "What's New" message to the shift channel on minor/major releases (patches stay silent). - The bot gains an App Home "About" tab showing what it does, the command list, and the current version's notes. release.yaml runs on Deploy success (not release:published — GITHUB_TOKEN events don't start downstream workflows), checks out the deployed commit, and tags + publishes a GitHub Release + invokes the notifier. It assumes a dedicated, boundary-carrying OIDC role scoped to InvokeFunction on the notifier; the account's cfn role gates role creation on that boundary. The manual Version Bump workflow is retired. A CI guard enforces that a CHANGELOG edit is a clean SemVer bump and that the in-package copy matches. * Harden release workflow and regex against CodeQL findings Address three code-scanning alerts on the PR: - Critical (actions/untrusted-checkout): split release.yaml into a read-only `prepare` job that checks out and runs repo code, and a privileged `publish` job (contents:write + OIDC) that never checks out repo code — it tags, releases, and invokes purely through the GitHub and AWS APIs. Also assert head_branch == main. - High x2 (py/polynomial-redos): rewrite the italic and link regexes in markdown_to_mrkdwn with possessive quantifiers and exclusive character classes so they run in linear time on adversarial input. Adds a regression test. * Move release/announce into Deploy workflow to clear CodeQL The workflow_run-triggered release.yaml kept tripping CodeQL's privileged-context rules (untrusted-checkout, then cache-poisoning) — CodeQL distrusts any workflow_run that checks out a ref, regardless of the main-only guarantee, and there is no autofix. Fold the release job into deploy.yaml gated on `needs: deploy`. A push-to-main run is a trusted context, so checking out and running repo code with write/OIDC is safe there. This still gates on deploy success and serializes via the deploy concurrency group, and removes the separate workflow entirely.
2026-06-11 19:41:31 -04:00
ReleaseNotifierLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub "/aws/lambda/${ReleaseNotifierFunction}"
RetentionInDays: 60
Outputs:
SlackBotApiUrl:
Description: URL for Slack app Request URL configuration
Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/slack/events"
ShiftTableName:
Value: !Ref ShiftTable
SlackBotFunctionArn:
Value: !GetAtt SlackBotFunction.Arn
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer Move from flat src/ to per-function directories: - src/slack-bot/ — Slack Bolt Lambda handler - src/weekly-post/ — Monday schedule + pay post - src/roster-sync/ — Daily 3CX roster sync - src/shared/ — Lambda Layer with schedule, blocks, three_cx_client Each function has its own requirements.txt and CodeUri. Shared modules are deployed as a SAM Layer (afterhours-shared) importable as `from shared.X import Y`. * Migrate secrets from SSM Parameter Store to Secrets Manager - Slack bot token and signing secret now read from Secrets Manager - 3CX credentials (domain, client-id, client-secret) moved to Secrets Manager under afterhours-shift-manager/3cx-* prefix - Channel ID is now a non-secret CloudFormation parameter (ShiftChannel) - Add shared secrets.py helper for Secrets Manager reads - Remove SSM and KMS IAM policies, add secretsmanager:GetSecretValue * Merge ring-scheduler-3cx as 4th Lambda function - Add afterhours-ring-scheduler Lambda with 4 EventBridge rules (daily 8am EST/EDT + weekend 5pm EST/EDT) for 3CX ring group routing updates - Extract shared ring_scheduler.py module for direct ring group updates from both the scheduled Lambda and the Slack bot - Replace cross-Lambda invoke with direct update_ring_group() call in the Slack bot — eliminates lambda:InvokeFunction dependency - Use RingGroup API (correct) instead of Queue API (was wrong in the original ring-scheduler repo) - Eliminate YAML config fallback — DynamoDB is the sole schedule source - Add RingGroupNumber CloudFormation parameter * Add schedule post live-update and old post deletion (#40, #41) - Store schedule message timestamp in DynamoDB (SCHEDULE_POST record) - Delete previous week's schedule post before posting the new one - Live-update the schedule post via chat_update after any pick/drop/swap/button-pickup so it always reflects current state * Disallow past shifts and add day/night labels (#43, #42) - Reject /oncall pick and /oncall drop for past dates - Show ephemeral error when stale pickup buttons are clicked - Hide pickup buttons for dates in the past - Add explicit "Day (8am-5pm)" and "Night (5pm-8am)" labels to schedule lines, pickup buttons, and shift change notifications * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 The 3CX routing was changed from ring group 800 to queue 801 in a previous PR on ring-scheduler-3cx. Updates all callers and the SAM template parameter default accordingly. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - Ring scheduler: use DynamoDBCrudPolicy (resolve_shift needs Query) - Button pickup: update 3CX for active shift type, not just night - Pick/drop/swap commands: only update 3CX when shift type is active - Swap command: add missing past-date guard - add_roster_entry: reject if extension already exists - Apply ruff formatting * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - Add _find_employee_shift() to check both day/night on weekends - Drop/swap now correctly find and operate on weekend day shifts - Pick finds first available shift type on weekends - Admin override/open/clear update 3CX for same-day active shifts * Fix dependabot directories and admin weekend shift handling Dependabot now scans per-function requirement directories instead of the repo root. Admin override/open/clear commands accept an optional day/night parameter for weekend day shift management. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications Night shift notifications on weekends were missing the type label, making them ambiguous. Also fix schedule post text fallback to use this_monday instead of now for the start date. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
WeeklyPostFunctionArn:
Value: !GetAtt WeeklyPostFunction.Arn
RosterSyncFunctionArn:
Value: !GetAtt RosterSyncFunction.Arn
Merge ring-scheduler-3cx and resolve all open issues (#62) * Add arm64, log retention, and compliance fixes - Set arm64 architecture globally for all Lambda functions - Add explicit CloudWatch log groups with 60-day retention - Add missing WeeklyPostFunctionArn to stack outputs - Add Dependabot assignees for both ecosystems - Add samconfig.toml.example for onboarding * Restructure src/ to per-function layout with shared Layer Move from flat src/ to per-function directories: - src/slack-bot/ — Slack Bolt Lambda handler - src/weekly-post/ — Monday schedule + pay post - src/roster-sync/ — Daily 3CX roster sync - src/shared/ — Lambda Layer with schedule, blocks, three_cx_client Each function has its own requirements.txt and CodeUri. Shared modules are deployed as a SAM Layer (afterhours-shared) importable as `from shared.X import Y`. * Migrate secrets from SSM Parameter Store to Secrets Manager - Slack bot token and signing secret now read from Secrets Manager - 3CX credentials (domain, client-id, client-secret) moved to Secrets Manager under afterhours-shift-manager/3cx-* prefix - Channel ID is now a non-secret CloudFormation parameter (ShiftChannel) - Add shared secrets.py helper for Secrets Manager reads - Remove SSM and KMS IAM policies, add secretsmanager:GetSecretValue * Merge ring-scheduler-3cx as 4th Lambda function - Add afterhours-ring-scheduler Lambda with 4 EventBridge rules (daily 8am EST/EDT + weekend 5pm EST/EDT) for 3CX ring group routing updates - Extract shared ring_scheduler.py module for direct ring group updates from both the scheduled Lambda and the Slack bot - Replace cross-Lambda invoke with direct update_ring_group() call in the Slack bot — eliminates lambda:InvokeFunction dependency - Use RingGroup API (correct) instead of Queue API (was wrong in the original ring-scheduler repo) - Eliminate YAML config fallback — DynamoDB is the sole schedule source - Add RingGroupNumber CloudFormation parameter * Add schedule post live-update and old post deletion (#40, #41) - Store schedule message timestamp in DynamoDB (SCHEDULE_POST record) - Delete previous week's schedule post before posting the new one - Live-update the schedule post via chat_update after any pick/drop/swap/button-pickup so it always reflects current state * Disallow past shifts and add day/night labels (#43, #42) - Reject /oncall pick and /oncall drop for past dates - Show ephemeral error when stale pickup buttons are clicked - Hide pickup buttons for dates in the past - Add explicit "Day (8am-5pm)" and "Night (5pm-8am)" labels to schedule lines, pickup buttons, and shift change notifications * Add admin slash commands for shift and roster management (#39) - /oncall admin override <date> <ext> — assign a shift - /oncall admin open <date> — mark shift as open - /oncall admin clear <date> — remove override, revert to weekly - /oncall admin roster add/remove/rename — manage roster entries - Admin access gated by admin_users list in DynamoDB CONFIG - Help message shows admin commands for admin users * Update README for merged architecture and new features * Switch from RingGroup API to Queue API at extension 801 The 3CX routing was changed from ring group 800 to queue 801 in a previous PR on ring-scheduler-3cx. Updates all callers and the SAM template parameter default accordingly. * Pass SAM parameter overrides in deploy workflow * Fix review findings: IAM, routing guards, past-date check, roster safety - Ring scheduler: use DynamoDBCrudPolicy (resolve_shift needs Query) - Button pickup: update 3CX for active shift type, not just night - Pick/drop/swap commands: only update 3CX when shift type is active - Swap command: add missing past-date guard - add_roster_entry: reject if extension already exists - Apply ruff formatting * Add error handling to ring scheduler 3CX call * Fix weekend day shift commands and admin 3CX routing - Add _find_employee_shift() to check both day/night on weekends - Drop/swap now correctly find and operate on weekend day shifts - Pick finds first available shift type on weekends - Admin override/open/clear update 3CX for same-day active shifts * Fix dependabot directories and admin weekend shift handling Dependabot now scans per-function requirement directories instead of the repo root. Admin override/open/clear commands accept an optional day/night parameter for weekend day shift management. * Fix weekend day shift active window to 8am-5pm Before midnight-8am on weekends incorrectly reported the day shift as active when the previous night shift is still running. * Show shift type label for both weekend shifts in notifications Night shift notifications on weekends were missing the type label, making them ambiguous. Also fix schedule post text fallback to use this_monday instead of now for the start date. * Extract determine_shift_type into shared layer Eliminates duplicated weekend day/night boundary logic between the ring scheduler and Slack bot Lambdas. * Fix weekly schedule fallback start date Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Include weekend shift type in command confirmations Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * Apply ruff formatting to app.py * Only show day/night shift labels on weekends in schedule display Weekday shifts are always night — the label was redundant clutter. * Deduplicate 3CX forwarding payload and add shift type to pick command Extract _update_forwarding helper in ThreeCXClient to share the payload between queue and ring group methods. Add optional day/night argument to /oncall pick so users can target a specific weekend shift. * Consolidate WEEKEND_DAYS and fix weekday pickup button labels Import WEEKEND_DAYS from shared.schedule instead of redefining in blocks.py and weekly-post/app.py. Gate pickup button day/night labels on weekends only, matching all other display surfaces. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-12 19:55:39 -04:00
RingSchedulerFunctionArn:
Value: !GetAtt RingSchedulerFunction.Arn
HolidayRouterFunctionArn:
Value: !GetAtt HolidayRouterFunction.Arn
HolidaySchedulerExecutionRoleArn:
Description: Role EventBridge Scheduler assumes to invoke the holiday router; the slack-bot passes this when creating per-holiday schedules
Value: !GetAtt HolidaySchedulerExecutionRole.Arn
Add changelog-driven releases and App Home tab (#112) * Add changelog-driven releases and App Home tab Version the bot continuously from CHANGELOG.md (the single source of truth for both the version and the staff-readable notes) and surface changes to users in two ways: - A new afterhours-release-notifier Lambda posts a "What's New" message to the shift channel on minor/major releases (patches stay silent). - The bot gains an App Home "About" tab showing what it does, the command list, and the current version's notes. release.yaml runs on Deploy success (not release:published — GITHUB_TOKEN events don't start downstream workflows), checks out the deployed commit, and tags + publishes a GitHub Release + invokes the notifier. It assumes a dedicated, boundary-carrying OIDC role scoped to InvokeFunction on the notifier; the account's cfn role gates role creation on that boundary. The manual Version Bump workflow is retired. A CI guard enforces that a CHANGELOG edit is a clean SemVer bump and that the in-package copy matches. * Harden release workflow and regex against CodeQL findings Address three code-scanning alerts on the PR: - Critical (actions/untrusted-checkout): split release.yaml into a read-only `prepare` job that checks out and runs repo code, and a privileged `publish` job (contents:write + OIDC) that never checks out repo code — it tags, releases, and invokes purely through the GitHub and AWS APIs. Also assert head_branch == main. - High x2 (py/polynomial-redos): rewrite the italic and link regexes in markdown_to_mrkdwn with possessive quantifiers and exclusive character classes so they run in linear time on adversarial input. Adds a regression test. * Move release/announce into Deploy workflow to clear CodeQL The workflow_run-triggered release.yaml kept tripping CodeQL's privileged-context rules (untrusted-checkout, then cache-poisoning) — CodeQL distrusts any workflow_run that checks out a ref, regardless of the main-only guarantee, and there is no autofix. Fold the release job into deploy.yaml gated on `needs: deploy`. A push-to-main run is a trusted context, so checking out and running repo code with write/OIDC is safe there. This still gates on deploy success and serializes via the deploy concurrency group, and removes the separate workflow entirely.
2026-06-11 19:41:31 -04:00
ReleaseNotifierFunctionArn:
Value: !GetAtt ReleaseNotifierFunction.Arn
ReleaseNotifyInvokeRoleArn:
Description: Set this as the RELEASE_NOTIFY_INVOKE_ROLE_ARN repo variable for release.yaml
Value: !GetAtt ReleaseNotifyInvokeRole.Arn