Split out standalone into user and instance boundaries

What does this MR do and why?

Split out standalone into user and instance boundaries for authorizing API endpoints with explicit access check.

References

Issue: #583042

New queries

Rails

Query using the new for_namespaces scope

PersonalAccessToken.last.permitted_for_boundary?(Authz::Boundary.for(Group.first(2)), :read_job)

Query using the new for_standalone scope

PersonalAccessToken.last.permitted_for_boundary?(Authz::Boundary.for(:instance), :read_job)

Raw SQL

Namespace boundary

SELECT DISTINCT
	jsonb_array_elements_text(permissions)
FROM
	"granular_scopes"
	INNER JOIN "personal_access_token_granular_scopes" ON "granular_scopes"."id" = "personal_access_token_granular_scopes"."granular_scope_id"
WHERE
	"personal_access_token_granular_scopes"."personal_access_token_id" = (
		SELECT
			"personal_access_tokens".id
		FROM
			"personal_access_tokens"
		ORDER BY
			"personal_access_tokens"."id" DESC
		LIMIT 1)
	AND("granular_scopes"."namespace_id" IN((
			SELECT
				id FROM namespaces
			WHERE
				namespaces.type = 'Group'
			LIMIT 2))
	AND "granular_scopes"."access" IN(0, 2)
	OR "granular_scopes"."namespace_id" IS NULL
	AND "granular_scopes"."access" = 1);

Standalone boundary

SELECT DISTINCT
	jsonb_array_elements_text(permissions)
FROM
	"granular_scopes"
	INNER JOIN "personal_access_token_granular_scopes" ON "granular_scopes"."id" = "personal_access_token_granular_scopes"."granular_scope_id"
WHERE
	"personal_access_token_granular_scopes"."personal_access_token_id" = (
		SELECT
			"personal_access_tokens".id
		FROM
			"personal_access_tokens"
		ORDER BY
			"personal_access_tokens"."id" DESC
		LIMIT 1)
	AND "granular_scopes"."namespace_id" IS NULL
	AND "granular_scopes"."access" = 4
	AND "granular_scopes"."access" != 1

Query plan

Namespace boundary

Postgresql.ai: https://console.postgres.ai/shared/8432adbe-207e-40fd-8e71-38c0f0a5b2e8

 HashAggregate  (cost=338.71..339.96 rows=100 width=32) (actual time=5.313..5.315 rows=1 loops=1)
   Group Key: jsonb_array_elements_text(granular_scopes.permissions)
   Buffers: shared hit=306 read=5
   I/O Timings: read=4.487 write=0.000
   InitPlan 1 (returns $0)
     ->  Limit  (cost=0.44..0.46 rows=1 width=4) (actual time=0.025..0.026 rows=1 loops=1)
           Buffers: shared hit=5
           I/O Timings: read=0.000 write=0.000
           ->  Index Only Scan Backward using personal_access_tokens_pkey on public.personal_access_tokens  (cost=0.44..753579.94 rows=29526974 width=4) (actual time=0.024..0.024 rows=1 loops=1)
                 Heap Fetches: 1
                 Buffers: shared hit=5
                 I/O Timings: read=0.000 write=0.000
   ->  ProjectSet  (cost=0.79..329.50 rows=3500 width=32) (actual time=4.658..5.302 rows=28 loops=1)
         Buffers: shared hit=306 read=5
         I/O Timings: read=4.487 write=0.000
         ->  Nested Loop  (cost=0.79..311.74 rows=35 width=17) (actual time=4.636..5.260 rows=28 loops=1)
               Buffers: shared hit=306 read=5
               I/O Timings: read=4.487 write=0.000
               ->  Seq Scan on public.personal_access_token_granular_scopes  (cost=0.00..5.25 rows=100 width=8) (actual time=0.030..0.044 rows=100 loops=1)
                     Filter: (personal_access_token_granular_scopes.personal_access_token_id = $0)
                     Rows Removed by Filter: 0
                     Buffers: shared hit=6
                     I/O Timings: read=0.000 write=0.000
               ->  Index Scan using granular_scopes_pkey on public.granular_scopes  (cost=0.79..3.06 rows=1 width=25) (actual time=0.052..0.052 rows=0 loops=100)
                     Index Cond: (granular_scopes.id = personal_access_token_granular_scopes.granular_scope_id)
                     Filter: (((hashed SubPlan 2) AND (granular_scopes.access = ANY ('{0,2}'::integer[]))) OR ((granular_scopes.namespace_id IS NULL) AND (granular_scopes.access = 1)))
                     Rows Removed by Filter: 1
                     Buffers: shared hit=300 read=5
                     I/O Timings: read=4.487 write=0.000
                     SubPlan 2
                       ->  Limit  (cost=0.43..0.50 rows=2 width=4) (actual time=3.905..4.548 rows=2 loops=1)
                             Buffers: shared read=5
                             I/O Timings: read=4.487 write=0.000
                             ->  Index Only Scan using index_groups_on_parent_id_id on public.namespaces  (cost=0.43..273332.10 rows=8764295 width=4) (actual time=3.905..4.547 rows=2 loops=1)
                                   Heap Fetches: 0
                                   Buffers: shared read=5
                                   I/O Timings: read=4.487 write=0.000
Settings: effective_cache_size = '472585MB', seq_page_cost = '4', work_mem = '100MB', random_page_cost = '1.5', jit = 'off'

Standalone boundary

Postgresql.ai: https://console.postgres.ai/shared/9d170180-0eaf-4759-81b8-3406ecd879c6

 HashAggregate  (cost=273.08..274.33 rows=100 width=32) (actual time=0.772..0.773 rows=1 loops=1)
   Group Key: jsonb_array_elements_text(granular_scopes.permissions)
   Buffers: shared hit=306
   I/O Timings: read=0.000 write=0.000
   InitPlan 1 (returns $0)
     ->  Limit  (cost=0.44..0.46 rows=1 width=4) (actual time=0.025..0.025 rows=1 loops=1)
           Buffers: shared hit=5
           I/O Timings: read=0.000 write=0.000
           ->  Index Only Scan Backward using personal_access_tokens_pkey on public.personal_access_tokens  (cost=0.44..753579.94 rows=29526974 width=4) (actual time=0.024..0.024 rows=1 loops=1)
                 Heap Fetches: 1
                 Buffers: shared hit=5
                 I/O Timings: read=0.000 write=0.000
   ->  ProjectSet  (cost=0.29..268.86 rows=1500 width=32) (actual time=0.076..0.759 rows=38 loops=1)
         Buffers: shared hit=306
         I/O Timings: read=0.000 write=0.000
         ->  Nested Loop  (cost=0.29..261.25 rows=15 width=17) (actual time=0.059..0.718 rows=38 loops=1)
               Buffers: shared hit=306
               I/O Timings: read=0.000 write=0.000
               ->  Seq Scan on public.personal_access_token_granular_scopes  (cost=0.00..5.25 rows=100 width=8) (actual time=0.030..0.040 rows=100 loops=1)
                     Filter: (personal_access_token_granular_scopes.personal_access_token_id = $0)
                     Rows Removed by Filter: 0
                     Buffers: shared hit=6
                     I/O Timings: read=0.000 write=0.000
               ->  Index Scan using granular_scopes_pkey on public.granular_scopes  (cost=0.29..2.56 rows=1 width=25) (actual time=0.006..0.006 rows=0 loops=100)
                     Index Cond: (granular_scopes.id = personal_access_token_granular_scopes.granular_scope_id)
                     Filter: ((granular_scopes.namespace_id IS NULL) AND (granular_scopes.access <> 1) AND (granular_scopes.access = 4))
                     Rows Removed by Filter: 1
                     Buffers: shared hit=300
                     I/O Timings: read=0.000 write=0.000
Settings: work_mem = '100MB', random_page_cost = '1.5', jit = 'off', effective_cache_size = '472585MB', seq_page_cost = '4'

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Alex Buijs

Merge request reports

Loading