Update from merge request

This commit is contained in:
root
2025-07-18 16:07:43 +00:00
parent c467ae2b64
commit f33a804406
19 changed files with 165 additions and 191 deletions

View File

@ -2124,7 +2124,6 @@ Gitlab/BoundedContexts:
- 'ee/app/finders/gpg_keys_finder.rb'
- 'ee/app/finders/group_saml_identity_finder.rb'
- 'ee/app/finders/groups/saved_replies_finder.rb'
- 'ee/app/finders/groups/users_finder.rb'
- 'ee/app/finders/groups_with_templates_finder.rb'
- 'ee/app/finders/incident_management/escalation_policies_finder.rb'
- 'ee/app/finders/incident_management/escalation_rules_finder.rb'

View File

@ -1 +1 @@
47335750c521ec469a5074962fb28b48b9dc06a6
bacb63191f1ba77f6be796e72e9b5bff68fde31d

View File

@ -303,7 +303,7 @@ export default {
is-collapsible
:collapsed="isCollapsed"
persist-collapsed-state
class="!gl-mt-5"
class="!gl-mt-5 !gl-overflow-hidden"
:body-class="{ '!gl-m-0 !gl-p-0': data.count || isPreview }"
@collapsed="isCollapsed = true"
@expanded="isCollapsed = false"
@ -323,7 +323,7 @@ export default {
<component :is="previewPresenter.component" v-else-if="previewPresenter && !hasError" />
<div
v-if="data.count && data.nodes.length < data.count"
class="gl-border-t gl-border-section gl-p-3"
class="glql-load-more gl-border-t gl-border-section gl-p-3"
>
<glql-pagination :count="data.nodes.length" :total-count="data.count" />
</div>
@ -331,11 +331,8 @@ export default {
<template v-if="showEmptyState" #empty>
{{ __('No data found for this query.') }}
</template>
<template #footer>
<glql-footnote />
</template>
</crud-component>
<glql-footnote v-if="!isCollapsed" />
</template>
<div v-else-if="hasError" class="markdown-code-block gl-relative">
<pre :class="preClasses"><code>{{ query }}</code></pre>

View File

@ -21,7 +21,7 @@ export default {
</script>
<template>
<div class="gl-flex gl-items-center gl-gap-1 gl-text-sm gl-text-subtle">
<div class="gl-mb-5 gl-mt-2 gl-flex gl-items-center gl-gap-1 gl-text-sm gl-text-subtle">
<gl-icon class="gl-mb-1 gl-mr-1" :size="12" name="tanuki" />
<gl-sprintf :message="__('%{linkStart}Embedded view%{linkEnd} powered by GLQL')">
<template #link="{ content }">

View File

@ -208,6 +208,11 @@
}
}
// Fix border bleed.
.gl-table-shadow:not(:has(+ .glql-load-more)) {
clip-path: inset(0 round 0 0 1px 1px);
}
.gl-table-shadow {
overflow-x: auto;
overflow-y: hidden;

View File

@ -111,9 +111,9 @@ module Mutations
end
end
def find_parent_by_full_path(full_path)
def find_parent_by_full_path(full_path, model = ::Project)
# Note: Group support is added in the EE module. For CE, we only support bulk edit for projects
::Gitlab::Graphql::Loaders::FullPathModelLoader.new(::Project, full_path).find.sync
::Gitlab::Graphql::Loaders::FullPathModelLoader.new(model, full_path).find.sync
end
end
end

View File

@ -912,105 +912,6 @@ Example response:
]
```
### List users
{{< details >}}
- Tier: Premium, Ultimate
- Offering: GitLab.com, GitLab Self-Managed, GitLab Dedicated
- Status: Experiment
{{< /details >}}
{{< history >}}
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/424505) in GitLab 16.6. This feature is an [experiment](../policy/development_stages_support.md).
{{< /history >}}
{{< alert type="warning" >}}
This endpoint is scheduled for removal in GitLab 18.3 (August 11th, 2025).
Use [`GET /groups/:id/saml_users`](#list-all-saml-users) and [`GET /groups/:id/service_accounts`](service_accounts.md#list-all-group-service-accounts) instead.
{{< /alert >}}
Get a list of users for a group. This endpoint returns users that are related to a top-level group regardless
of their current membership. For example, users that have a SAML identity connected to the group, or service accounts created
by the group or subgroups.
This endpoint is an [experiment](../policy/development_stages_support.md) and might be changed or removed without notice.
Requires Owner role for the group.
```plaintext
GET /groups/:id/users
```
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/345/users?include_saml_users=true&include_service_accounts=true"
```
Parameters:
| Attribute | Type | Required | Description |
|:---------------------------|:---------------|:----------------------|:------------|
| `id` | integer/string | yes | ID or [URL-encoded path of the group](rest/_index.md#namespaced-paths). |
| `include_saml_users` | boolean | yes (see description) | Include users with a SAML identity. Either this value or `include_service_accounts` must be `true`. |
| `include_service_accounts` | boolean | yes (see description) | Include service account users. Either this value or `include_saml_users` must be `true`. |
| `search` | string | no | Search users by name, email, username. |
If successful, returns [`200 OK`](rest/troubleshooting.md#status-codes) and the
following response attributes:
Example response:
```json
[
{
"id": 66,
"username": "user22",
"name": "John Doe22",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/xxx?s=80&d=identicon",
"web_url": "http://my.gitlab.com/user22",
"created_at": "2021-09-10T12:48:22.381Z",
"bio": "",
"location": null,
"public_email": "",
"linkedin": "",
"twitter": "",
"website_url": "",
"organization": null,
"job_title": "",
"pronouns": null,
"bot": false,
"work_information": null,
"followers": 0,
"following": 0,
"local_time": null,
"last_sign_in_at": null,
"confirmed_at": "2021-09-10T12:48:22.330Z",
"last_activity_on": null,
"email": "user22@example.org",
"theme_id": 1,
"color_scheme_id": 1,
"projects_limit": 100000,
"current_sign_in_at": null,
"identities": [ ],
"can_create_group": true,
"can_create_project": true,
"two_factor_enabled": false,
"external": false,
"private_profile": false,
"commit_email": "user22@example.org",
"shared_runners_minutes_limit": null,
"extra_shared_runners_minutes_limit": null
},
...
]
```
### List subgroups
Get a list of visible direct subgroups in this group.

View File

@ -2418,68 +2418,6 @@ paths:
tags:
- groups
operationId: getApiV4GroupsIdProvisionedUsers
"/api/v4/groups/{id}/users":
get:
description: Get a list of users for the group
produces:
- application/json
parameters:
- in: query
name: search
description: Search users by name, email or username
type: string
required: false
- in: query
name: active
description: Filters only active users
type: boolean
default: false
required: false
- in: query
name: include_saml_users
description: Return users with a SAML identity in this group
type: boolean
required: false
- in: query
name: include_service_accounts
description: Return service accounts owned by this group
type: boolean
required: false
- in: query
name: page
description: Current page number
type: integer
format: int32
default: 1
required: false
example: 1
- in: query
name: per_page
description: Number of items per page
type: integer
format: int32
default: 20
required: false
example: 20
- in: path
name: id
type: integer
format: int32
required: true
responses:
'200':
description: Get a list of users for the group
schema:
"$ref": "#/definitions/API_Entities_UserPublic"
'400':
description: Bad request
'403':
description: Forbidden
'404':
description: 404 Not Found
tags:
- groups
operationId: getApiV4GroupsIdUsers
"/api/v4/groups/{id}/ssh_certificates":
get:
summary: Get a list of Groups::SshCertificate for a Group.

View File

@ -277,6 +277,8 @@ module Gitlab
strategy.increment(cache_key, expiry)
end
return false if value.nil?
report_metrics(key, value, threshold_value, peek)
value > threshold_value

View File

@ -17,7 +17,7 @@ module Gitlab
private
def with_redis(&block)
::Gitlab::Redis::RateLimiting.with(&block) # rubocop: disable CodeReuse/ActiveRecord
::Gitlab::Redis::RateLimiting.with_suppressed_errors(&block)
end
end
end

View File

@ -40,11 +40,7 @@ module Gitlab
private
def with(&block)
Gitlab::Redis::RateLimiting.with(&block)
rescue ::Redis::BaseConnectionError
# Do not raise an error if we cannot connect to Redis. If
# Redis::RateLimiting is unavailable it should not take the site down.
nil
Gitlab::Redis::RateLimiting.with_suppressed_errors(&block)
end
end
end

View File

@ -38,15 +38,7 @@ module Gitlab
private
def with(&block)
# rubocop: disable CodeReuse/ActiveRecord
Gitlab::Redis::RateLimiting.with(&block)
# rubocop: enable CodeReuse/ActiveRecord
rescue ::Redis::BaseConnectionError
# Following the example of
# https://github.com/rack/rack-attack/blob/v6.6.1/lib/rack/attack/store_proxy/redis_proxy.rb#L61-L65,
# do not raise an error if we cannot connect to Redis. If
# Redis::RateLimiting is unavailable it should not take the site down.
nil
Gitlab::Redis::RateLimiting.with_suppressed_errors(&block)
end
def namespace(key)

View File

@ -8,6 +8,13 @@ module Gitlab
def config_fallback
Cache
end
# Rescue Redis errors so we do not take the site down when the rate limiting instance is down
def with_suppressed_errors(&block)
with(&block)
rescue ::Redis::BaseError, ::RedisClient::Error => e
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
end
end
end
end

View File

@ -9101,9 +9101,6 @@ msgstr ""
msgid "At least one field of %{one_of_required_fields} must be present"
msgstr ""
msgid "At least one of %{params} must be true"
msgstr ""
msgid "At least one of group_id or project_id must be specified"
msgstr ""

View File

@ -28,6 +28,21 @@ RSpec.describe Gitlab::ApplicationRateLimiter, :clean_gitlab_redis_rate_limiting
end
describe '.throttled?' do
context 'when redis is unavailable' do
before do
broken_redis = Redis.new(
url: 'redis://127.0.0.0:0',
custom: { instrumentation_class: Gitlab::Redis::RateLimiting.instrumentation_class }
)
allow(Gitlab::Redis::RateLimiting).to receive(:with).and_yield(broken_redis)
allow(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
end
it 'returns false' do
expect(subject.throttled?(:test_action, scope: [user])).to eq(false)
end
end
context 'when the key is invalid' do
context 'is provided as a Symbol' do
context 'but is not defined in the rate_limits Hash' do

View File

@ -9,6 +9,8 @@ RSpec.describe Gitlab::CircuitBreaker::Store, :clean_gitlab_redis_rate_limiting,
shared_examples 'reliable circuit breaker store method' do
it 'does not raise an error when Redis::BaseConnectionError is encountered' do
allow(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
allow(Gitlab::Redis::RateLimiting)
.to receive(:with)
.and_raise(Redis::BaseConnectionError)

View File

@ -105,6 +105,7 @@ RSpec.describe Gitlab::RackAttack::Store, :clean_gitlab_redis_rate_limiting, fea
custom: { instrumentation_class: Gitlab::Redis::RateLimiting.instrumentation_class }
)
allow(Gitlab::Redis::RateLimiting).to receive(:with).and_yield(broken_redis)
allow(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
end
it { expect(subject).to eq(nil) }

View File

@ -4,4 +4,57 @@ require 'spec_helper'
RSpec.describe Gitlab::Redis::RateLimiting do
include_examples "redis_new_instance_shared_examples", 'rate_limiting', Gitlab::Redis::Cache
describe '.with_suppressed_errors' do
subject(:ping) { described_class.with_suppressed_errors(&:ping) }
before do
allow(described_class).to receive(:with).and_yield(redis)
end
context 'when using Redis' do
let(:redis) { described_class.send(:init_redis, { url: 'redis://127.0.0.0:0' }) }
it 'tracks the error and returns nil' do
expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
.with(a_kind_of(::Redis::CannotConnectError))
expect(ping).to be_nil
end
end
context 'when using RedisCluster' do
let(:redis) do
described_class.send(:init_redis, {
nodes: [
{ host: '127.0.0.0', port: 0, db: 1 },
{ host: '127.0.0.0', port: 0, db: 2 },
{ host: '127.0.0.0', port: 0, db: 3 }
]
})
end
it 'tracks the error and returns nil' do
expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
.with(a_kind_of(::Redis::Cluster::InitialSetupError))
expect(ping).to be_nil
end
end
context 'with a RedisClient exception' do
let(:redis) { instance_double(Redis) }
before do
allow(redis).to receive(:ping).and_raise(::RedisClient::ReadTimeoutError)
end
it 'tracks the error and returns nil' do
expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
.with(a_kind_of(::RedisClient::ReadTimeoutError))
expect(ping).to be_nil
end
end
end
end

View File

@ -50,9 +50,78 @@ RSpec.shared_examples 'it has loose foreign keys' do
model = create(factory_name) # rubocop: disable Rails/SaveBang
model_id = model.id
puts "## LFK Debug: Starting test for #{table_name} with model_id: #{model_id} ##"
# Check initial state
initial_pending_count = deleted_records.status_pending.count
initial_processed_count = deleted_records.status_processed.count
initial_total_count = deleted_records.count
puts "## LFK Debug: Initial state - Pending: #{initial_pending_count}, Processed: #{initial_processed_count}, Total: #{initial_total_count} ##"
# Check for existing records for this model_id (shouldn't exist)
existing_records = deleted_records.where(primary_key_value: model_id)
puts "## LFK Debug: Existing records for model_id #{model_id}: #{existing_records.count} ##"
expect { model.delete }.to change { deleted_records.count }.by(1)
LooseForeignKeys::ProcessDeletedRecordsService.new(connection: connection).execute
# Check state after deletion
after_delete_pending = deleted_records.where(primary_key_value: model_id).status_pending.count
after_delete_processed = deleted_records.where(primary_key_value: model_id).status_processed.count
total_pending_after_delete = deleted_records.status_pending.count
total_processed_after_delete = deleted_records.status_processed.count
puts "## LFK Debug: After delete - Model #{model_id} Pending: #{after_delete_pending}, Processed: #{after_delete_processed} ##"
puts "## LFK Debug: After delete - Total Pending: #{total_pending_after_delete}, Total Processed: #{total_processed_after_delete} ##"
# Check all pending records across all tables before processing
all_pending_records = []
Gitlab::Database::LooseForeignKeys.definitions_by_table.each_key do |table|
fqtn = "#{connection.current_schema}.#{table}"
table_records = LooseForeignKeys::DeletedRecord.where(fully_qualified_table_name: fqtn).status_pending
all_pending_records.concat(table_records.to_a)
end
puts "## LFK Debug: Total pending records across all tables before processing: #{all_pending_records.count} ##"
puts "## LFK Debug: Tables with pending records: #{all_pending_records.group_by(&:fully_qualified_table_name).transform_values(&:count)} ##"
# Process records and capture stats
start_time = Time.current
service = LooseForeignKeys::ProcessDeletedRecordsService.new(connection: connection)
stats = service.execute
processing_time = Time.current - start_time
puts "## LFK Debug: Processing took #{processing_time} seconds ##"
puts "## LFK Debug: Processing stats: #{stats.inspect} ##"
# Check state after processing
after_process_pending = deleted_records.where(primary_key_value: model_id).status_pending.count
after_process_processed = deleted_records.where(primary_key_value: model_id).status_processed.count
total_pending_after_process = deleted_records.status_pending.count
total_processed_after_process = deleted_records.status_processed.count
puts "## LFK Debug: After processing - Model #{model_id} Pending: #{after_process_pending}, Processed: #{after_process_processed} ##"
puts "## LFK Debug: After processing - Total Pending: #{total_pending_after_process}, Total Processed: #{total_processed_after_process} ##"
# If the test is about to fail, provide additional debugging
if after_process_pending != 0 || after_process_processed != 1
puts "## LFK Debug: TEST FAILURE IMMINENT - Additional debugging ##"
# Check if our specific record exists and its status
our_record = deleted_records.find_by(primary_key_value: model_id)
if our_record
puts "## LFK Debug: Our record exists - Status: #{our_record.status}, Cleanup attempts: #{our_record.cleanup_attempts}, Consume after: #{our_record.consume_after} ##"
puts "## LFK Debug: Our record details: #{our_record.inspect} ##"
else
puts "## LFK Debug: Our record not found in deleted_records table ##"
end
# Check if there are still pending records for our table
table_pending = deleted_records.status_pending
puts "## LFK Debug: Remaining pending records for #{table_name}: #{table_pending.count} ##"
puts "## LFK Debug: Pending record IDs: #{table_pending.pluck(:primary_key_value)} ##" if table_pending.any?
end
expect(deleted_records.where(primary_key_value: model_id).status_pending.count).to eq(0)
expect(deleted_records.where(primary_key_value: model_id).status_processed.count).to eq(1)