Fix bypass of IP restriction allowing access to restricted group snippets

What does this MR do and why?

Related to: Bypassing "Restrict access by IP address" to vi... (#499487 - closed)

This MR aims to prevent users from viewing snippet names belonging to restricted group projects if their IP address is not authorized. This ensures that IP-based access control is enforced consistently for snippet visibility.

To help you better understand, this is what the filter_unauthorised_snippets method does: It excludes unauthorized snippets from the list of snippets it receives as an argument.

Screenshot_2024-12-12_at_9.33.11_am

The Query Plans: The Postgres.io link

Sort  (cost=6129.73..6129.74 rows=6 width=2129) (actual time=1.328..1.337 rows=52 loops=1)
   Sort Key: snippets.id DESC
   Sort Method: quicksort  Memory: 76kB
   Buffers: shared hit=1728
   I/O Timings: read=0.000 write=0.000
   ->  Nested Loop  (cost=874.54..6129.65 rows=6 width=2129) (actual time=1.060..1.256 rows=52 loops=1)
         Buffers: shared hit=1725
         I/O Timings: read=0.000 write=0.000
         ->  HashAggregate  (cost=400.11..400.23 rows=12 width=4) (actual time=0.496..0.505 rows=52 loops=1)
               Group Key: snippets_1.id
               Buffers: shared hit=608
               I/O Timings: read=0.000 write=0.000
               ->  Append  (cost=0.42..400.08 rows=12 width=4) (actual time=0.019..0.479 rows=67 loops=1)
                     Buffers: shared hit=608
                     I/O Timings: read=0.000 write=0.000
                     ->  Index Scan using index_snippets_on_author_id on public.snippets snippets_1  (cost=0.42..82.79 rows=8 width=4) (actual time=0.018..0.096 rows=33 loops=1)
                           Index Cond: (snippets_1.author_id = 64248)
                           Filter: ((snippets_1.project_id IS NULL) AND (snippets_1.visibility_level = ANY ('{10,20}'::integer[])))
                           Rows Removed by Filter: 24
                           Buffers: shared hit=60
                           I/O Timings: read=0.000 write=0.000
                     ->  Nested Loop  (cost=1.42..114.00 rows=3 width=4) (actual time=0.033..0.154 rows=15 loops=1)
                           Buffers: shared hit=208
                           I/O Timings: read=0.000 write=0.000
                           ->  Nested Loop  (cost=0.86..111.59 rows=2 width=12) (actual time=0.021..0.099 rows=16 loops=1)
                                 Buffers: shared hit=128
                                 I/O Timings: read=0.000 write=0.000
                                 ->  Index Scan using index_snippets_on_author_id on public.snippets snippets_2  (cost=0.42..82.79 rows=14 width=8) (actual time=0.007..0.032 rows=49 loops=1)
                                       Index Cond: (snippets_2.author_id = 64248)
                                       Filter: (snippets_2.visibility_level = ANY ('{10,20}'::integer[]))
                                       Rows Removed by Filter: 8
                                       Buffers: shared hit=60
                                       I/O Timings: read=0.000 write=0.000
                                 ->  Index Only Scan using index_projects_on_id_partial_for_visibility on public.projects  (cost=0.43..2.06 rows=1 width=4) (actual time=0.001..0.001 rows=0 loops=49)
                                       Index Cond: (projects.id = snippets_2.project_id)
                                       Heap Fetches: 15
                                       Buffers: shared hit=68
                                       I/O Timings: read=0.000 write=0.000
                           ->  Index Scan using index_project_features_on_project_id on public.project_features  (cost=0.56..1.21 rows=1 width=4) (actual time=0.003..0.003 rows=1 loops=16)
                                 Index Cond: (project_features.project_id = projects.id)
                                 Filter: (project_features.snippets_access_level = ANY ('{20,30}'::integer[]))
                                 Rows Removed by Filter: 0
                                 Buffers: shared hit=80
                                 I/O Timings: read=0.000 write=0.000
                     ->  Nested Loop  (cost=2.13..203.23 rows=1 width=4) (actual time=0.042..0.222 rows=19 loops=1)
                           Buffers: shared hit=340
                           I/O Timings: read=0.000 write=0.000
                           ->  Nested Loop  (cost=1.57..202.56 rows=1 width=16) (actual time=0.036..0.171 rows=19 loops=1)
                                 Buffers: shared hit=245
                                 I/O Timings: read=0.000 write=0.000
                                 ->  Nested Loop Semi Join  (cost=1.00..200.49 rows=1 width=12) (actual time=0.026..0.093 rows=19 loops=1)
                                       Buffers: shared hit=143
                                       I/O Timings: read=0.000 write=0.000
                                       ->  Index Scan using index_snippets_on_author_id on public.snippets snippets_3  (cost=0.42..82.65 rows=54 width=8) (actual time=0.007..0.028 rows=57 loops=1)
                                             Index Cond: (snippets_3.author_id = 64248)
                                             Buffers: shared hit=60
                                             I/O Timings: read=0.000 write=0.000
                                       ->  Index Only Scan using project_authorizations_pkey on public.project_authorizations  (cost=0.58..2.15 rows=1 width=4) (actual time=0.001..0.001 rows=0 loops=57)
                                             Index Cond: ((project_authorizations.user_id = 21572755) AND (project_authorizations.project_id = snippets_3.project_id))
                                             Heap Fetches: 0
                                             Buffers: shared hit=83
                                             I/O Timings: read=0.000 write=0.000
                                 ->  Index Only Scan using projects_pkey on public.projects projects_1  (cost=0.56..2.06 rows=1 width=4) (actual time=0.003..0.003 rows=1 loops=19)
                                       Index Cond: (projects_1.id = project_authorizations.project_id)
                                       Heap Fetches: 18
                                       Buffers: shared hit=102
                                       I/O Timings: read=0.000 write=0.000
                           ->  Index Scan using index_project_features_on_project_id on public.project_features project_features_1  (cost=0.56..0.66 rows=1 width=4) (actual time=0.002..0.002 rows=1 loops=19)
                                 Index Cond: (project_features_1.project_id = projects_1.id)
                                 Filter: (project_features_1.snippets_access_level = ANY ('{20,30,10}'::integer[]))
                                 Rows Removed by Filter: 0
                                 Buffers: shared hit=95
                                 I/O Timings: read=0.000 write=0.000
         ->  Index Scan using index_snippets_on_id_and_type on public.snippets  (cost=474.43..477.45 rows=1 width=2129) (actual time=0.014..0.014 rows=1 loops=52)
               Index Cond: (snippets.id = snippets_1.id)
               Filter: (NOT (hashed SubPlan 2))
               Rows Removed by Filter: 0
               Buffers: shared hit=1117
               I/O Timings: read=0.000 write=0.000
               SubPlan 2
                 ->  Nested Loop  (cost=401.95..474.00 rows=1 width=4) (actual time=0.543..0.546 rows=0 loops=1)
                       Buffers: shared hit=909
                       I/O Timings: read=0.000 write=0.000
                       ->  Nested Loop  (cost=401.38..446.41 rows=1 width=12) (actual time=0.543..0.545 rows=0 loops=1)
                             Buffers: shared hit=909
                             I/O Timings: read=0.000 write=0.000
                             ->  Nested Loop  (cost=401.10..438.94 rows=5 width=8) (actual time=0.322..0.521 rows=19 loops=1)
                                   Buffers: shared hit=871
                                   I/O Timings: read=0.000 write=0.000
                                   ->  Nested Loop  (cost=400.53..425.07 rows=5 width=8) (actual time=0.316..0.481 rows=19 loops=1)
                                         Buffers: shared hit=776
                                         I/O Timings: read=0.000 write=0.000
                                         ->  HashAggregate  (cost=400.11..400.23 rows=12 width=4) (actual time=0.305..0.312 rows=52 loops=1)
                                               Group Key: snippets_5.id
                                               Buffers: shared hit=608
                                               I/O Timings: read=0.000 write=0.000
                                               ->  Append  (cost=0.42..400.08 rows=12 width=4) (actual time=0.007..0.291 rows=67 loops=1)
                                                     Buffers: shared hit=608
                                                     I/O Timings: read=0.000 write=0.000
                                                     ->  Index Scan using index_snippets_on_author_id on public.snippets snippets_5  (cost=0.42..82.79 rows=8 width=4) (actual time=0.006..0.027 rows=33 loops=1)
                                                           Index Cond: (snippets_5.author_id = 64248)
                                                           Filter: ((snippets_5.project_id IS NULL) AND (snippets_5.visibility_level = ANY ('{10,20}'::integer[])))
                                                           Rows Removed by Filter: 24
                                                           Buffers: shared hit=60
                                                           I/O Timings: read=0.000 write=0.000
                                                     ->  Nested Loop  (cost=1.42..114.00 rows=3 width=4) (actual time=0.019..0.100 rows=15 loops=1)
                                                           Buffers: shared hit=208
                                                           I/O Timings: read=0.000 write=0.000
                                                           ->  Nested Loop  (cost=0.86..111.59 rows=2 width=12) (actual time=0.013..0.068 rows=16 loops=1)
                                                                 Buffers: shared hit=128
                                                                 I/O Timings: read=0.000 write=0.000
                                                                 ->  Index Scan using index_snippets_on_author_id on public.snippets snippets_6  (cost=0.42..82.79 rows=14 width=8) (actual time=0.005..0.028 rows=49 loops=1)
                                                                       Index Cond: (snippets_6.author_id = 64248)
                                                                       Filter: (snippets_6.visibility_level = ANY ('{10,20}'::integer[]))
                                                                       Rows Removed by Filter: 8
                                                                       Buffers: shared hit=60
                                                                       I/O Timings: read=0.000 write=0.000
                                                                 ->  Index Only Scan using index_projects_on_id_partial_for_visibility on public.projects projects_3  (cost=0.43..2.06 rows=1 width=4) (actual time=0.001..0.001 rows=0 loops=49)
                                                                       Index Cond: (projects_3.id = snippets_6.project_id)
                                                                       Heap Fetches: 15
                                                                       Buffers: shared hit=68
                                                                       I/O Timings: read=0.000 write=0.000
                                                           ->  Index Scan using index_project_features_on_project_id on public.project_features project_features_2  (cost=0.56..1.21 rows=1 width=4) (actual time=0.002..0.002 rows=1 loops=16)
                                                                 Index Cond: (project_features_2.project_id = projects_3.id)
                                                                 Filter: (project_features_2.snippets_access_level = ANY ('{20,30}'::integer[]))
                                                                 Rows Removed by Filter: 0
                                                                 Buffers: shared hit=80
                                                                 I/O Timings: read=0.000 write=0.000
                                                     ->  Nested Loop  (cost=2.13..203.23 rows=1 width=4) (actual time=0.028..0.158 rows=19 loops=1)
                                                           Buffers: shared hit=340
                                                           I/O Timings: read=0.000 write=0.000
                                                           ->  Nested Loop  (cost=1.57..202.56 rows=1 width=16) (actual time=0.022..0.119 rows=19 loops=1)
                                                                 Buffers: shared hit=245
                                                                 I/O Timings: read=0.000 write=0.000
                                                                 ->  Nested Loop Semi Join  (cost=1.00..200.49 rows=1 width=12) (actual time=0.015..0.076 rows=19 loops=1)
                                                                       Buffers: shared hit=143
                                                                       I/O Timings: read=0.000 write=0.000
                                                                       ->  Index Scan using index_snippets_on_author_id on public.snippets snippets_7  (cost=0.42..82.65 rows=54 width=8) (actual time=0.005..0.027 rows=57 loops=1)
                                                                             Index Cond: (snippets_7.author_id = 64248)
                                                                             Buffers: shared hit=60
                                                                             I/O Timings: read=0.000 write=0.000
                                                                       ->  Index Only Scan using project_authorizations_pkey on public.project_authorizations project_authorizations_1  (cost=0.58..2.15 rows=1 width=4) (actual time=0.001..0.001 rows=0 loops=57)
                                                                             Index Cond: ((project_authorizations_1.user_id = 21572755) AND (project_authorizations_1.project_id = snippets_7.project_id))
                                                                             Heap Fetches: 0
                                                                             Buffers: shared hit=83
                                                                             I/O Timings: read=0.000 write=0.000
                                                                 ->  Index Only Scan using projects_pkey on public.projects projects_4  (cost=0.56..2.06 rows=1 width=4) (actual time=0.002..0.002 rows=1 loops=19)
                                                                       Index Cond: (projects_4.id = project_authorizations_1.project_id)
                                                                       Heap Fetches: 18
                                                                       Buffers: shared hit=102
                                                                       I/O Timings: read=0.000 write=0.000
                                                           ->  Index Scan using index_project_features_on_project_id on public.project_features project_features_3  (cost=0.56..0.66 rows=1 width=4) (actual time=0.002..0.002 rows=1 loops=19)
                                                                 Index Cond: (project_features_3.project_id = projects_4.id)
                                                                 Filter: (project_features_3.snippets_access_level = ANY ('{20,30,10}'::integer[]))
                                                                 Rows Removed by Filter: 0
                                                                 Buffers: shared hit=95
                                                                 I/O Timings: read=0.000 write=0.000
                                         ->  Index Only Scan using index_snippet_on_id_and_project_id on public.snippets snippets_4  (cost=0.42..2.07 rows=1 width=8) (actual time=0.003..0.003 rows=0 loops=52)
                                               Index Cond: ((snippets_4.id = snippets_5.id) AND (snippets_4.project_id IS NOT NULL))
                                               Heap Fetches: 0
                                               Buffers: shared hit=168
                                               I/O Timings: read=0.000 write=0.000
                                   ->  Index Scan using projects_pkey on public.projects projects_2  (cost=0.56..2.77 rows=1 width=8) (actual time=0.002..0.002 rows=1 loops=19)
                                         Index Cond: (projects_2.id = snippets_4.project_id)
                                         Buffers: shared hit=95
                                         I/O Timings: read=0.000 write=0.000
                             ->  Index Only Scan using index_ip_restrictions_on_group_id on public.ip_restrictions ip_restrictions_1  (cost=0.29..1.06 rows=44 width=4) (actual time=0.001..0.001 rows=0 loops=19)
                                   Index Cond: (ip_restrictions_1.group_id = projects_2.namespace_id)
                                   Heap Fetches: 0
                                   Buffers: shared hit=38
                                   I/O Timings: read=0.000 write=0.000
                       ->  Index Only Scan using index_namespaces_on_type_and_id on public.namespaces  (cost=0.56..27.59 rows=1 width=4) (actual time=0.000..0.000 rows=0 loops=0)
                             Index Cond: ((namespaces.type = 'Group'::text) AND (namespaces.id = projects_2.namespace_id))
                             Heap Fetches: 0
                             Filter: (NOT (SubPlan 1))
                             Rows Removed by Filter: 0
                             I/O Timings: read=0.000 write=0.000
                             SubPlan 1
                               ->  Index Scan using index_ip_restrictions_on_group_id on public.ip_restrictions  (cost=0.29..53.57 rows=44 width=32) (actual time=0.000..0.000 rows=0 loops=0)
                                     Index Cond: (ip_restrictions.group_id = namespaces.id)
                                     I/O Timings: read=0.000 write=0.000
Settings: seq_page_cost = '4', effective_cache_size = '472585MB', jit = 'off', random_page_cost = '1.5', work_mem = '100MB'

Statistics

Time: 16.799 ms
  - planning: 15.151 ms
  - execution: 1.648 ms
    - I/O read: 0.000 ms
    - I/O write: 0.000 ms

Shared buffers:
  - hits: 1728 (~13.50 MiB) from the buffer pool
  - reads: 0 from the OS file cache, including disk I/O
  - dirtied: 0
  - writes: 0

Screenshots or screen recordings

Before After
Screenshot_2024-12-10_at_10.11.42_pm Screenshot_2024-12-10_at_10.09.29_pm

How to set up and validate locally

Scenario:

1. Actors:
User A - private group A(PoC) owner with Gitlab public profile
User B - private group A(PoC) member e.g. role Guest

2. Steps:

  1. User A - create the private project AP in PoC group A
  2. User A - create a private snippet APS_1 in the PoC project AP
  3. User A - in the PoC group A go to Settings -> General -> Permissions and group features and set Restrict access by IP address to specified ip address
  4. User B - try to access to the PoC group A from another ip address than specified - you should see 404 not found - expected behaviour
  5. User B - go to User A public Gitlab profile then open tab Snippets - notice that you see there the snippet APS_1 - Bypassing "Restrict access by IP address" to view snippets names of the restricted group projects
  6. User A - now, in the project AP create the new private snippet APS_2
  7. User B - refresh the tab Snippets of the public profile of User A - notice that you also see the snippet APS_2 there - Bypassing "Restrict access by IP address" to view snippets names of the restricted group projects
Edited by Emma Park

Merge request reports

Loading