Skip to content

Commit ab517b5

Browse files
committed
fix(api): allow scoped keys past cli warnings
1 parent f14f684 commit ab517b5

2 files changed

Lines changed: 132 additions & 0 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
CREATE OR REPLACE FUNCTION "public"."get_organization_cli_warnings"("orgid" uuid, "cli_version" text)
2+
RETURNS jsonb[]
3+
LANGUAGE plpgsql
4+
SECURITY DEFINER
5+
SET search_path = ''
6+
AS $$
7+
DECLARE
8+
messages jsonb[] := ARRAY[]::jsonb[];
9+
has_read_access boolean;
10+
BEGIN
11+
PERFORM cli_version;
12+
13+
SELECT public.check_min_rights(
14+
'read'::public.user_min_right,
15+
public.get_identity_apikey_only('{write,all,upload,read}'::public.key_mode[]),
16+
orgid,
17+
NULL::varchar,
18+
NULL::bigint
19+
)
20+
INTO has_read_access;
21+
22+
IF NOT COALESCE(has_read_access, false) THEN
23+
-- Upload performs app-scoped permission and plan checks after this RPC.
24+
-- App-scoped API keys may legitimately upload without org-level read access,
25+
-- so skip org warnings instead of blocking the upload here.
26+
RETURN messages;
27+
END IF;
28+
29+
IF (
30+
public.is_paying_and_good_plan_org_action(orgid, ARRAY['mau']::public.action_type[]) = true
31+
AND public.is_paying_and_good_plan_org_action(orgid, ARRAY['bandwidth']::public.action_type[]) = true
32+
AND public.is_paying_and_good_plan_org_action(orgid, ARRAY['storage']::public.action_type[]) = false
33+
) THEN
34+
messages := array_append(messages, jsonb_build_object(
35+
'message',
36+
'You have exceeded your storage limit.\nUpload will fail, but you can still download your data.\nMAU and bandwidth limits are not exceeded.\nIn order to upload your plan, please upgrade your plan here: https://console.capgo.app/settings/plans.',
37+
'fatal',
38+
true
39+
));
40+
END IF;
41+
42+
RETURN messages;
43+
END;
44+
$$;
45+
46+
ALTER FUNCTION "public"."get_organization_cli_warnings"("orgid" uuid, "cli_version" text) OWNER TO "postgres";
47+
REVOKE ALL ON FUNCTION "public"."get_organization_cli_warnings"("orgid" uuid, "cli_version" text) FROM PUBLIC;
48+
GRANT ALL ON FUNCTION "public"."get_organization_cli_warnings"("orgid" uuid, "cli_version" text) TO "anon";
49+
GRANT ALL ON FUNCTION "public"."get_organization_cli_warnings"("orgid" uuid, "cli_version" text) TO "authenticated";
50+
GRANT ALL ON FUNCTION "public"."get_organization_cli_warnings"("orgid" uuid, "cli_version" text) TO "service_role";

tests/rbac-permissions.test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,88 @@ describe('rbac permission system', () => {
739739
expect(deniedResult.rows[0].allowed).toBe(false)
740740
expect(allowedResult.rows[0].allowed).toBe(true)
741741
})
742+
743+
it('returns no CLI warnings for an app-scoped API key without org read', async () => {
744+
const id = randomUUID()
745+
const orgId = randomUUID()
746+
const appUuid = randomUUID()
747+
const appId = `com.cli.warning.${id}`
748+
const scopedKey = `rbac-cli-warning-${id}`
749+
750+
await query(`
751+
INSERT INTO public.orgs (id, name, management_email, created_by, use_new_rbac)
752+
VALUES ($1::uuid, $2, $3, $4::uuid, true)
753+
`, [orgId, `CLI Warning Org ${id}`, `cli-warning-${id}@capgo.app`, USER_ID])
754+
755+
await query(`
756+
INSERT INTO public.apps (id, app_id, icon_url, owner_org, name)
757+
VALUES ($1::uuid, $2, $3, $4::uuid, $5)
758+
`, [appUuid, appId, 'https://example.com/icon.png', orgId, `CLI Warning App ${id}`])
759+
760+
await query(`
761+
INSERT INTO public.apikeys (user_id, key, key_hash, mode, name, limited_to_orgs, limited_to_apps)
762+
VALUES ($1::uuid, $2, NULL, NULL, $3, ARRAY[$4::uuid], ARRAY[$5]::varchar[])
763+
`, [USER_ID, scopedKey, `CLI warning scoped ${id}`, orgId, appId])
764+
765+
await query(`
766+
INSERT INTO public.role_bindings (
767+
principal_type,
768+
principal_id,
769+
role_id,
770+
scope_type,
771+
org_id,
772+
app_id,
773+
granted_by,
774+
reason,
775+
is_direct
776+
)
777+
SELECT
778+
public.rbac_principal_apikey(),
779+
ak.rbac_id,
780+
r.id,
781+
public.rbac_scope_app(),
782+
$2::uuid,
783+
$3::uuid,
784+
$4::uuid,
785+
'cli warning app-scoped key regression',
786+
true
787+
FROM public.apikeys ak
788+
CROSS JOIN public.roles r
789+
WHERE ak.key = $1
790+
AND r.name = public.rbac_role_app_admin()
791+
LIMIT 1
792+
`, [scopedKey, orgId, appUuid, USER_ID])
793+
794+
await query(`SELECT set_config('request.headers', jsonb_build_object('capgkey', $1::text)::text, true)`, [scopedKey])
795+
796+
const orgReadResult = await query(`
797+
SELECT public.check_min_rights(
798+
'read'::public.user_min_right,
799+
public.get_identity_apikey_only('{write,all,upload,read}'::public.key_mode[]),
800+
$1::uuid,
801+
NULL::varchar,
802+
NULL::bigint
803+
) AS allowed
804+
`, [orgId])
805+
806+
const uploadResult = await query(`
807+
SELECT public.check_min_rights(
808+
'upload'::public.user_min_right,
809+
public.get_identity_apikey_only('{write,all,upload,read}'::public.key_mode[]),
810+
$1::uuid,
811+
$2,
812+
NULL::bigint
813+
) AS allowed
814+
`, [orgId, appId])
815+
816+
const warningsResult = await query(`
817+
SELECT cardinality(public.get_organization_cli_warnings($1::uuid, '7.95.12')) AS warning_count
818+
`, [orgId])
819+
820+
expect(orgReadResult.rows[0].allowed).toBe(false)
821+
expect(uploadResult.rows[0].allowed).toBe(true)
822+
expect(warningsResult.rows[0].warning_count).toBe(0)
823+
})
742824
})
743825

744826
describe('feature flag routing', () => {

0 commit comments

Comments
 (0)