feat(slack_app): cache is_admin and is_owner on SlackUserProfileCache#60107
Conversation
Generated-By: PostHog Code Task-Id: a2c69d62-2aff-4acf-bc2c-96680cac24b0
| "email": profile.get("email") or None, | ||
| "display_name": profile.get("display_name") or "", | ||
| "real_name": profile.get("real_name") or "", | ||
| "is_admin": bool(user.get("is_admin")), |
There was a problem hiding this comment.
Indefinitely stale Slack admin-flag cache enables persistent privilege escalation
The DB cache (SlackUserProfileCache) has no TTL or staleness mechanism. _get_slack_user_info resolves in order: Redis (TTL=600 s) → DB → Slack API. Once a row exists in SlackUserProfileCache, the live Slack API is never called again for that user, so is_admin/is_owner stored in the DB can never be refreshed. If a Slack admin has their privileges revoked, their cached is_admin=True persists in the DB indefinitely. The PR description explicitly states these flags will gate "upcoming Slack bot commands" — any future authorization check that reads from this cache will honour the stale elevation permanently, allowing a de-admined user to continue exercising admin-only actions.
Prompt To Fix With AI
The `_get_slack_user_info` function returns DB-cached data without any freshness check, meaning `is_admin`/`is_owner` flags can never be updated for an existing user. Two complementary fixes are needed:
1. **Add a staleness TTL to the DB cache layer.** In `_get_slack_user_info_from_db`, reject DB records whose `updated_at` is older than `SLACK_USER_INFO_CACHE_TTL_SECONDS` (600 s). This ensures the live Slack API is consulted after both caches expire, and the DB record is refreshed. Example:
```python
from django.utils import timezone
import datetime
def _get_slack_user_info_from_db(integration, slack_user_id):
...
if not profile:
return None
max_age = datetime.timedelta(seconds=SLACK_USER_INFO_CACHE_TTL_SECONDS)
if timezone.now() - profile.updated_at > max_age:
return None # treat as cache miss; caller will call Slack API and refresh
return _format_slack_user_info_payload(...)
```
2. **At every authorization gate that reads `is_admin`/`is_owner`**, force a re-fetch from the Slack API (bypassing the cache entirely), or verify the DB `updated_at` is within the acceptable window. Authorization decisions must not rely on data with no expiry path. A dedicated `_get_fresh_slack_user_flags(integration, slack_user_id)` helper that always calls `users.info` and updates the DB would make the security boundary explicit.Severity: medium | Confidence: 82%
|
Migration SQL ChangesHey 👋, we've detected some migrations on this PR. Here's the SQL output for each migration, make sure they make sense:
|
🔍 Migration Risk AnalysisWe've analyzed your migrations for potential risks. Summary: 1 Safe | 0 Needs Review | 0 Blocked ✅ SafeBrief or no lock, backwards compatible Last updated: 2026-05-26 15:41 UTC (9a5b9a2) |
joshsny
left a comment
There was a problem hiding this comment.
how frequently do we sync this, just want to check what happens when I move from owner / admin role to member, do we invalidate?
|
@joshsny 10mins |
Problem
SlackUserProfileCachealready stores email, display name, and real name fromusers.info, but not the role flags. Upcoming Slack bot commands will need to gate behavior on whether the Slack caller is a workspace admin or owner, and we don't want to hitusers.infoon every interaction just to check that.Changes
is_adminandis_ownerboolean columns toSlackUserProfileCache(bothdefault=False,db_default=False).users.inforesponse (top-level on theuserobject per the Slack spec, not nested underprofile)._format_slack_user_info_payloadso the in-memory cache shape mirrors Slack's response.Note: in the Slack API,
is_adminandis_ownerare reported independently — an Owner is not guaranteed to haveis_admin: true. Callers that want "admin or above" should check both (and eventuallyis_primary_ownerif we add it).How did you test this code?
hogli test products/slack_app/backend/tests/test_resolve_slack_user.py— 8/8 passing../manage.py sqlmigrate slack_app 0002shows a cleanADD COLUMN ... DEFAULT false NOT NULLfor both columns with no trailingDROP DEFAULT../manage.py makemigrations --check --dry-runreports no drift.Publish to changelog?
no
🤖 Agent context
Authored by PostHog Code. The decision to store both
is_adminandis_owner(rather than justis_admin) came from confirming on Slack's API reference that the two flags are independent — relying onis_adminalone would miss workspace Owners. Manual local verification was performed by the human author.Created with PostHog Code