mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: stop group spend limits from leaking across org boundaries (#24294)
Three SQL queries (`GetUserGroupSpendLimit`, `ResolveUserChatSpendLimit`, `GetUserChatSpendInPeriod`) aggregated chat spend limits and usage globally across all organizations. A restrictive group limit in org A would bleed into org B. ## Changes - Add `organization_id` parameter to all three SQL queries in `coderd/database/queries/chats.sql` - When nil UUID is passed, queries fall back to global behavior (backward compat for HTTP dashboard endpoints) - When real org ID is passed, limits and spend are scoped to that organization - Thread `organizationID` through `ResolveUsageLimitStatus` → `checkUsageLimit` → all chatd call sites - Update dbauthz wrappers for new param structs - HTTP endpoints (`chatCostSummary`, `getMyChatUsageLimitStatus`) pass `uuid.Nil` with TODO for future org-scoped UI - Add `TestResolveUsageLimitStatus_OrgScoped` with 5 test cases covering org isolation, nil-UUID fallback, spend scoping, and user override priority Closes coder/internal#1466 > 🤖
This commit is contained in:
@@ -1134,10 +1134,16 @@ FROM users
|
||||
WHERE id = @user_id::uuid AND chat_spend_limit_micros IS NOT NULL;
|
||||
|
||||
-- name: GetUserChatSpendInPeriod :one
|
||||
-- Returns the total spend for a user in the given period.
|
||||
-- When organization_id is NULL, spend across all organizations is
|
||||
-- returned (global behavior). Otherwise only spend within the
|
||||
-- specified organization is included.
|
||||
SELECT COALESCE(SUM(cm.total_cost_micros), 0)::bigint AS total_spend_micros
|
||||
FROM chat_messages cm
|
||||
JOIN chats c ON c.id = cm.chat_id
|
||||
WHERE c.owner_id = @user_id::uuid
|
||||
AND (sqlc.narg('organization_id')::uuid IS NULL
|
||||
OR c.organization_id = sqlc.narg('organization_id')::uuid)
|
||||
AND cm.created_at >= @start_time::timestamptz
|
||||
AND cm.created_at < @end_time::timestamptz
|
||||
AND cm.total_cost_micros IS NOT NULL;
|
||||
@@ -1189,11 +1195,16 @@ WHERE id = @group_id::uuid AND chat_spend_limit_micros IS NOT NULL;
|
||||
|
||||
-- name: GetUserGroupSpendLimit :one
|
||||
-- Returns the minimum (most restrictive) group limit for a user.
|
||||
-- Returns -1 if the user has no group limits applied.
|
||||
-- Returns -1 if no group limits match the specified scope.
|
||||
-- When organization_id is NULL, groups across all organizations are
|
||||
-- considered (global behavior). Otherwise only groups within the
|
||||
-- specified organization are considered.
|
||||
SELECT COALESCE(MIN(g.chat_spend_limit_micros), -1)::bigint AS limit_micros
|
||||
FROM groups g
|
||||
JOIN group_members_expanded gme ON gme.group_id = g.id
|
||||
WHERE gme.user_id = @user_id::uuid
|
||||
AND (sqlc.narg('organization_id')::uuid IS NULL
|
||||
OR g.organization_id = sqlc.narg('organization_id')::uuid)
|
||||
AND g.chat_spend_limit_micros IS NOT NULL;
|
||||
|
||||
-- name: GetChatsByWorkspaceIDs :many
|
||||
@@ -1205,20 +1216,28 @@ ORDER BY workspace_id, updated_at DESC;
|
||||
|
||||
-- name: ResolveUserChatSpendLimit :one
|
||||
-- Resolves the effective spend limit for a user using the hierarchy:
|
||||
-- 1. Individual user override (highest priority)
|
||||
-- 2. Minimum group limit across all user's groups
|
||||
-- 1. Individual user override (highest priority, applies globally across
|
||||
-- all organizations since it lives on the users table)
|
||||
-- 2. Minimum group limit across the user's groups
|
||||
-- 3. Global default from config
|
||||
-- Returns -1 if limits are not enabled.
|
||||
-- When organization_id is NULL, groups across all organizations are
|
||||
-- considered (global behavior). Otherwise only groups within the
|
||||
-- specified organization are considered.
|
||||
-- limit_source indicates which tier won: 'user', 'group', 'default',
|
||||
-- or 'disabled'.
|
||||
SELECT CASE
|
||||
-- If limits are disabled, return -1.
|
||||
WHEN NOT cfg.enabled THEN -1
|
||||
-- Individual override takes priority.
|
||||
WHEN u.chat_spend_limit_micros IS NOT NULL THEN u.chat_spend_limit_micros
|
||||
-- Group limit (minimum across all user's groups) is next.
|
||||
WHEN gl.limit_micros IS NOT NULL THEN gl.limit_micros
|
||||
-- Fall back to global default.
|
||||
ELSE cfg.default_limit_micros
|
||||
END::bigint AS effective_limit_micros
|
||||
END::bigint AS effective_limit_micros,
|
||||
CASE
|
||||
WHEN NOT cfg.enabled THEN 'disabled'
|
||||
WHEN u.chat_spend_limit_micros IS NOT NULL THEN 'user'
|
||||
WHEN gl.limit_micros IS NOT NULL THEN 'group'
|
||||
ELSE 'default'
|
||||
END AS limit_source
|
||||
FROM chat_usage_limit_config cfg
|
||||
CROSS JOIN users u
|
||||
LEFT JOIN LATERAL (
|
||||
@@ -1226,6 +1245,8 @@ LEFT JOIN LATERAL (
|
||||
FROM groups g
|
||||
JOIN group_members_expanded gme ON gme.group_id = g.id
|
||||
WHERE gme.user_id = @user_id::uuid
|
||||
AND (sqlc.narg('organization_id')::uuid IS NULL
|
||||
OR g.organization_id = sqlc.narg('organization_id')::uuid)
|
||||
AND g.chat_spend_limit_micros IS NOT NULL
|
||||
) gl ON TRUE
|
||||
WHERE u.id = @user_id::uuid
|
||||
|
||||
Reference in New Issue
Block a user