fix(kol): Phase 1 review — ACL lineage, expiry filters, tighter tenant scope

Addresses findings from consolidated Phase 1 code review:

- Add composite unique indexes + composite FKs so subscription_prompts and
  packages cannot reference mismatched (tenant_id, package_id, prompt_id)
  tuples, enforcing ACL lineage at the schema level.
- Add status CHECK constraints on all KOL tables as defense-in-depth.
- ListActiveSubscribersForPackage: drop meaningless tenant_id IS NOT NULL,
  add end_at expiry filter so fan-out skips expired subscribers.
- ApproveKolSubscription: add status='pending' guard to prevent double-approval.
- ListSubscriptionPromptsByTenant: join subscription/prompt/package/profile
  lifecycle so hidden/archived content no longer leaks; surface
  kol_display_name for UI.
- ListPublishedKolPackages: exclude expired subscribers from marketplace count.
- KolDashboardTrend: rewrite as CTE with day-series, returning both daily
  usage and new subscription counts.
- Remove cosmetic tenant_id IS NOT NULL from dashboard overview queries
  (the column is NOT NULL by schema).
- check_tenant_scope.sh: require tenant_id = sqlc.(n?)arg filter, reject
  tenant_id IS NOT NULL as a fake scope, maintain explicit exemption list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-17 08:34:12 +08:00
parent 3d4842e983
commit 70725193eb
12 changed files with 234 additions and 61 deletions
@@ -122,7 +122,8 @@ SELECT kp.id, kp.tenant_id, kp.kol_profile_id, kp.name, kp.description,
(SELECT COUNT(*) FROM kol_prompts p
WHERE p.package_id = kp.id AND p.status = 'active' AND p.deleted_at IS NULL) AS prompt_count,
(SELECT COUNT(*) FROM kol_subscriptions s
WHERE s.package_id = kp.id AND s.status = 'active') AS subscriber_count
WHERE s.package_id = kp.id AND s.status = 'active'
AND (s.end_at IS NULL OR s.end_at > NOW())) AS subscriber_count
FROM kol_packages kp
JOIN kol_profiles pf ON pf.id = kp.kol_profile_id
AND pf.status = 'active'
@@ -20,6 +20,7 @@ SET status = 'active',
updated_at = NOW()
WHERE id = $3
AND tenant_id = $4::bigint
AND status = 'pending'
`
type ApproveKolSubscriptionParams struct {
@@ -217,10 +218,12 @@ const listActiveSubscribersForPackage = `-- name: ListActiveSubscribersForPackag
SELECT id, tenant_id, package_id, status, approved_by, start_at, end_at, created_at, updated_at
FROM kol_subscriptions
WHERE package_id = $1
AND tenant_id IS NOT NULL
AND status = 'active'
AND (end_at IS NULL OR end_at > NOW())
`
// Cross-tenant read: used by platform admin for subscription fan-out.
// Excludes expired subscriptions so new prompts are not granted to them.
func (q *Queries) ListActiveSubscribersForPackage(ctx context.Context, packageID int64) ([]KolSubscription, error) {
rows, err := q.db.Query(ctx, listActiveSubscribersForPackage, packageID)
if err != nil {
@@ -292,10 +295,25 @@ const listSubscriptionPromptsByTenant = `-- name: ListSubscriptionPromptsByTenan
SELECT sp.id, sp.tenant_id, sp.subscription_id, sp.package_id, sp.prompt_id,
sp.status, sp.granted_at, sp.revoked_at,
p.name AS prompt_name, p.platform_hint, p.published_revision_no,
k.name AS package_name
k.name AS package_name,
pf.display_name AS kol_display_name
FROM kol_subscription_prompts sp
JOIN kol_prompts p ON p.id = sp.prompt_id AND p.deleted_at IS NULL
JOIN kol_packages k ON k.id = sp.package_id AND k.deleted_at IS NULL
JOIN kol_subscriptions s ON s.id = sp.subscription_id
AND s.tenant_id = sp.tenant_id
AND s.package_id = sp.package_id
AND s.status = 'active'
AND (s.end_at IS NULL OR s.end_at > NOW())
JOIN kol_prompts p ON p.id = sp.prompt_id
AND p.package_id = sp.package_id
AND p.status = 'active'
AND p.published_revision_no IS NOT NULL
AND p.deleted_at IS NULL
JOIN kol_packages k ON k.id = sp.package_id
AND k.status = 'published'
AND k.deleted_at IS NULL
JOIN kol_profiles pf ON pf.id = k.kol_profile_id
AND pf.status = 'active'
AND pf.deleted_at IS NULL
WHERE sp.tenant_id = $1::bigint
AND sp.status = 'active'
ORDER BY sp.granted_at DESC
@@ -314,6 +332,7 @@ type ListSubscriptionPromptsByTenantRow struct {
PlatformHint pgtype.Text `json:"platform_hint"`
PublishedRevisionNo pgtype.Int4 `json:"published_revision_no"`
PackageName string `json:"package_name"`
KolDisplayName string `json:"kol_display_name"`
}
func (q *Queries) ListSubscriptionPromptsByTenant(ctx context.Context, tenantID int64) ([]ListSubscriptionPromptsByTenantRow, error) {
@@ -338,6 +357,7 @@ func (q *Queries) ListSubscriptionPromptsByTenant(ctx context.Context, tenantID
&i.PlatformHint,
&i.PublishedRevisionNo,
&i.PackageName,
&i.KolDisplayName,
); err != nil {
return nil, err
}
@@ -115,15 +115,12 @@ SELECT
AND s.status='active'
AND s.created_at >= date_trunc('month', NOW())) AS month_subs,
(SELECT COUNT(*) FROM kol_usage_logs u
WHERE u.package_id = ANY($1::bigint[])
AND u.tenant_id IS NOT NULL) AS total_usage,
WHERE u.package_id = ANY($1::bigint[])) AS total_usage,
(SELECT COUNT(*) FROM kol_usage_logs u
WHERE u.package_id = ANY($1::bigint[])
AND u.tenant_id IS NOT NULL
AND u.created_at >= date_trunc('month', NOW())) AS month_usage,
(SELECT COUNT(*) FROM kol_usage_logs u
WHERE u.package_id = ANY($1::bigint[])
AND u.tenant_id IS NOT NULL
AND u.status='failed') AS failure_count
`
@@ -135,6 +132,7 @@ type KolDashboardOverviewRow struct {
FailureCount int64 `json:"failure_count"`
}
// Cross-tenant aggregation: KOL owns packages across subscriber tenants.
func (q *Queries) KolDashboardOverview(ctx context.Context, packageIds []int64) (KolDashboardOverviewRow, error) {
row := q.db.QueryRow(ctx, kolDashboardOverview, packageIds)
var i KolDashboardOverviewRow
@@ -149,28 +147,53 @@ func (q *Queries) KolDashboardOverview(ctx context.Context, packageIds []int64)
}
const kolDashboardTrend = `-- name: KolDashboardTrend :many
SELECT date_trunc('day', u.created_at)::date AS d,
COUNT(*) FILTER (WHERE u.status='completed') AS usages
FROM kol_usage_logs u
WHERE u.package_id = ANY($1::bigint[])
AND u.tenant_id IS NOT NULL
AND u.created_at >= NOW() - $2::int * INTERVAL '1 day'
GROUP BY d
ORDER BY d ASC
WITH days AS (
SELECT generate_series(
date_trunc('day', NOW()) - ($1::int - 1) * INTERVAL '1 day',
date_trunc('day', NOW()),
INTERVAL '1 day'
)::date AS d
),
u AS (
SELECT date_trunc('day', created_at)::date AS d,
COUNT(*) FILTER (WHERE status = 'completed') AS usages
FROM kol_usage_logs
WHERE package_id = ANY($2::bigint[])
AND created_at >= NOW() - $1::int * INTERVAL '1 day'
GROUP BY 1
),
s AS (
SELECT date_trunc('day', created_at)::date AS d,
COUNT(*) AS subscriptions
FROM kol_subscriptions
WHERE package_id = ANY($2::bigint[])
AND status IN ('active', 'expired', 'revoked')
AND created_at >= NOW() - $1::int * INTERVAL '1 day'
GROUP BY 1
)
SELECT days.d,
COALESCE(u.usages, 0)::bigint AS usages,
COALESCE(s.subscriptions, 0)::bigint AS subscriptions
FROM days
LEFT JOIN u ON u.d = days.d
LEFT JOIN s ON s.d = days.d
ORDER BY days.d ASC
`
type KolDashboardTrendParams struct {
PackageIds []int64 `json:"package_ids"`
PeriodDays int32 `json:"period_days"`
PackageIds []int64 `json:"package_ids"`
}
type KolDashboardTrendRow struct {
D pgtype.Date `json:"d"`
Usages int64 `json:"usages"`
D pgtype.Date `json:"d"`
Usages int64 `json:"usages"`
Subscriptions int64 `json:"subscriptions"`
}
// Returns a per-day series combining usage count and new subscription count for KOL dashboard.
func (q *Queries) KolDashboardTrend(ctx context.Context, arg KolDashboardTrendParams) ([]KolDashboardTrendRow, error) {
rows, err := q.db.Query(ctx, kolDashboardTrend, arg.PackageIds, arg.PeriodDays)
rows, err := q.db.Query(ctx, kolDashboardTrend, arg.PeriodDays, arg.PackageIds)
if err != nil {
return nil, err
}
@@ -178,7 +201,7 @@ func (q *Queries) KolDashboardTrend(ctx context.Context, arg KolDashboardTrendPa
var items []KolDashboardTrendRow
for rows.Next() {
var i KolDashboardTrendRow
if err := rows.Scan(&i.D, &i.Usages); err != nil {
if err := rows.Scan(&i.D, &i.Usages, &i.Subscriptions); err != nil {
return nil, err
}
items = append(items, i)
@@ -195,7 +218,6 @@ SELECT package_id, COUNT(*) AS total,
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed
FROM kol_usage_logs
WHERE package_id = ANY($1::bigint[])
AND tenant_id IS NOT NULL
GROUP BY package_id
`
@@ -206,6 +228,7 @@ type KolUsageCountByPackageRow struct {
Failed int64 `json:"failed"`
}
// Cross-tenant aggregation for KOL dashboard.
func (q *Queries) KolUsageCountByPackage(ctx context.Context, packageIds []int64) ([]KolUsageCountByPackageRow, error) {
rows, err := q.db.Query(ctx, kolUsageCountByPackage, packageIds)
if err != nil {
@@ -76,11 +76,16 @@ type Querier interface {
GetUserByID(ctx context.Context, id int64) (GetUserByIDRow, error)
InsertImageAsset(ctx context.Context, arg InsertImageAssetParams) (int64, error)
InsertQuotaLedger(ctx context.Context, arg InsertQuotaLedgerParams) (int64, error)
// Cross-tenant aggregation: KOL owns packages across subscriber tenants.
KolDashboardOverview(ctx context.Context, packageIds []int64) (KolDashboardOverviewRow, error)
// Returns a per-day series combining usage count and new subscription count for KOL dashboard.
KolDashboardTrend(ctx context.Context, arg KolDashboardTrendParams) ([]KolDashboardTrendRow, error)
// Cross-tenant aggregation for KOL dashboard.
KolUsageCountByPackage(ctx context.Context, packageIds []int64) ([]KolUsageCountByPackageRow, error)
ListActiveImagesByFolder(ctx context.Context, arg ListActiveImagesByFolderParams) ([]ListActiveImagesByFolderRow, error)
ListActiveKolPromptsByPackage(ctx context.Context, arg ListActiveKolPromptsByPackageParams) ([]ListActiveKolPromptsByPackageRow, error)
// Cross-tenant read: used by platform admin for subscription fan-out.
// Excludes expired subscriptions so new prompts are not granted to them.
ListActiveSubscribersForPackage(ctx context.Context, packageID int64) ([]KolSubscription, error)
ListAllPromptRulesSimple(ctx context.Context, tenantID int64) ([]ListAllPromptRulesSimpleRow, error)
ListArticles(ctx context.Context, arg ListArticlesParams) ([]ListArticlesRow, error)
@@ -35,6 +35,8 @@ type KolSubscriptionPrompt struct {
PlatformHint *string
PackageName string
PublishedRevisionNo *int
// KolDisplayName is populated by ListSubscriptionPromptsByTenant (joins kol_profiles).
KolDisplayName string
// Fields below only populated by GetSubscriptionPromptByID.
SubscriptionStatus string
SubscriptionEndAt *time.Time
@@ -201,6 +203,7 @@ func (r *kolSubscriptionRepository) ListSubscriptionPromptsByTenant(ctx context.
PlatformHint: nullableText(row.PlatformHint),
PackageName: row.PackageName,
PublishedRevisionNo: nullableIntFromInt4(row.PublishedRevisionNo),
KolDisplayName: row.KolDisplayName,
}
}
return result, nil
@@ -46,8 +46,9 @@ type KolDashboardOverview struct {
}
type KolDashboardTrendPoint struct {
Day time.Time
Usages int64
Day time.Time
Usages int64
Subscriptions int64
}
type KolUsagePackageStats struct {
@@ -170,8 +171,9 @@ func (r *kolUsageRepository) DashboardTrend(ctx context.Context, packageIDs []in
day = row.D.Time
}
result[i] = KolDashboardTrendPoint{
Day: day,
Usages: row.Usages,
Day: day,
Usages: row.Usages,
Subscriptions: row.Subscriptions,
}
}
return result, nil
@@ -8,7 +8,8 @@ SELECT kp.id, kp.tenant_id, kp.kol_profile_id, kp.name, kp.description,
(SELECT COUNT(*) FROM kol_prompts p
WHERE p.package_id = kp.id AND p.status = 'active' AND p.deleted_at IS NULL) AS prompt_count,
(SELECT COUNT(*) FROM kol_subscriptions s
WHERE s.package_id = kp.id AND s.status = 'active') AS subscriber_count
WHERE s.package_id = kp.id AND s.status = 'active'
AND (s.end_at IS NULL OR s.end_at > NOW())) AS subscriber_count
FROM kol_packages kp
JOIN kol_profiles pf ON pf.id = kp.kol_profile_id
AND pf.status = 'active'
@@ -23,11 +23,13 @@ WHERE tenant_id = sqlc.arg(tenant_id)::bigint
ORDER BY created_at DESC;
-- name: ListActiveSubscribersForPackage :many
-- Cross-tenant read: used by platform admin for subscription fan-out.
-- Excludes expired subscriptions so new prompts are not granted to them.
SELECT id, tenant_id, package_id, status, approved_by, start_at, end_at, created_at, updated_at
FROM kol_subscriptions
WHERE package_id = sqlc.arg(package_id)
AND tenant_id IS NOT NULL
AND status = 'active';
AND status = 'active'
AND (end_at IS NULL OR end_at > NOW());
-- name: ApproveKolSubscription :exec
UPDATE kol_subscriptions
@@ -37,7 +39,8 @@ SET status = 'active',
end_at = sqlc.arg(end_at),
updated_at = NOW()
WHERE id = sqlc.arg(id)
AND tenant_id = sqlc.arg(tenant_id)::bigint;
AND tenant_id = sqlc.arg(tenant_id)::bigint
AND status = 'pending';
-- name: RevokeKolSubscription :exec
UPDATE kol_subscriptions
@@ -55,10 +58,25 @@ ON CONFLICT (subscription_id, prompt_id) DO NOTHING;
SELECT sp.id, sp.tenant_id, sp.subscription_id, sp.package_id, sp.prompt_id,
sp.status, sp.granted_at, sp.revoked_at,
p.name AS prompt_name, p.platform_hint, p.published_revision_no,
k.name AS package_name
k.name AS package_name,
pf.display_name AS kol_display_name
FROM kol_subscription_prompts sp
JOIN kol_prompts p ON p.id = sp.prompt_id AND p.deleted_at IS NULL
JOIN kol_packages k ON k.id = sp.package_id AND k.deleted_at IS NULL
JOIN kol_subscriptions s ON s.id = sp.subscription_id
AND s.tenant_id = sp.tenant_id
AND s.package_id = sp.package_id
AND s.status = 'active'
AND (s.end_at IS NULL OR s.end_at > NOW())
JOIN kol_prompts p ON p.id = sp.prompt_id
AND p.package_id = sp.package_id
AND p.status = 'active'
AND p.published_revision_no IS NOT NULL
AND p.deleted_at IS NULL
JOIN kol_packages k ON k.id = sp.package_id
AND k.status = 'published'
AND k.deleted_at IS NULL
JOIN kol_profiles pf ON pf.id = k.kol_profile_id
AND pf.status = 'active'
AND pf.deleted_at IS NULL
WHERE sp.tenant_id = sqlc.arg(tenant_id)::bigint
AND sp.status = 'active'
ORDER BY sp.granted_at DESC;
@@ -44,6 +44,7 @@ WHERE generation_task_id = sqlc.arg(generation_task_id)
AND tenant_id = sqlc.arg(tenant_id)::bigint;
-- name: KolDashboardOverview :one
-- Cross-tenant aggregation: KOL owns packages across subscriber tenants.
SELECT
(SELECT COUNT(DISTINCT s.tenant_id) FROM kol_subscriptions s
WHERE s.package_id = ANY(sqlc.arg(package_ids)::bigint[]) AND s.status='active'
@@ -53,32 +54,53 @@ SELECT
AND s.status='active'
AND s.created_at >= date_trunc('month', NOW())) AS month_subs,
(SELECT COUNT(*) FROM kol_usage_logs u
WHERE u.package_id = ANY(sqlc.arg(package_ids)::bigint[])
AND u.tenant_id IS NOT NULL) AS total_usage,
WHERE u.package_id = ANY(sqlc.arg(package_ids)::bigint[])) AS total_usage,
(SELECT COUNT(*) FROM kol_usage_logs u
WHERE u.package_id = ANY(sqlc.arg(package_ids)::bigint[])
AND u.tenant_id IS NOT NULL
AND u.created_at >= date_trunc('month', NOW())) AS month_usage,
(SELECT COUNT(*) FROM kol_usage_logs u
WHERE u.package_id = ANY(sqlc.arg(package_ids)::bigint[])
AND u.tenant_id IS NOT NULL
AND u.status='failed') AS failure_count;
-- name: KolDashboardTrend :many
SELECT date_trunc('day', u.created_at)::date AS d,
COUNT(*) FILTER (WHERE u.status='completed') AS usages
FROM kol_usage_logs u
WHERE u.package_id = ANY(sqlc.arg(package_ids)::bigint[])
AND u.tenant_id IS NOT NULL
AND u.created_at >= NOW() - sqlc.arg(period_days)::int * INTERVAL '1 day'
GROUP BY d
ORDER BY d ASC;
-- Returns a per-day series combining usage count and new subscription count for KOL dashboard.
WITH days AS (
SELECT generate_series(
date_trunc('day', NOW()) - (sqlc.arg(period_days)::int - 1) * INTERVAL '1 day',
date_trunc('day', NOW()),
INTERVAL '1 day'
)::date AS d
),
u AS (
SELECT date_trunc('day', created_at)::date AS d,
COUNT(*) FILTER (WHERE status = 'completed') AS usages
FROM kol_usage_logs
WHERE package_id = ANY(sqlc.arg(package_ids)::bigint[])
AND created_at >= NOW() - sqlc.arg(period_days)::int * INTERVAL '1 day'
GROUP BY 1
),
s AS (
SELECT date_trunc('day', created_at)::date AS d,
COUNT(*) AS subscriptions
FROM kol_subscriptions
WHERE package_id = ANY(sqlc.arg(package_ids)::bigint[])
AND status IN ('active', 'expired', 'revoked')
AND created_at >= NOW() - sqlc.arg(period_days)::int * INTERVAL '1 day'
GROUP BY 1
)
SELECT days.d,
COALESCE(u.usages, 0)::bigint AS usages,
COALESCE(s.subscriptions, 0)::bigint AS subscriptions
FROM days
LEFT JOIN u ON u.d = days.d
LEFT JOIN s ON s.d = days.d
ORDER BY days.d ASC;
-- name: KolUsageCountByPackage :many
-- Cross-tenant aggregation for KOL dashboard.
SELECT package_id, COUNT(*) AS total,
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed
FROM kol_usage_logs
WHERE package_id = ANY(sqlc.arg(package_ids)::bigint[])
AND tenant_id IS NOT NULL
GROUP BY package_id;