Fix namespace validation (unique path) on group creation
What does this MR do?
There are two bugs in validating the namespace (unique path) when creating a group:
Initial situation:
There's a top-level group group-1
and its sub group group-2
resulting in the full path group-1/group-2
:
Group | Path | Full path |
---|---|---|
Group 1 | group-1 |
group-1 |
Group 2 (sub group of Group 1) | group-2 |
group-1/group-2 |
Using the API, it's possible to create a new top-level group with path group-2
but it's not possible to create a new sub group with path group-2
in group Group 1
(resulting in full path group-1/group-2
). This behavior is correct.
1. ~bug: Add a top-level group
Using the UI (https://gitlab.com/groups/new), if you enter Group 2
as group name, the path is set to group-2
, but the path is changed to group-21
after a second. If you manually change the path back to group-2
, the error Group path is already taken. Suggestions: group-21
is shown.
What happens in the background: To ensure that the path is unique, it checks if a namespace with the same path already exists. However, this check ignores nesting. So the validation prevents to create a top-level group group-2
, because a nested group group-2
(with full path group-1/group-2
) exists. This bug is fixed by this MR.
before MR | after MR |
---|---|
Create_group_before | Create_group_after |
2. ~bug: Add a sub group
Using the UI (https://gitlab.com/groups/new?parent_id= <id of Group 1>
), there is no unique path validation at all. So if you enter Group 2
as group name, the path is set to group-2
and doesn't change. Only after you press Create group
, the error Group URL has already been taken
is shown. This MR enables unique path validation for sub group as well (in the scope of its parent group).
before MR | after MR |
---|---|
Create_subgroup_before | Create_subgroup_after |
Changes by this MR:
Previously, the endpoint /users/:namespace/suggests
was used for the group path validation. This MR implements a separate API endpoint /api/v4/namespaces/:namespace/exists
. The new endpoint is designed for namespaces and takes into account the nesting of namespaces.
/cc @bufferoverflow
Does this MR meet the acceptance criteria?
Conformity
-
📋 Does this MR need a changelog?-
I have included a changelog entry.
-
- [-] Documentation (if required)
-
Code review guidelines -
Merge request performance guidelines -
Style guides -
Database guides - [-] Separation of EE specific content
Availability and Testing
-
Review and add/update tests for this feature/bug. Consider all test levels. See the Test Planning Process. - [-] Tested in all supported browsers
- [-] Informed Infrastructure department of a default or new setting change, if applicable per definition of done
Security
If this MR contains changes to processing or storing of credentials or tokens, authorization and authentication methods and other items described in the security review guidelines:
- [-] Label as security and @ mention
@gitlab-com/gl-security/appsec
- [-] The MR includes necessary changes to maintain consistency between UI, API, email, or other methods
- [-] Security reports checked/validated by a reviewer from the AppSec team
Database Review Information
Plan - no match - https://console.postgres.ai/shared/65f7b299-21b3-4c70-acf3-5dacfdf6f228 Plan - with match - https://console.postgres.ai/shared/8c741db7-5f2a-4525-a221-5330b618641cQuery info for Namespace.by_parent(params[:parent_id]).filter_by_name_or_path(namespace_path).exists?
SELECT 1 AS one
FROM "namespaces"
WHERE "namespaces"."parent_id" = 9970
AND (lower(path) = 'gitlab' OR lower(name) = 'gitlab')
LIMIT 1
Limit (cost=0.56..15.72 rows=1 width=4) (actual time=16.948..16.950 rows=0 loops=1)
Buffers: shared read=55 dirtied=1
I/O Timings: read=15.828
-> Index Scan using index_namespaces_on_parent_id_and_id on public.namespaces (cost=0.56..15.72 rows=1 width=4) (actual time=16.937..16.938 rows=0 loops=1)
Index Cond: (namespaces.parent_id = 9970)
Filter: ((lower((namespaces.path)::text) = 'gitlab'::text) OR (lower((namespaces.name)::text) = 'gitlab'::text))
Rows Removed by Filter: 51
Buffers: shared read=55 dirtied=1
I/O Timings: read=15.828
Time: 26.003 ms
- planning: 8.939 ms
- execution: 17.064 ms
- I/O read: 15.828 ms
- I/O write: N/A
Shared buffers:
- hits: 0 from the buffer pool
- reads: 55 (~440.00 KiB) from the OS file cache, including disk I/O
- dirtied: 1 (~8.00 KiB)
- writes: 0
Limit (cost=6.67..8.19 rows=1 width=4) (actual time=25.163..25.169 rows=1 loops=1)
Buffers: shared read=14
I/O Timings: read=24.753
-> Bitmap Heap Scan on public.namespaces (cost=6.67..8.19 rows=1 width=4) (actual time=25.160..25.165 rows=1 loops=1)
Buffers: shared read=14
I/O Timings: read=24.753
-> BitmapAnd (cost=6.67..6.67 rows=1 width=0) (actual time=21.911..21.915 rows=0 loops=1)
Buffers: shared read=13
I/O Timings: read=21.526
-> Bitmap Index Scan using index_namespaces_on_parent_id_and_id (cost=0.00..2.12 rows=8 width=0) (actual time=6.251..6.252 rows=51 loops=1)
Index Cond: (namespaces.parent_id = 9970)
Buffers: shared read=4
I/O Timings: read=6.166
-> BitmapOr (cost=4.30..4.30 rows=7 width=0) (actual time=15.649..15.651 rows=0 loops=1)
Buffers: shared read=9
I/O Timings: read=15.360
-> Bitmap Index Scan using index_on_namespaces_lower_path (cost=0.00..2.08 rows=2 width=0) (actual time=6.963..6.963 rows=2 loops=1)
Index Cond: (lower((namespaces.path)::text) = 'geo-team'::text)
Buffers: shared read=4
I/O Timings: read=6.890
-> Bitmap Index Scan using index_on_namespaces_lower_name (cost=0.00..2.22 rows=5 width=0) (actual time=8.684..8.684 rows=1 loops=1)
Index Cond: (lower((namespaces.name)::text) = 'geo-team'::text)
Buffers: shared read=5
I/O Timings: read=8.471
Time: 27.178 ms
- planning: 1.936 ms
- execution: 25.242 ms
- I/O read: 24.753 ms
- I/O write: N/A
Shared buffers:
- hits: 0 from the buffer pool
- reads: 14 (~112.00 KiB) from the OS file cache, including disk I/O
- dirtied: 0
- writes: 0