Skip to content

feat: add Slack /bacon slash command handler#23

Open
aryanghai12 wants to merge 4 commits intoOWASP-BLT:mainfrom
aryanghai12:feat/slack-bacon-command
Open

feat: add Slack /bacon slash command handler#23
aryanghai12 wants to merge 4 commits intoOWASP-BLT:mainfrom
aryanghai12:feat/slack-bacon-command

Conversation

@aryanghai12
Copy link
Copy Markdown
Contributor

@aryanghai12 aryanghai12 commented Feb 20, 2026

Closes #19

Implements the /bacon @user <amount> Slack slash command requested in issue #19


Changes

src/slack_handler.py (new)

Core handler for the slash command:

  • Signature verification – HMAC-SHA256 check against SLACK_SIGNING_SECRET with a ±5 minute replay-attack window
  • Input parsing – accepts both plain @username 100 and Slack rich-mention <@U12345|username> 100 formats (regex-backed, handles decimals)
  • Form decoding – uses urllib.parse.unquote_plus; no Cloudflare-JS–only deps in the parseable path
  • KV persistence (optional) – records every transfer to a BACON_LEDGER KV namespace when bound in wrangler.toml
  • Slack response – posts an in_channel message so the whole channel sees the transfer

src/index.py

  • Imports handle_bacon_command and routes POST /api/slack/bacon to it

wrangler.toml

  • Documents that SLACK_SIGNING_SECRET and SLACK_BOT_TOKEN must be added via wrangler secret put

tests/test_slack_handler.py (new)

22 unit tests covering:

  • _parse_text: plain mentions, rich mentions, decimals, dots/hyphens in usernames, zero/large amounts, missing @, non-numeric amounts, empty input
  • _verify_slack_signature: valid signature, wrong secret, tampered body, old/future timestamp, non-numeric timestamp, malformed signature

Slack App Setup

  1. Go to api.slack.com/apps → your app → Slash Commands
  2. Create command /bacon, set Request URL to: https://blt-rewards..workers.dev/api/slack/bacon
  3. Add the signing secret as a Worker secret:
wrangler secret put SLACK_SIGNING_SECRET
wrangler secret put SLACK_BOT_TOKEN

Test Results

WhatsApp Image 2026-02-20 at 7 26 17 PM

Summary by CodeRabbit

New Features

  • Introduced Slack /bacon command enabling users to transfer rewards to other team members
  • Added admin approval system to manage and authorize users
  • Implemented admin controls for removing users and viewing approved user lists
  • Secured Slack command integration with request signature verification

- Add src/slack_handler.py with handle_bacon_command() for POST /api/slack/bacon
- Validate requests via HMAC-SHA256 Slack signature verification (replay-attack safe)
- Parse both plain @username and rich <@uid|display> Slack mention formats
- URL-decode form body with urllib.parse.unquote_plus (no CF-only JS dep)
- Optional BACON_LEDGER KV persistence when namespace is bound
- Route POST /api/slack/bacon in src/index.py
- Document SLACK_SIGNING_SECRET / SLACK_BOT_TOKEN in wrangler.toml
- Add 22-test suite in tests/test_slack_handler.py (all passing)
@aryanghai12 aryanghai12 changed the title feat: add Slack /bacon slash command handler (closes #19) feat: add Slack /bacon slash command handler Feb 20, 2026
Copy link
Copy Markdown

@e-esakman e-esakman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have u tested it on ur slack workspace? please show working demo of it.

@github-project-automation github-project-automation bot moved this from Backlog to Ready in 📌 OWASP BLT Project Board Feb 20, 2026
Copy link
Copy Markdown

@Nachiket-Roy Nachiket-Roy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also prefer using D1, and we should add a restriction so that only admins (or an approved role) can use this slash command rather than all workspace users.

…kPassing a plain Python dict as Response headers goes through pyodide as aPyProxy, which the JS Response constructor silently ignores, causing Slackto receive text/plain and render the raw JSON string in the channel.Fix: use to_js(..., dict_converter=Object.fromEntries) to produce a realJS plain object that the Response constructor correctly reads as headers.Also sets ADMIN_USER_IDS in wrangler.toml for admin bootstrap.
@aryanghai12
Copy link
Copy Markdown
Contributor Author

@DonnieBLT i tested it on my workspace and here is the working demo of the same feature.

2026-03-05.14-32-08.mp4

@owasp-blt owasp-blt bot added the needs-peer-review PR needs peer review label Mar 11, 2026
@owasp-blt
Copy link
Copy Markdown

owasp-blt bot commented Mar 11, 2026

👋 Hi @aryanghai12!

This pull request needs a peer review before it can be merged. Please request a review from a team member who is not:

  • The PR author
  • coderabbitai
  • copilot

Once a valid peer review is submitted, this check will pass automatically. Thank you!

⚠️ Peer review enforcement is active.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 11, 2026

Walkthrough

This PR implements a complete Slack slash-command handler for /bacon that enables users to transfer rewards and provides admin functionality for managing approved users. It includes database schema setup, Slack request verification, authorization logic, command parsing, and comprehensive tests.

Changes

Cohort / File(s) Summary
Database Schema & Migrations
migrations/0001_init.sql, migrations/0002_add_is_admin.sql
Establishes SQLite schema with transfers, approved_users, and config tables; adds indexes on transfers for performance; extends approved_users with admin flag in follow-up migration.
Slack Handler Implementation
src/slack_handler.py
Implements handle_bacon_command with request signature verification, form parsing, authorization (bootstrap and DB-driven), and sub-command routing (approve, remove, list, transfer); persists transfers and user management to D1 database with error handling.
Route Integration
src/index.py
Adds POST /api/slack/bacon endpoint for Slack integration; preserves existing routing and CORS behavior.
Configuration
wrangler.toml
Configures D1 database binding, admin user list, static asset serving, and observability settings.
Test Coverage
tests/test_slack_handler.py
Comprehensive unit tests for text parsing (plain mentions, rich Slack mentions, edge cases) and signature verification (valid/invalid scenarios).

Sequence Diagram(s)

sequenceDiagram
    participant Slack as Slack (Client)
    participant Handler as Handler
    participant Auth as Authorization
    participant Parser as Parser
    participant DB as D1 Database
    participant Response as Slack Response

    Slack->>Handler: POST /api/slack/bacon<br/>(signed request)
    Handler->>Handler: Verify Slack signature
    Handler->>Parser: Parse form data<br/>(text, user, channel)
    Parser->>Handler: Parsed command & args
    Handler->>Auth: Check admin status<br/>(bootstrap + DB)
    Auth->>Handler: Authorization result
    
    alt Admin Command (approve/remove/list)
        Handler->>DB: Execute command<br/>(add/remove/list users)
        DB->>Handler: Operation result
    else Transfer Command
        Handler->>Parser: Extract `@user` & amount
        Parser->>Handler: User ID & amount
        Handler->>DB: Insert transfer record
        DB->>Handler: Confirmation
    end
    
    Handler->>Response: Build Slack message<br/>(ephemeral/in-channel)
    Response->>Slack: Return formatted response
    Slack->>Slack: Display message to user
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

quality: high

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: adding a Slack /bacon slash command handler, which is the primary focus of the PR.
Linked Issues check ✅ Passed The PR fully implements issue #19 requirements: a Slack /bacon slash command that accepts @user and amount parameters, with proper verification, parsing, and database persistence.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the Slack /bacon command. No unrelated refactoring, dependency upgrades, or extraneous modifications are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/slack_handler.py`:
- Around line 184-187: The current db.prepare(...) call unconditionally sets
is_admin to 0 which strips D1-backed admin status on re-approve; change the
upsert to preserve an existing is_admin by either (a) querying approved_users
for the user's is_admin and passing that value into the INSERT, or (b) use an
UPSERT that on conflict updates user_name, added_by, added_at but leaves
is_admin unchanged (e.g. ON CONFLICT(user_id) DO UPDATE SET
user_name=excluded.user_name, added_by=excluded.added_by,
added_at=excluded.added_at). Update the db.prepare(...) call that inserts into
approved_users accordingly so re-approving does not overwrite is_admin.
- Around line 299-307: The handler currently skips signature verification when
signing_secret (from SLACK_SIGNING_SECRET) is unset, allowing forged requests;
update the request handling so if signing_secret is None/falsey the endpoint
fails closed instead of bypassing verification — e.g., call _slack_response with
a suitable error message and return make_response(json.dumps(payload), 500 or
401) immediately when signing_secret is missing (before using
_verify_slack_signature), and ensure the same response shape as signature
failures so callers cannot impersonate admins.

In `@tests/test_slack_handler.py`:
- Line 20: The test imports a non-existent symbol _parse_text from
slack_handler; update the import in tests/test_slack_handler.py to import the
actual helper _parse_transfer (or import _parse_transfer as _parse_text if you
prefer to keep the test name) and keep the existing _verify_slack_signature
import so tests can run; update the import line to reference _parse_transfer (or
alias it) to match the function defined in slack_handler.py.

In `@wrangler.toml`:
- Around line 17-24: The documented DB setup only runs migrations/0001_init.sql
but the code expects an approved_users.is_admin column (accessed in
src/slack_handler.py around the approval/read paths), so update the deployment
instructions to also run the second migration that adds the is_admin column
(e.g. migrations/0002_add_is_admin.sql) or explicitly note the ALTER TABLE step;
ensure the README/wrangler.toml comments instruct operators to run wrangler d1
execute ... --file=migrations/0002_add_is_admin.sql (or include the SQL to add
approved_users.is_admin) so the column exists before slack_handler.py performs
reads (line ~89) and inserts (line ~185).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: OWASP-BLT/coderabbit/.coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 76f7af9a-b1f4-468c-8452-da935472027a

📥 Commits

Reviewing files that changed from the base of the PR and between 715c91e and 2da16af.

📒 Files selected for processing (6)
  • migrations/0001_init.sql
  • migrations/0002_add_is_admin.sql
  • src/index.py
  • src/slack_handler.py
  • tests/test_slack_handler.py
  • wrangler.toml

Comment on lines +184 to +187
await db.prepare(
"INSERT OR REPLACE INTO approved_users (user_id, user_name, added_by, added_at, is_admin) "
"VALUES (?, ?, ?, ?, 0)"
).bind(uid, display, sender_id, int(time.time())).run()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Preserve existing admin state on re-approve.

Line 186 hard-codes is_admin to 0 on the same path used for existing rows. Re-approving someone who already has a D1-backed admin role silently strips that role.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/slack_handler.py` around lines 184 - 187, The current db.prepare(...)
call unconditionally sets is_admin to 0 which strips D1-backed admin status on
re-approve; change the upsert to preserve an existing is_admin by either (a)
querying approved_users for the user's is_admin and passing that value into the
INSERT, or (b) use an UPSERT that on conflict updates user_name, added_by,
added_at but leaves is_admin unchanged (e.g. ON CONFLICT(user_id) DO UPDATE SET
user_name=excluded.user_name, added_by=excluded.added_by,
added_at=excluded.added_at). Update the db.prepare(...) call that inserts into
approved_users accordingly so re-approving does not overwrite is_admin.

Comment on lines +299 to +307
signing_secret = getattr(env, "SLACK_SIGNING_SECRET", None)
if signing_secret:
# JS Headers.get() returns null (Python None) for missing headers,
# not "", so we must coerce explicitly.
timestamp = request.headers.get("X-Slack-Request-Timestamp") or ""
signature = request.headers.get("X-Slack-Signature") or ""
if not _verify_slack_signature(signing_secret, body_text, timestamp, signature):
payload = _slack_response(":x: Request signature verification failed.")
return make_response(json.dumps(payload), 401)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fail closed when the signing secret is missing.

When SLACK_SIGNING_SECRET is unset, this branch skips verification entirely and the rest of the handler trusts the posted user_id. That makes the public endpoint forgeable: a caller can impersonate an admin and invoke approve, remove, or transfer commands.

🔒 Minimal fix
-    signing_secret = getattr(env, "SLACK_SIGNING_SECRET", None)
-    if signing_secret:
-        # JS Headers.get() returns null (Python None) for missing headers,
-        # not "", so we must coerce explicitly.
-        timestamp = request.headers.get("X-Slack-Request-Timestamp") or ""
-        signature = request.headers.get("X-Slack-Signature") or ""
-        if not _verify_slack_signature(signing_secret, body_text, timestamp, signature):
-            payload = _slack_response(":x: Request signature verification failed.")
-            return make_response(json.dumps(payload), 401)
+    signing_secret = (getattr(env, "SLACK_SIGNING_SECRET", None) or "").strip()
+    if not signing_secret:
+        payload = _slack_response(":x: Slack signing secret is not configured.")
+        return make_response(json.dumps(payload), 500)
+
+    # JS Headers.get() returns null (Python None) for missing headers,
+    # not "", so we must coerce explicitly.
+    timestamp = request.headers.get("X-Slack-Request-Timestamp") or ""
+    signature = request.headers.get("X-Slack-Signature") or ""
+    if not _verify_slack_signature(signing_secret, body_text, timestamp, signature):
+        payload = _slack_response(":x: Request signature verification failed.")
+        return make_response(json.dumps(payload), 401)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/slack_handler.py` around lines 299 - 307, The handler currently skips
signature verification when signing_secret (from SLACK_SIGNING_SECRET) is unset,
allowing forged requests; update the request handling so if signing_secret is
None/falsey the endpoint fails closed instead of bypassing verification — e.g.,
call _slack_response with a suitable error message and return
make_response(json.dumps(payload), 500 or 401) immediately when signing_secret
is missing (before using _verify_slack_signature), and ensure the same response
shape as signature failures so callers cannot impersonate admins.

# Make src/ importable without installing the package
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))

from slack_handler import _parse_text, _verify_slack_signature # noqa: E402
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Import the parser name that actually exists.

src/slack_handler.py defines _parse_transfer, not _parse_text, so this module will fail before any tests execute. The smallest fix is to alias the real helper here.

🧪 Minimal fix
-from slack_handler import _parse_text, _verify_slack_signature  # noqa: E402
+from slack_handler import _parse_transfer as _parse_text, _verify_slack_signature  # noqa: E402
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from slack_handler import _parse_text, _verify_slack_signature # noqa: E402
from slack_handler import _parse_transfer as _parse_text, _verify_slack_signature # noqa: E402
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_slack_handler.py` at line 20, The test imports a non-existent
symbol _parse_text from slack_handler; update the import in
tests/test_slack_handler.py to import the actual helper _parse_transfer (or
import _parse_transfer as _parse_text if you prefer to keep the test name) and
keep the existing _verify_slack_signature import so tests can run; update the
import line to reference _parse_transfer (or alias it) to match the function
defined in slack_handler.py.

Comment on lines +17 to +24
# ---------------------------------------------------------------------------
# D1 Database – stores BACON transfers and the approved-user allowlist.
# Steps:
# 1. wrangler d1 create blt-bacon-db (run once – note the id)
# 2. Replace "YOUR_DATABASE_ID" below with the printed database_id.
# 3. wrangler d1 execute blt-bacon-db --file=migrations/0001_init.sql
# 4. wrangler deploy
# ---------------------------------------------------------------------------
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Run the second migration in the documented setup.

These steps only apply migrations/0001_init.sql, but src/slack_handler.py reads approved_users.is_admin on Line 89 and inserts it on Line 185. A fresh deployment following this block will ship without that column, so DB-backed authorization and /bacon approve break immediately.

📝 Suggested update
 #   1. wrangler d1 create blt-bacon-db          (run once – note the id)
 #   2. Replace "YOUR_DATABASE_ID" below with the printed database_id.
 #   3. wrangler d1 execute blt-bacon-db --file=migrations/0001_init.sql
-#   4. wrangler deploy
+#   4. wrangler d1 execute blt-bacon-db --file=migrations/0002_add_is_admin.sql
+#   5. wrangler deploy
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wrangler.toml` around lines 17 - 24, The documented DB setup only runs
migrations/0001_init.sql but the code expects an approved_users.is_admin column
(accessed in src/slack_handler.py around the approval/read paths), so update the
deployment instructions to also run the second migration that adds the is_admin
column (e.g. migrations/0002_add_is_admin.sql) or explicitly note the ALTER
TABLE step; ensure the README/wrangler.toml comments instruct operators to run
wrangler d1 execute ... --file=migrations/0002_add_is_admin.sql (or include the
SQL to add approved_users.is_admin) so the column exists before slack_handler.py
performs reads (line ~89) and inserts (line ~185).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Ready

Development

Successfully merging this pull request may close these issues.

have a slack bot command that can /bacon @user X amount

3 participants