mirror of
https://gitlab.com/gitlab-org/gitlab-foss.git
synced 2025-07-21 23:43:41 +00:00
Update from merge request
This commit is contained in:
@ -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'
|
||||
|
@ -1 +1 @@
|
||||
47335750c521ec469a5074962fb28b48b9dc06a6
|
||||
bacb63191f1ba77f6be796e72e9b5bff68fde31d
|
||||
|
@ -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>
|
||||
|
@ -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 }">
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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 ""
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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) }
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user