Skip to content

Commit fe38a28

Browse files
committed
feat(gov-000): harden feature flag governance
1 parent 43251b0 commit fe38a28

File tree

11 files changed

+671
-6
lines changed

11 files changed

+671
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3434
### Fixed
3535

3636
- Added the missing `rbac_hardening_v1` enum value to feature flag migrations to keep Supabase schema in sync with governance defaults.
37+
- Locked the admin feature flag API (including PURGE) behind RBAC checks, emitting denial telemetry and documenting reversible down migrations.
3738

3839
### Planned - Library Feature
3940
- **User Library System**: Complete Medium-style library feature for saving and organizing content

docs/06-data-model-delta.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
- **Webhooks:** Store delivery payloads for 30 days for debugging, then purge.
6767

6868
## 4. Migration Approach
69-
1. **Feature-flag gated migrations:** Introduce new tables with `enabled` flags default false; ensure down migrations exist.
69+
1. **Feature-flag gated migrations:** Introduce new tables with `enabled` flags default false; ensure down migrations exist. `0017_create_feature_flags.down.sql` and `0018_expand_role_matrix.down.sql` provide rollback paths for GOV-000 and SEC-001 respectively.
7070
2. **Backfill Strategy:** Use Supabase functions or background workers to populate new tables (e.g., `reputation_aggregates`) with resume tokens.
7171
3. **Incremental rollout:** Deploy schema changes in small batches (spaces, then content, then commerce) to minimize lock times.
7272
4. **Testing:** Integration tests validating RLS and referential integrity must run in CI before enabling flags.

docs/08-observability.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
| `event_rsvp_to_attendance_rate` | Attendance / RSVPs | Gauge | `event_type`, `space` |
1818
| `moderation_queue_oldest_min` | Age of oldest open report | Gauge | `queue_type`, `space` |
1919
| `crash_free_sessions` | % of sessions without fatal error | Gauge | `platform` |
20-
| `authz_denied_count` | Authorization failures | Counter | `resource`, `role`, `space` |
20+
| `authz_denied_count` | Authorization failures | Counter | `context`, `resource`, `role`, `space`, `reason` |
2121
| `flag_evaluation_latency_ms` | Feature flag evaluation | Histogram | `flag_key` |
2222
| `webhook_delivery_success_rate` | Webhook successes vs. attempts | Gauge | `event_type` |
2323
| `automod_trigger_count` | Automod actions per rule | Counter | `rule_type`, `space` |

docs/09-test-strategy.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
- Tests must run with flags ON and OFF to ensure fallback behavior.
3535
- Provide helper to set flag context in tests (`withFeatureFlag('spaces_v1', true)`).
3636
- CI includes matrix builds for critical flags (Spaces, Commerce, Events).
37-
- Added Vitest coverage for the feature flag SDK (caching, invalidation, telemetry) as part of GOV-000; extend coverage to admin API routes in upcoming iterations.
37+
- Added Vitest coverage for the feature flag SDK (caching, invalidation, telemetry) as part of GOV-000; admin route guard tests (`tests/unit/feature-flags-admin-route.test.ts`) now verify unauthorized flows and cache purge protections.
3838
- SEC-001 adds Vitest coverage for role slug normalization and observability counters, plus a Supabase RBAC harness (`tests/security/rbac-policies.test.ts`) that runs when credentials are provided.
3939

4040
## 7. Performance & Load

docs/10-release-plan.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545

4646
## 6. Rollback Procedures
4747
- **Feature-level:** Toggle flag OFF, clear caches, notify stakeholders.
48-
- **Database-level:** Execute down migration scripts; for irreversible data changes, restore from Supabase point-in-time recovery.
48+
- **Database-level:** Execute down migration scripts (`0017_create_feature_flags.down.sql`, `0018_expand_role_matrix.down.sql`); for irreversible data changes, restore from Supabase point-in-time recovery.
4949
- **Payments:** Pause webhook processing via provider dashboard, ensure escrow funds safe.
5050
- **Events:** Notify attendees of postponement if event module impacted.
5151

docs/assumptions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@
1212
| A-008 | 2025-02-14 | Legal/compliance resources available before Phase 3 commerce rollout. | Necessary for payments, KYC, events. | Open |
1313
| A-009 | 2025-10-10 | Feature flag ownership stored as free-text owner label until RBAC revamp in SEC-001. | Allows governance UI to launch without expanded role matrix; revisit once role hierarchy finalized. | Closed (2025-10-17) |
1414
| A-010 | 2025-02-18 | Despite cutover directive, execution continues ticket-by-ticket under feature flags to avoid destabilizing one-shot release until all prerequisites validated. | Aligns with risk register and roadmap gating; supports rehearsed cutover later without skipping validation. | Open |
15+
| A-011 | 2025-02-19 | Supabase migration runner respects paired `.down.sql` files for rollback in staging/production. | Verified locally; staging rehearsal scheduled before enabling GOV-000 for pilot use. | Open |

docs/progress/weekly-2025-10-17.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Weekly Progress — 2025-10-17
22

33
## Highlights
4+
- Hardened GOV-000 governance tooling with admin guard telemetry, cache purge protection, and reversible migrations for feature flags/RBAC.
45
- Delivered SEC-001 RBAC hardening: refreshed Supabase `roles`/`profile_roles`, migrated legacy slugs, and rewrote taxonomy/community policies for the new member → admin ladder.
56
- Gated admin role management behind the new `rbac_hardening_v1` flag, surfaced highest-role badges in the console sidebar, and mapped legacy editor/author selections.
67
- Instrumented `authz_denied_count` counter plus console telemetry for admin/community APIs; added Vitest coverage for role slug normalization and metrics emission.
@@ -21,6 +22,7 @@
2122

2223
## Metrics Snapshot
2324
- `authz_denied_count`: 0 during admin API smoke tests post-migration (baseline logging now available).
25+
- Added `context`/`reason` tags to `authz_denied_count` to differentiate governance vs. RBAC denials in dashboards.
2426
- `flag_evaluation_latency_ms` remains stable at ~4ms p95 (no regression after RBAC changes share the metrics adapter).
2527

2628
## Risks / Follow-ups

src/app/api/admin/feature-flags/route.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
invalidateFeatureFlagCache,
1212
upsertFeatureFlagCache,
1313
} from '@/lib/feature-flags/server'
14+
import { recordAuthzDeny } from '@/lib/observability/metrics'
1415
import { createServerClient, createServiceRoleClient } from '@/lib/supabase/server-client'
1516
import type { Database } from '@/lib/supabase/types'
1617
import type { AdminFeatureFlagAuditEntry, AdminFeatureFlagRecord } from '@/utils/types'
@@ -72,7 +73,7 @@ const mapAuditRow = (row: Database['public']['Tables']['feature_flag_audit']['Ro
7273
createdAt: row.created_at,
7374
})
7475

75-
const requireAdminProfile = async (): Promise<{ profile: ProfileRecord } | { response: NextResponse }> => {
76+
export const requireAdminProfile = async (): Promise<{ profile: ProfileRecord } | { response: NextResponse }> => {
7677
const supabase = createServerClient()
7778
const {
7879
data: { user },
@@ -86,6 +87,7 @@ const requireAdminProfile = async (): Promise<{ profile: ProfileRecord } | { res
8687
}
8788

8889
if (!user) {
90+
recordAuthzDeny('feature_flag_admin', { reason: 'no_session' })
8991
return { response: NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) }
9092
}
9193

@@ -104,7 +106,16 @@ const requireAdminProfile = async (): Promise<{ profile: ProfileRecord } | { res
104106
}
105107
}
106108

107-
if (!profile?.is_admin) {
109+
if (!profile) {
110+
recordAuthzDeny('feature_flag_admin', { reason: 'missing_profile', user_id: user.id })
111+
return { response: NextResponse.json({ error: 'Forbidden: admin access required.' }, { status: 403 }) }
112+
}
113+
114+
if (!profile.is_admin) {
115+
recordAuthzDeny('feature_flag_admin', {
116+
reason: 'forbidden',
117+
profile_id: profile.id,
118+
})
108119
return { response: NextResponse.json({ error: 'Forbidden: admin access required.' }, { status: 403 }) }
109120
}
110121

@@ -385,6 +396,12 @@ export async function PATCH(request: Request) {
385396
}
386397

387398
export async function PURGE() {
399+
const result = await requireAdminProfile()
400+
401+
if ('response' in result) {
402+
return result.response
403+
}
404+
388405
invalidateFeatureFlagCache()
389406
return NextResponse.json({ message: 'Feature flag cache cleared.' })
390407
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
-- =================================================================
2+
-- DOWN MIGRATION: FEATURE FLAG GOVERNANCE
3+
-- =================================================================
4+
-- Drops feature flag tables, policies, and enum to revert GOV-000.
5+
-- Safe to run after ensuring no other tables depend on feature_flag_key.
6+
7+
DROP TABLE IF EXISTS public.feature_flag_audit CASCADE;
8+
DROP TABLE IF EXISTS public.feature_flags CASCADE;
9+
10+
DO $$
11+
BEGIN
12+
IF EXISTS (
13+
SELECT 1
14+
FROM pg_type t
15+
WHERE t.typname = 'feature_flag_key'
16+
AND t.typnamespace = 'public'::regnamespace
17+
) THEN
18+
DROP TYPE public.feature_flag_key;
19+
END IF;
20+
END;
21+
$$;

0 commit comments

Comments
 (0)