Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
c568cb4dbc
commit
d089b5729e
40 changed files with 687 additions and 1867 deletions
|
@ -119,7 +119,6 @@ overrides:
|
|||
- '@graphql-eslint'
|
||||
parserOptions:
|
||||
parser: '@graphql-eslint/eslint-plugin'
|
||||
schema: './tmp/tests/graphql/gitlab_schema_apollo.graphql'
|
||||
operations:
|
||||
- '{,ee/,jh/}app/**/*.graphql'
|
||||
# You can run `bundle exec rake gitlab:graphql:schema:dump` and then uncomment this line
|
||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -95,7 +95,7 @@ jsdoc/
|
|||
webpack-dev-server.json
|
||||
/.nvimrc
|
||||
.solargraph.yml
|
||||
./apollo.config.js
|
||||
apollo.config.js
|
||||
/tmp/matching_foss_tests.txt
|
||||
/tmp/matching_tests.txt
|
||||
ee/changelogs/unreleased-ee
|
||||
|
|
|
@ -51,18 +51,13 @@ eslint:
|
|||
extends:
|
||||
- .static-analysis-base
|
||||
- .yarn-cache
|
||||
- .frontend:rules:default-frontend-jobs
|
||||
needs: ['graphql-schema-dump']
|
||||
- .static-analysis:rules:ee
|
||||
needs: []
|
||||
variables:
|
||||
USE_BUNDLE_INSTALL: "false"
|
||||
script:
|
||||
- run_timed_command "retry yarn install --frozen-lockfile"
|
||||
- yarn run apollo client:download-schema --config=config/apollo.config.js tmp/tests/graphql/gitlab_schema_apollo.graphql
|
||||
- run_timed_command "yarn run lint:eslint:all"
|
||||
artifacts:
|
||||
name: graphql-schema-apollo
|
||||
paths:
|
||||
- tmp/tests/graphql/gitlab_schema_apollo.graphql
|
||||
|
||||
eslint as-if-foss:
|
||||
extends:
|
||||
|
|
|
@ -176,7 +176,6 @@ Rails/TimeZone:
|
|||
- 'ee/spec/lib/gitlab/geo/log_cursor/events/repository_renamed_event_spec.rb'
|
||||
- 'ee/spec/lib/gitlab/geo/log_cursor/events/repository_updated_event_spec.rb'
|
||||
- 'ee/spec/lib/gitlab/geo/log_cursor/events/reset_checksum_event_spec.rb'
|
||||
- 'ee/spec/lib/gitlab/geo/log_cursor/events/upload_deleted_event_spec.rb'
|
||||
- 'ee/spec/lib/gitlab/geo/log_cursor/logger_spec.rb'
|
||||
- 'ee/spec/lib/gitlab/git_access_spec.rb'
|
||||
- 'ee/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb'
|
||||
|
|
|
@ -1,7 +1,71 @@
|
|||
<script>
|
||||
export default {};
|
||||
import { GlLoadingIcon, GlTable } from '@gitlab/ui';
|
||||
import createFlash from '~/flash';
|
||||
import { s__, __ } from '~/locale';
|
||||
import getGroupOrganizationsQuery from './queries/get_group_organizations.query.graphql';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlLoadingIcon,
|
||||
GlTable,
|
||||
},
|
||||
inject: ['groupFullPath'],
|
||||
data() {
|
||||
return { organizations: [] };
|
||||
},
|
||||
apollo: {
|
||||
organizations: {
|
||||
query() {
|
||||
return getGroupOrganizationsQuery;
|
||||
},
|
||||
variables() {
|
||||
return {
|
||||
groupFullPath: this.groupFullPath,
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
return this.extractOrganizations(data);
|
||||
},
|
||||
error(error) {
|
||||
createFlash({
|
||||
message: __('Something went wrong. Please try again.'),
|
||||
error,
|
||||
captureError: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isLoading() {
|
||||
return this.$apollo.queries.organizations.loading;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
extractOrganizations(data) {
|
||||
const organizations = data?.group?.organizations?.nodes || [];
|
||||
return organizations.slice().sort((a, b) => a.name.localeCompare(b.name));
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{ key: 'name', sortable: true },
|
||||
{ key: 'defaultRate', sortable: true },
|
||||
{ key: 'description', sortable: true },
|
||||
],
|
||||
i18n: {
|
||||
emptyText: s__('Crm|No organizations found'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div></div>
|
||||
<div>
|
||||
<gl-loading-icon v-if="isLoading" class="gl-mt-5" size="lg" />
|
||||
<gl-table
|
||||
v-else
|
||||
:items="organizations"
|
||||
:fields="$options.fields"
|
||||
:empty-text="$options.i18n.emptyText"
|
||||
show-empty
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
query organizations($groupFullPath: ID!) {
|
||||
group(fullPath: $groupFullPath) {
|
||||
__typename
|
||||
id
|
||||
organizations {
|
||||
nodes {
|
||||
__typename
|
||||
id
|
||||
name
|
||||
defaultRate
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +1,25 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import CrmOrganizationsRoot from './components/organizations_root.vue';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
export default () => {
|
||||
const el = document.getElementById('js-crm-organizations-app');
|
||||
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: createDefaultClient(),
|
||||
});
|
||||
|
||||
if (!el) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
apolloProvider,
|
||||
provide: { groupFullPath: el.dataset.groupFullPath },
|
||||
render(createElement) {
|
||||
return createElement(CrmOrganizationsRoot);
|
||||
},
|
||||
|
|
|
@ -63,9 +63,9 @@
|
|||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"if": {
|
||||
"type": "string"
|
||||
},
|
||||
"if": { "$ref": "#/definitions/if" },
|
||||
"changes": { "$ref": "#/definitions/changes" },
|
||||
"exists": { "$ref": "#/definitions/exists" },
|
||||
"variables": { "$ref": "#/definitions/variables" },
|
||||
"when": {
|
||||
"type": "string",
|
||||
|
@ -497,24 +497,9 @@
|
|||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"if": {
|
||||
"type": "string",
|
||||
"description": "Expression to evaluate whether additional attributes should be provided to the job"
|
||||
},
|
||||
"changes": {
|
||||
"type": "array",
|
||||
"description": "Additional attributes will be provided to job if any of the provided paths matches a modified file",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"exists": {
|
||||
"type": "array",
|
||||
"description": "Additional attributes will be provided to job if any of the provided paths matches an existing file in the repository",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"if": { "$ref": "#/definitions/if" },
|
||||
"changes": { "$ref": "#/definitions/changes" },
|
||||
"exists": { "$ref": "#/definitions/exists" },
|
||||
"variables": { "$ref": "#/definitions/variables" },
|
||||
"when": { "$ref": "#/definitions/when" },
|
||||
"start_in": { "$ref": "#/definitions/start_in" },
|
||||
|
@ -541,6 +526,24 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"if": {
|
||||
"type": "string",
|
||||
"description": "Expression to evaluate whether additional attributes should be provided to the job"
|
||||
},
|
||||
"changes": {
|
||||
"type": "array",
|
||||
"description": "Additional attributes will be provided to job if any of the provided paths matches a modified file",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"exists": {
|
||||
"type": "array",
|
||||
"description": "Additional attributes will be provided to job if any of the provided paths matches an existing file in the repository",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"variables": {
|
||||
"type": "object",
|
||||
"description": "Defines environment variables for specific jobs. Job level property overrides global variables. If a job sets `variables: {}`, all global variables are turned off.",
|
||||
|
|
|
@ -20,7 +20,7 @@ module Resolvers
|
|||
description: 'Filter by permissions the user has on groups.'
|
||||
|
||||
before_connection_authorization do |nodes, current_user|
|
||||
Preloaders::UserMaxAccessLevelInGroupsPreloader.new(nodes, current_user).execute
|
||||
Preloaders::GroupPolicyPreloader.new(nodes, current_user).execute
|
||||
end
|
||||
|
||||
def ready?(**args)
|
||||
|
|
|
@ -30,6 +30,8 @@ class AuditEvent < ApplicationRecord
|
|||
scope :by_entity_type, -> (entity_type) { where(entity_type: entity_type) }
|
||||
scope :by_entity_id, -> (entity_id) { where(entity_id: entity_id) }
|
||||
scope :by_author_id, -> (author_id) { where(author_id: author_id) }
|
||||
scope :by_entity_username, -> (username) { where(entity_id: find_user_id(username)) }
|
||||
scope :by_author_username, -> (username) { where(author_id: find_user_id(username)) }
|
||||
|
||||
after_initialize :initialize_details
|
||||
|
||||
|
@ -106,6 +108,10 @@ class AuditEvent < ApplicationRecord
|
|||
self[name] = self.details[name] = original
|
||||
end
|
||||
end
|
||||
|
||||
def self.find_user_id(username)
|
||||
User.find_by_username(username)&.id
|
||||
end
|
||||
end
|
||||
|
||||
AuditEvent.prepend_mod_with('AuditEvent')
|
||||
|
|
23
app/models/preloaders/group_policy_preloader.rb
Normal file
23
app/models/preloaders/group_policy_preloader.rb
Normal file
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Preloaders
|
||||
class GroupPolicyPreloader
|
||||
def initialize(groups, current_user)
|
||||
@groups = groups
|
||||
@current_user = current_user
|
||||
end
|
||||
|
||||
def execute
|
||||
Preloaders::UserMaxAccessLevelInGroupsPreloader.new(@groups, @current_user).execute
|
||||
Preloaders::GroupRootAncestorPreloader.new(@groups, root_ancestor_preloads).execute
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def root_ancestor_preloads
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Preloaders::GroupPolicyPreloader.prepend_mod_with('Preloaders::GroupPolicyPreloader')
|
32
app/models/preloaders/group_root_ancestor_preloader.rb
Normal file
32
app/models/preloaders/group_root_ancestor_preloader.rb
Normal file
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Preloaders
|
||||
class GroupRootAncestorPreloader
|
||||
def initialize(groups, root_ancestor_preloads = [])
|
||||
@groups = groups
|
||||
@root_ancestor_preloads = root_ancestor_preloads
|
||||
end
|
||||
|
||||
def execute
|
||||
return unless ::Feature.enabled?(:use_traversal_ids, default_enabled: :yaml)
|
||||
|
||||
# type == 'Group' condition located on subquery to prevent a filter in the query
|
||||
root_query = Namespace.joins("INNER JOIN (#{join_sql}) as root_query ON root_query.root_id = namespaces.id")
|
||||
.select('namespaces.*, root_query.id as source_id')
|
||||
|
||||
root_query = root_query.preload(*@root_ancestor_preloads) if @root_ancestor_preloads.any?
|
||||
|
||||
root_ancestors_by_id = root_query.group_by(&:source_id)
|
||||
|
||||
@groups.each do |group|
|
||||
group.root_ancestor = root_ancestors_by_id[group.id].first
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def join_sql
|
||||
Group.select('id, traversal_ids[1] as root_id').where(id: @groups.map(&:id)).to_sql
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,7 +3,6 @@
|
|||
module Preloaders
|
||||
# This class preloads the max access level (role) for the user within the given groups and
|
||||
# stores the values in requests store.
|
||||
# Will only be able to preload max access level for groups where the user is a direct member
|
||||
class UserMaxAccessLevelInGroupsPreloader
|
||||
include BulkMemberAccessLoad
|
||||
|
||||
|
@ -13,8 +12,17 @@ module Preloaders
|
|||
end
|
||||
|
||||
def execute
|
||||
if ::Feature.enabled?(:use_traversal_ids, default_enabled: :yaml)
|
||||
preload_with_traversal_ids
|
||||
else
|
||||
preload_direct_memberships
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def preload_direct_memberships
|
||||
group_memberships = GroupMember.active_without_invites_and_requests
|
||||
.non_minimal_access
|
||||
.where(user: @user, source_id: @groups)
|
||||
.group(:source_id)
|
||||
.maximum(:access_level)
|
||||
|
@ -23,5 +31,22 @@ module Preloaders
|
|||
merge_value_to_request_store(User, @user.id, group_id, max_access_level)
|
||||
end
|
||||
end
|
||||
|
||||
def preload_with_traversal_ids
|
||||
max_access_levels = GroupMember.active_without_invites_and_requests
|
||||
.where(user: @user)
|
||||
.joins("INNER JOIN (#{traversal_join_sql}) as hierarchy ON members.source_id = hierarchy.traversal_id")
|
||||
.group('hierarchy.id')
|
||||
.maximum(:access_level)
|
||||
|
||||
@groups.each do |group|
|
||||
max_access_level = max_access_levels[group.id] || Gitlab::Access::NO_ACCESS
|
||||
merge_value_to_request_store(User, @user.id, group.id, max_access_level)
|
||||
end
|
||||
end
|
||||
|
||||
def traversal_join_sql
|
||||
Namespace.select('id, unnest(traversal_ids) as traversal_id').where(id: @groups.map(&:id)).to_sql
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
- breadcrumb_title _('Customer Relations Organizations')
|
||||
- page_title _('Customer Relations Organizations')
|
||||
|
||||
#js-crm-organizations-app
|
||||
#js-crm-organizations-app{ data: { group_full_path: @group.full_path } }
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
module.exports = {
|
||||
client: {
|
||||
service: {
|
||||
name: 'gitlab',
|
||||
localSchemaFile: './tmp/tests/graphql/gitlab_schema.graphql',
|
||||
},
|
||||
includes: ['../{ee/,jh/,}app/assets/javascripts/**/*.{js,graphql}'],
|
||||
excludes: ['../{ee/,jh/,}spec/{frontend,frontend_integration}/**/*'],
|
||||
},
|
||||
};
|
|
@ -256,8 +256,8 @@
|
|||
:when: 2019-09-11 13:08:28.431132000 Z
|
||||
- - :permit
|
||||
- "(MIT OR CC0-1.0)"
|
||||
- :who:
|
||||
:why:
|
||||
- :who:
|
||||
:why:
|
||||
:versions: []
|
||||
:when: 2019-11-08 10:03:31.787226000 Z
|
||||
- - :permit
|
||||
|
@ -362,9 +362,3 @@
|
|||
- - :approve
|
||||
- 0.0.62
|
||||
- *2
|
||||
- - :approve
|
||||
- sha.js
|
||||
- :who: Vitaly Slobodin
|
||||
:why: Dual license BSD-3-Clause and MIT. See https://gitlab.com/gitlab-com/legal-and-compliance/-/issues/686
|
||||
:versions: []
|
||||
:when: 2021-11-12 18:08:27.036400090 Z
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveGeoUploadDeprecatedFields < Gitlab::Database::Migration[1.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
remove_column :geo_event_log, :upload_deleted_event_id, :bigint
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
add_column(:geo_event_log, :upload_deleted_event_id, :bigint) unless column_exists?(:geo_event_log, :upload_deleted_event_id)
|
||||
end
|
||||
|
||||
add_concurrent_foreign_key :geo_event_log, :geo_upload_deleted_events,
|
||||
column: :upload_deleted_event_id,
|
||||
name: 'fk_c1f241c70d',
|
||||
on_delete: :cascade
|
||||
|
||||
add_concurrent_index :geo_event_log,
|
||||
:upload_deleted_event_id,
|
||||
name: 'index_geo_event_log_on_upload_deleted_event_id',
|
||||
where: "(upload_deleted_event_id IS NOT NULL)"
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DropGeoUploadDeletedEventsTable < Gitlab::Database::Migration[1.0]
|
||||
def up
|
||||
drop_table :geo_upload_deleted_events
|
||||
end
|
||||
|
||||
def down
|
||||
create_table :geo_upload_deleted_events, id: :bigserial do |t|
|
||||
t.integer :upload_id, null: false, index: true
|
||||
t.string :file_path, null: false
|
||||
t.integer :model_id, null: false
|
||||
t.string :model_type, null: false
|
||||
t.string :uploader, null: false
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveOutdatedFieldsFromGeoNodeStatus < Gitlab::Database::Migration[1.0]
|
||||
enable_lock_retries!
|
||||
|
||||
def change
|
||||
remove_column :geo_node_statuses, :attachments_count, :integer
|
||||
remove_column :geo_node_statuses, :attachments_synced_count, :integer
|
||||
remove_column :geo_node_statuses, :attachments_failed_count, :integer
|
||||
remove_column :geo_node_statuses, :attachments_synced_missing_on_primary_count, :integer
|
||||
end
|
||||
end
|
1
db/schema_migrations/20211021140426
Normal file
1
db/schema_migrations/20211021140426
Normal file
|
@ -0,0 +1 @@
|
|||
bc7974917509bfbda47375299009295bc5a55970b92443dd5d7134075b161279
|
1
db/schema_migrations/20211021141930
Normal file
1
db/schema_migrations/20211021141930
Normal file
|
@ -0,0 +1 @@
|
|||
483e4cbe2a0be2afbda511f2298e3715abaca29afafeeae26449fc862f49a08f
|
1
db/schema_migrations/20211022160154
Normal file
1
db/schema_migrations/20211022160154
Normal file
|
@ -0,0 +1 @@
|
|||
c474870a626c909da772a1c9f459f369d50658ce8f585a35e7cc3c7ef64af657
|
|
@ -14170,7 +14170,6 @@ CREATE TABLE geo_event_log (
|
|||
hashed_storage_migrated_event_id bigint,
|
||||
lfs_object_deleted_event_id bigint,
|
||||
hashed_storage_attachments_event_id bigint,
|
||||
upload_deleted_event_id bigint,
|
||||
job_artifact_deleted_event_id bigint,
|
||||
reset_checksum_event_id bigint,
|
||||
cache_invalidation_event_id bigint,
|
||||
|
@ -14300,9 +14299,6 @@ CREATE TABLE geo_node_statuses (
|
|||
lfs_objects_count integer,
|
||||
lfs_objects_synced_count integer,
|
||||
lfs_objects_failed_count integer,
|
||||
attachments_count integer,
|
||||
attachments_synced_count integer,
|
||||
attachments_failed_count integer,
|
||||
last_event_id integer,
|
||||
last_event_date timestamp without time zone,
|
||||
cursor_last_event_id integer,
|
||||
|
@ -14327,7 +14323,6 @@ CREATE TABLE geo_node_statuses (
|
|||
wikis_verification_failed_count integer,
|
||||
lfs_objects_synced_missing_on_primary_count integer,
|
||||
job_artifacts_synced_missing_on_primary_count integer,
|
||||
attachments_synced_missing_on_primary_count integer,
|
||||
repositories_checksummed_count integer,
|
||||
repositories_checksum_failed_count integer,
|
||||
repositories_checksum_mismatch_count integer,
|
||||
|
@ -14496,24 +14491,6 @@ CREATE SEQUENCE geo_reset_checksum_events_id_seq
|
|||
|
||||
ALTER SEQUENCE geo_reset_checksum_events_id_seq OWNED BY geo_reset_checksum_events.id;
|
||||
|
||||
CREATE TABLE geo_upload_deleted_events (
|
||||
id bigint NOT NULL,
|
||||
upload_id integer NOT NULL,
|
||||
file_path character varying NOT NULL,
|
||||
model_id integer NOT NULL,
|
||||
model_type character varying NOT NULL,
|
||||
uploader character varying NOT NULL
|
||||
);
|
||||
|
||||
CREATE SEQUENCE geo_upload_deleted_events_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE geo_upload_deleted_events_id_seq OWNED BY geo_upload_deleted_events.id;
|
||||
|
||||
CREATE TABLE gitlab_subscription_histories (
|
||||
id bigint NOT NULL,
|
||||
gitlab_subscription_created_at timestamp with time zone,
|
||||
|
@ -21510,8 +21487,6 @@ ALTER TABLE ONLY geo_repository_updated_events ALTER COLUMN id SET DEFAULT nextv
|
|||
|
||||
ALTER TABLE ONLY geo_reset_checksum_events ALTER COLUMN id SET DEFAULT nextval('geo_reset_checksum_events_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY geo_upload_deleted_events ALTER COLUMN id SET DEFAULT nextval('geo_upload_deleted_events_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY gitlab_subscription_histories ALTER COLUMN id SET DEFAULT nextval('gitlab_subscription_histories_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY gitlab_subscriptions ALTER COLUMN id SET DEFAULT nextval('gitlab_subscriptions_id_seq'::regclass);
|
||||
|
@ -23129,9 +23104,6 @@ ALTER TABLE ONLY geo_repository_updated_events
|
|||
ALTER TABLE ONLY geo_reset_checksum_events
|
||||
ADD CONSTRAINT geo_reset_checksum_events_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY geo_upload_deleted_events
|
||||
ADD CONSTRAINT geo_upload_deleted_events_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY gitlab_subscription_histories
|
||||
ADD CONSTRAINT gitlab_subscription_histories_pkey PRIMARY KEY (id);
|
||||
|
||||
|
@ -25950,8 +25922,6 @@ CREATE INDEX index_geo_event_log_on_repository_updated_event_id ON geo_event_log
|
|||
|
||||
CREATE INDEX index_geo_event_log_on_reset_checksum_event_id ON geo_event_log USING btree (reset_checksum_event_id) WHERE (reset_checksum_event_id IS NOT NULL);
|
||||
|
||||
CREATE INDEX index_geo_event_log_on_upload_deleted_event_id ON geo_event_log USING btree (upload_deleted_event_id) WHERE (upload_deleted_event_id IS NOT NULL);
|
||||
|
||||
CREATE INDEX index_geo_hashed_storage_attachments_events_on_project_id ON geo_hashed_storage_attachments_events USING btree (project_id);
|
||||
|
||||
CREATE INDEX index_geo_hashed_storage_migrated_events_on_project_id ON geo_hashed_storage_migrated_events USING btree (project_id);
|
||||
|
@ -25988,8 +25958,6 @@ CREATE INDEX index_geo_repository_updated_events_on_source ON geo_repository_upd
|
|||
|
||||
CREATE INDEX index_geo_reset_checksum_events_on_project_id ON geo_reset_checksum_events USING btree (project_id);
|
||||
|
||||
CREATE INDEX index_geo_upload_deleted_events_on_upload_id ON geo_upload_deleted_events USING btree (upload_id);
|
||||
|
||||
CREATE INDEX index_gin_ci_pending_builds_on_namespace_traversal_ids ON ci_pending_builds USING gin (namespace_traversal_ids);
|
||||
|
||||
CREATE INDEX index_gitlab_subscription_histories_on_gitlab_subscription_id ON gitlab_subscription_histories USING btree (gitlab_subscription_id);
|
||||
|
@ -29352,9 +29320,6 @@ ALTER TABLE ONLY design_management_versions
|
|||
ALTER TABLE ONLY packages_packages
|
||||
ADD CONSTRAINT fk_c188f0dba4 FOREIGN KEY (creator_id) REFERENCES users(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY geo_event_log
|
||||
ADD CONSTRAINT fk_c1f241c70d FOREIGN KEY (upload_deleted_event_id) REFERENCES geo_upload_deleted_events(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY analytics_cycle_analytics_project_stages
|
||||
ADD CONSTRAINT fk_c3339bdfc9 FOREIGN KEY (stage_event_hash_id) REFERENCES analytics_cycle_analytics_stage_event_hashes(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
@ -190,9 +190,6 @@ configuration option in `gitlab.yml`. These metrics are served from the
|
|||
| `geo_lfs_objects` | Gauge | 10.2 | Total number of LFS objects available on primary | `url` |
|
||||
| `geo_lfs_objects_synced` | Gauge | 10.2 | Number of LFS objects synced on secondary | `url` |
|
||||
| `geo_lfs_objects_failed` | Gauge | 10.2 | Number of LFS objects failed to sync on secondary | `url` |
|
||||
| `geo_attachments` | Gauge | 10.2 | Total number of file attachments available on primary | `url` |
|
||||
| `geo_attachments_synced` | Gauge | 10.2 | Number of attachments synced on secondary | `url` |
|
||||
| `geo_attachments_failed` | Gauge | 10.2 | Number of attachments failed to sync on secondary | `url` |
|
||||
| `geo_last_event_id` | Gauge | 10.2 | Database ID of the latest event log entry on the primary | `url` |
|
||||
| `geo_last_event_timestamp` | Gauge | 10.2 | UNIX timestamp of the latest event log entry on the primary | `url` |
|
||||
| `geo_cursor_last_event_id` | Gauge | 10.2 | Last database ID of the event log processed by the secondary | `url` |
|
||||
|
@ -201,7 +198,6 @@ configuration option in `gitlab.yml`. These metrics are served from the
|
|||
| `geo_last_successful_status_check_timestamp` | Gauge | 10.2 | Last timestamp when the status was successfully updated | `url` |
|
||||
| `geo_lfs_objects_synced_missing_on_primary` | Gauge | 10.7 | Number of LFS objects marked as synced due to the file missing on the primary | `url` |
|
||||
| `geo_job_artifacts_synced_missing_on_primary` | Gauge | 10.7 | Number of job artifacts marked as synced due to the file missing on the primary | `url` |
|
||||
| `geo_attachments_synced_missing_on_primary` | Gauge | 10.7 | Number of attachments marked as synced due to the file missing on the primary | `url` |
|
||||
| `geo_repositories_checksummed` | Gauge | 10.7 | Number of repositories checksummed on primary | `url` |
|
||||
| `geo_repositories_checksum_failed` | Gauge | 10.7 | Number of repositories failed to calculate the checksum on primary | `url` |
|
||||
| `geo_wikis_checksummed` | Gauge | 10.7 | Number of wikis checksummed on primary | `url` |
|
||||
|
|
|
@ -259,7 +259,8 @@ control over how the Pages daemon runs and serves content in your environment.
|
|||
| `FF_ENABLE_REDIRECTS` | Feature flag to enable/disable redirects (enabled by default). Read the [redirects documentation](../../user/project/pages/redirects.md#feature-flag-for-redirects) for more information. |
|
||||
| `FF_ENABLE_PLACEHOLDERS` | Feature flag to enable/disable rewrites (disabled by default). Read the [redirects documentation](../../user/project/pages/redirects.md#feature-flag-for-rewrites) for more information. |
|
||||
| `use_legacy_storage` | Temporarily-introduced parameter allowing to use legacy domain configuration source and storage. [Removed in 14.3](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/6166). |
|
||||
|
||||
| `rate_limit_source_ip` | Rate limit per source IP in number of requests per second. Set to `0` to disable this feature. |
|
||||
| `rate_limit_source_ip_burst` | Rate limit per source IP maximum burst allowed per second. |
|
||||
---
|
||||
|
||||
## Advanced configuration
|
||||
|
@ -1032,6 +1033,38 @@ GitLab Pages are part of the [regular backup](../../raketasks/backup_restore.md)
|
|||
You should strongly consider running GitLab Pages under a different hostname
|
||||
than GitLab to prevent XSS attacks.
|
||||
|
||||
### Rate limits
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab-pages/-/issues/631) in GitLab 14.5.
|
||||
|
||||
You can enforce source-IP rate limits to help minimize the risk of a Denial of Service (DoS) attack. GitLab Pages
|
||||
uses a [token bucket algorithm](https://en.wikipedia.org/wiki/Token_bucket) to enforce rate limiting. By default,
|
||||
requests that exceed the specified limits are reported but not rejected.
|
||||
|
||||
Source-IP rate limits are enforced using the following:
|
||||
|
||||
- `rate_limit_source_ip`: Set the maximum threshold in number of requests per second. Set to 0 to disable this feature.
|
||||
- `rate_limit_source_ip_burst`: Sets the maximum threshold of number of requests allowed in an initial outburst of requests.
|
||||
For example, when you load a web page that loads a number of resources at the same time.
|
||||
|
||||
#### Enable source-IP rate limits
|
||||
|
||||
1. Set rate limits in `/etc/gitlab/gitlab.rb`:
|
||||
|
||||
```ruby
|
||||
gitlab_pages['rate_limit_source_ip'] = 20.0
|
||||
gitlab_pages['rate_limit_source_ip_burst'] = 600
|
||||
```
|
||||
|
||||
1. To reject requests that exceed the specified limits, enable the `FF_ENABLE_RATE_LIMITER` feature flag in
|
||||
`/etc/gitlab/gitlab.rb`:
|
||||
|
||||
```ruby
|
||||
gitlab_pages['env'] = {'FF_ENABLE_RATE_LIMITER' => 'true'}
|
||||
```
|
||||
|
||||
1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
|
||||
|
||||
<!-- ## Troubleshooting
|
||||
|
||||
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
|
||||
|
|
|
@ -306,11 +306,6 @@ Example response:
|
|||
"health": "Healthy",
|
||||
"health_status": "Healthy",
|
||||
"missing_oauth_application": false,
|
||||
"attachments_count": 1,
|
||||
"attachments_synced_count": null,
|
||||
"attachments_failed_count": null,
|
||||
"attachments_synced_missing_on_primary_count": 0,
|
||||
"attachments_synced_in_percentage": "0.00%",
|
||||
"db_replication_lag_seconds": null,
|
||||
"lfs_objects_count": 0,
|
||||
"lfs_objects_synced_count": null,
|
||||
|
@ -465,11 +460,6 @@ Example response:
|
|||
"health": "Healthy",
|
||||
"health_status": "Healthy",
|
||||
"missing_oauth_application": false,
|
||||
"attachments_count": 1,
|
||||
"attachments_synced_count": 1,
|
||||
"attachments_failed_count": 0,
|
||||
"attachments_synced_missing_on_primary_count": 0,
|
||||
"attachments_synced_in_percentage": "100.00%",
|
||||
"db_replication_lag_seconds": 0,
|
||||
"lfs_objects_count": 0,
|
||||
"lfs_objects_synced_count": 0,
|
||||
|
@ -628,11 +618,6 @@ Example response:
|
|||
"health": "Healthy",
|
||||
"health_status": "Healthy",
|
||||
"missing_oauth_application": false,
|
||||
"attachments_count": 1,
|
||||
"attachments_synced_count": 1,
|
||||
"attachments_failed_count": 0,
|
||||
"attachments_synced_missing_on_primary_count": 0,
|
||||
"attachments_synced_in_percentage": "100.00%",
|
||||
"db_replication_lag_seconds": 0,
|
||||
"lfs_objects_count": 0,
|
||||
"lfs_objects_synced_count": 0,
|
||||
|
|
|
@ -231,10 +231,6 @@ We also collect metrics specific to [Geo](../../administration/geo/index.md) sec
|
|||
"repositories_replication_enabled"=>true,
|
||||
"repositories_synced_count"=>24,
|
||||
"repositories_failed_count"=>0,
|
||||
"attachments_replication_enabled"=>true,
|
||||
"attachments_count"=>1,
|
||||
"attachments_synced_count"=>1,
|
||||
"attachments_failed_count"=>0,
|
||||
"git_fetch_event_count_weekly"=>nil,
|
||||
"git_push_event_count_weekly"=>nil,
|
||||
... other geo node status fields
|
||||
|
|
|
@ -10,6 +10,7 @@ type: reference, howto
|
|||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/210181) in GitLab 13.0.
|
||||
> - [Became available on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/235765) in GitLab 13.5 for paid groups only.
|
||||
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/235765) in GitLab 13.5.
|
||||
> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/342327) in GitLab 14.5. Default prefix added.
|
||||
|
||||
Project access tokens are similar to [personal access tokens](../../profile/personal_access_tokens.md)
|
||||
except they are attached to a project rather than a user. They can be used to:
|
||||
|
@ -32,6 +33,9 @@ Project access tokens:
|
|||
For examples of how you can use a project access token to authenticate with the API, see the
|
||||
[relevant section from our API Docs](../../../api/index.md#personalproject-access-tokens).
|
||||
|
||||
NOTE:
|
||||
For GitLab.com and new self-managed instances, the default prefix is `glpat-`.
|
||||
|
||||
## Creating a project access token
|
||||
|
||||
1. Log in to GitLab.
|
||||
|
|
|
@ -220,7 +220,6 @@ geo_repository_deleted_events: :gitlab_main
|
|||
geo_repository_renamed_events: :gitlab_main
|
||||
geo_repository_updated_events: :gitlab_main
|
||||
geo_reset_checksum_events: :gitlab_main
|
||||
geo_upload_deleted_events: :gitlab_main
|
||||
gitlab_subscription_histories: :gitlab_main
|
||||
gitlab_subscriptions: :gitlab_main
|
||||
gpg_keys: :gitlab_main
|
||||
|
|
|
@ -10095,6 +10095,9 @@ msgstr ""
|
|||
msgid "Crm|No contacts found"
|
||||
msgstr ""
|
||||
|
||||
msgid "Crm|No organizations found"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cron Timezone"
|
||||
msgstr ""
|
||||
|
||||
|
@ -15337,9 +15340,6 @@ msgstr ""
|
|||
msgid "Geo|Could not remove tracking entry for an existing project."
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Could not remove tracking entry for an existing upload."
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Data replication lag"
|
||||
msgstr ""
|
||||
|
||||
|
@ -15613,9 +15613,6 @@ msgstr ""
|
|||
msgid "Geo|Tracking entry for project (%{project_id}) was successfully removed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Tracking entry for upload (%{type}/%{id}) was successfully removed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|URL can't be blank"
|
||||
msgstr ""
|
||||
|
||||
|
@ -28891,9 +28888,6 @@ msgstr ""
|
|||
msgid "Removed %{reviewer_text} %{reviewer_references}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Removed %{type} with id %{id}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Removed all labels."
|
||||
msgstr ""
|
||||
|
||||
|
@ -28918,6 +28912,9 @@ msgstr ""
|
|||
msgid "Removed time estimate."
|
||||
msgstr ""
|
||||
|
||||
msgid "Removed upload with id %{id}"
|
||||
msgstr ""
|
||||
|
||||
msgid "RemovedProjects|Projects which are removed and are yet to be permanently removed are visible here."
|
||||
msgstr ""
|
||||
|
||||
|
@ -37206,9 +37203,6 @@ msgstr ""
|
|||
msgid "Uploading changes to terminal"
|
||||
msgstr ""
|
||||
|
||||
msgid "Uploads"
|
||||
msgstr ""
|
||||
|
||||
msgid "Upon performing this action, the contents of this group, its subgroup and projects will be permanently deleted after %{deletion_adjourned_period} days on %{date}. Until that time:"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -209,7 +209,6 @@
|
|||
"@testing-library/dom": "^7.16.2",
|
||||
"@vue/test-utils": "1.2.0",
|
||||
"acorn": "^6.3.0",
|
||||
"apollo": "^2.33.8",
|
||||
"axios-mock-adapter": "^1.15.0",
|
||||
"babel-jest": "^26.5.2",
|
||||
"babel-plugin-dynamic-import-node": "^2.3.3",
|
||||
|
@ -264,8 +263,7 @@
|
|||
},
|
||||
"resolutions": {
|
||||
"chokidar": "^3.4.0",
|
||||
"@types/node": "14.17.5",
|
||||
"graphql": "^15.4.0"
|
||||
"@types/node": "14.17.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.22.1",
|
||||
|
|
|
@ -7,6 +7,17 @@ module QA
|
|||
# creating it if it doesn't yet exist.
|
||||
#
|
||||
class Sandbox < GroupBase
|
||||
class << self
|
||||
# Force top level group creation via UI if test is executed on dot_com environment
|
||||
def fabricate!(*args, &prepare_block)
|
||||
return fabricate_via_browser_ui!(*args, &prepare_block) if Specs::Helpers::ContextSelector.dot_com?
|
||||
|
||||
fabricate_via_api!(*args, &prepare_block)
|
||||
rescue NotImplementedError
|
||||
fabricate_via_browser_ui!(*args, &prepare_block)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize
|
||||
@path = Runtime::Namespace.sandbox_name
|
||||
end
|
||||
|
|
|
@ -22,9 +22,7 @@ module QA
|
|||
end
|
||||
|
||||
let(:source_group) do
|
||||
# top level group can't be created on staging via api, create via UI
|
||||
fabricate_method = staging? ? :fabricate_via_browser_ui! : :fabricate_via_api!
|
||||
Resource::Sandbox.send(fabricate_method) do |group|
|
||||
Resource::Sandbox.fabricate! do |group|
|
||||
group.api_client = api_client
|
||||
group.path = "source-group-for-import-#{SecureRandom.hex(4)}"
|
||||
end
|
||||
|
|
|
@ -48,7 +48,6 @@ RSpec.describe 'Database schema' do
|
|||
geo_node_statuses: %w[last_event_id cursor_last_event_id],
|
||||
geo_nodes: %w[oauth_application_id],
|
||||
geo_repository_deleted_events: %w[project_id],
|
||||
geo_upload_deleted_events: %w[upload_id model_id],
|
||||
gitlab_subscription_histories: %w[gitlab_subscription_id hosted_plan_id namespace_id],
|
||||
identities: %w[user_id],
|
||||
import_failures: %w[project_id],
|
||||
|
|
|
@ -45,3 +45,37 @@ export const getGroupContactsQueryResponse = {
|
|||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const getGroupOrganizationsQueryResponse = {
|
||||
data: {
|
||||
group: {
|
||||
__typename: 'Group',
|
||||
id: 'gid://gitlab/Group/26',
|
||||
organizations: {
|
||||
nodes: [
|
||||
{
|
||||
__typename: 'CustomerRelationsOrganization',
|
||||
id: 'gid://gitlab/CustomerRelations::Organization/1',
|
||||
name: 'Test Inc',
|
||||
defaultRate: 100,
|
||||
description: null,
|
||||
},
|
||||
{
|
||||
__typename: 'CustomerRelationsOrganization',
|
||||
id: 'gid://gitlab/CustomerRelations::Organization/2',
|
||||
name: 'ABC Company',
|
||||
defaultRate: 110,
|
||||
description: 'VIP',
|
||||
},
|
||||
{
|
||||
__typename: 'CustomerRelationsOrganization',
|
||||
id: 'gid://gitlab/CustomerRelations::Organization/3',
|
||||
name: 'GitLab',
|
||||
defaultRate: 120,
|
||||
description: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
60
spec/frontend/crm/organizations_root_spec.js
Normal file
60
spec/frontend/crm/organizations_root_spec.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import createFlash from '~/flash';
|
||||
import OrganizationsRoot from '~/crm/components/organizations_root.vue';
|
||||
import getGroupOrganizationsQuery from '~/crm/components/queries/get_group_organizations.query.graphql';
|
||||
import { getGroupOrganizationsQueryResponse } from './mock_data';
|
||||
|
||||
jest.mock('~/flash');
|
||||
|
||||
describe('Customer relations organizations root app', () => {
|
||||
Vue.use(VueApollo);
|
||||
let wrapper;
|
||||
let fakeApollo;
|
||||
|
||||
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||
const findRowByName = (rowName) => wrapper.findAllByRole('row', { name: rowName });
|
||||
const successQueryHandler = jest.fn().mockResolvedValue(getGroupOrganizationsQueryResponse);
|
||||
|
||||
const mountComponent = ({
|
||||
queryHandler = successQueryHandler,
|
||||
mountFunction = shallowMountExtended,
|
||||
} = {}) => {
|
||||
fakeApollo = createMockApollo([[getGroupOrganizationsQuery, queryHandler]]);
|
||||
wrapper = mountFunction(OrganizationsRoot, {
|
||||
provide: { groupFullPath: 'flightjs' },
|
||||
apolloProvider: fakeApollo,
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
fakeApollo = null;
|
||||
});
|
||||
|
||||
it('should render loading spinner', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findLoadingIcon().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should render error message on reject', async () => {
|
||||
mountComponent({ queryHandler: jest.fn().mockRejectedValue('ERROR') });
|
||||
await waitForPromises();
|
||||
|
||||
expect(createFlash).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders correct results', async () => {
|
||||
mountComponent({ mountFunction: mountExtended });
|
||||
await waitForPromises();
|
||||
|
||||
expect(findRowByName(/Test Inc/i)).toHaveLength(1);
|
||||
expect(findRowByName(/VIP/i)).toHaveLength(1);
|
||||
expect(findRowByName(/120/i)).toHaveLength(1);
|
||||
});
|
||||
});
|
45
spec/models/preloaders/group_policy_preloader_spec.rb
Normal file
45
spec/models/preloaders/group_policy_preloader_spec.rb
Normal file
|
@ -0,0 +1,45 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Preloaders::GroupPolicyPreloader do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:root_parent) { create(:group, :private, name: 'root-1', path: 'root-1') }
|
||||
let_it_be(:guest_group) { create(:group, name: 'public guest', path: 'public-guest') }
|
||||
let_it_be(:private_maintainer_group) { create(:group, :private, name: 'b private maintainer', path: 'b-private-maintainer', parent: root_parent) }
|
||||
let_it_be(:private_developer_group) { create(:group, :private, project_creation_level: nil, name: 'c public developer', path: 'c-public-developer') }
|
||||
let_it_be(:public_maintainer_group) { create(:group, :private, name: 'a public maintainer', path: 'a-public-maintainer') }
|
||||
|
||||
let(:base_groups) { [guest_group, private_maintainer_group, private_developer_group, public_maintainer_group] }
|
||||
|
||||
before_all do
|
||||
guest_group.add_guest(user)
|
||||
private_maintainer_group.add_maintainer(user)
|
||||
private_developer_group.add_developer(user)
|
||||
public_maintainer_group.add_maintainer(user)
|
||||
end
|
||||
|
||||
it 'avoids N+1 queries when authorizing a list of groups', :request_store do
|
||||
preload_groups_for_policy(user)
|
||||
control = ActiveRecord::QueryRecorder.new { authorize_all_groups(user) }
|
||||
|
||||
new_group1 = create(:group, :private).tap { |group| group.add_maintainer(user) }
|
||||
new_group2 = create(:group, :private, parent: private_maintainer_group)
|
||||
|
||||
another_root = create(:group, :private, name: 'root-3', path: 'root-3')
|
||||
new_group3 = create(:group, :private, parent: another_root).tap { |group| group.add_maintainer(user) }
|
||||
|
||||
pristine_groups = Group.where(id: base_groups + [new_group1, new_group2, new_group3])
|
||||
|
||||
preload_groups_for_policy(user, pristine_groups)
|
||||
expect { authorize_all_groups(user, pristine_groups) }.not_to exceed_query_limit(control)
|
||||
end
|
||||
|
||||
def authorize_all_groups(current_user, group_list = base_groups)
|
||||
group_list.each { |group| current_user.can?(:read_group, group) }
|
||||
end
|
||||
|
||||
def preload_groups_for_policy(current_user, group_list = base_groups)
|
||||
described_class.new(group_list, current_user).execute
|
||||
end
|
||||
end
|
63
spec/models/preloaders/group_root_ancestor_preloader_spec.rb
Normal file
63
spec/models/preloaders/group_root_ancestor_preloader_spec.rb
Normal file
|
@ -0,0 +1,63 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Preloaders::GroupRootAncestorPreloader do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:root_parent1) { create(:group, :private, name: 'root-1', path: 'root-1') }
|
||||
let_it_be(:root_parent2) { create(:group, :private, name: 'root-2', path: 'root-2') }
|
||||
let_it_be(:guest_group) { create(:group, name: 'public guest', path: 'public-guest') }
|
||||
let_it_be(:private_maintainer_group) { create(:group, :private, name: 'b private maintainer', path: 'b-private-maintainer', parent: root_parent1) }
|
||||
let_it_be(:private_developer_group) { create(:group, :private, project_creation_level: nil, name: 'c public developer', path: 'c-public-developer') }
|
||||
let_it_be(:public_maintainer_group) { create(:group, :private, name: 'a public maintainer', path: 'a-public-maintainer', parent: root_parent2) }
|
||||
|
||||
let(:root_query_regex) { /\ASELECT.+FROM "namespaces" WHERE "namespaces"."id" = \d+/ }
|
||||
let(:additional_preloads) { [] }
|
||||
let(:groups) { [guest_group, private_maintainer_group, private_developer_group, public_maintainer_group] }
|
||||
let(:pristine_groups) { Group.where(id: groups) }
|
||||
|
||||
shared_examples 'executes N matching DB queries' do |expected_query_count, query_method = nil|
|
||||
it 'executes the specified root_ancestor queries' do
|
||||
expect do
|
||||
pristine_groups.each do |group|
|
||||
root_ancestor = group.root_ancestor
|
||||
|
||||
root_ancestor.public_send(query_method) if query_method.present?
|
||||
end
|
||||
end.to make_queries_matching(root_query_regex, expected_query_count)
|
||||
end
|
||||
|
||||
it 'strong_memoizes the correct root_ancestor' do
|
||||
pristine_groups.each do |group|
|
||||
expected_parent_id = group.root_ancestor.id == group.id ? nil : group.root_ancestor.id
|
||||
|
||||
expect(group.parent_id).to eq(expected_parent_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the preloader is used' do
|
||||
before do
|
||||
preload_ancestors
|
||||
end
|
||||
|
||||
context 'when no additional preloads are provided' do
|
||||
it_behaves_like 'executes N matching DB queries', 0
|
||||
end
|
||||
|
||||
context 'when additional preloads are provided' do
|
||||
let(:additional_preloads) { [:route] }
|
||||
let(:root_query_regex) { /\ASELECT.+FROM "routes" WHERE "routes"."source_id" = \d+/ }
|
||||
|
||||
it_behaves_like 'executes N matching DB queries', 0, :full_path
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the preloader is not used' do
|
||||
it_behaves_like 'executes N matching DB queries', 2
|
||||
end
|
||||
|
||||
def preload_ancestors
|
||||
described_class.new(pristine_groups, additional_preloads).execute
|
||||
end
|
||||
end
|
|
@ -13,32 +13,47 @@ RSpec.describe Preloaders::UserMaxAccessLevelInGroupsPreloader do
|
|||
|
||||
shared_examples 'executes N max member permission queries to the DB' do
|
||||
it 'executes the specified max membership queries' do
|
||||
queries = ActiveRecord::QueryRecorder.new do
|
||||
groups.each { |group| user.can?(:read_group, group) }
|
||||
expect { groups.each { |group| user.can?(:read_group, group) } }.to make_queries_matching(max_query_regex, expected_query_count)
|
||||
end
|
||||
|
||||
it 'caches the correct access_level for each group' do
|
||||
groups.each do |group|
|
||||
access_level_from_db = group.members_with_parents.where(user_id: user.id).group(:user_id).maximum(:access_level)[user.id] || Gitlab::Access::NO_ACCESS
|
||||
cached_access_level = group.max_member_access_for_user(user)
|
||||
|
||||
expect(cached_access_level).to eq(access_level_from_db)
|
||||
end
|
||||
|
||||
max_queries = queries.log.grep(max_query_regex)
|
||||
|
||||
expect(max_queries.count).to eq(expected_query_count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the preloader is used', :request_store do
|
||||
before do
|
||||
described_class.new(groups, user).execute
|
||||
end
|
||||
context 'when user has indirect access to groups' do
|
||||
let_it_be(:child_maintainer) { create(:group, :private, parent: group1).tap {|g| g.add_maintainer(user)} }
|
||||
let_it_be(:child_indirect_access) { create(:group, :private, parent: group1) }
|
||||
|
||||
it_behaves_like 'executes N max member permission queries to the DB' do
|
||||
# Will query all groups where the user is not already a member
|
||||
let(:expected_query_count) { 1 }
|
||||
end
|
||||
let(:groups) { [group1, group2, group3, child_maintainer, child_indirect_access] }
|
||||
|
||||
context 'when user has access but is not a direct member of the group' do
|
||||
let(:groups) { [group1, group2, group3, create(:group, :private, parent: group1)] }
|
||||
context 'when traversal_ids feature flag is disabled' do
|
||||
it_behaves_like 'executes N max member permission queries to the DB' do
|
||||
before do
|
||||
stub_feature_flags(use_traversal_ids: false)
|
||||
described_class.new(groups, user).execute
|
||||
end
|
||||
|
||||
it_behaves_like 'executes N max member permission queries to the DB' do
|
||||
# One query for group with no access and another one where the user is not a direct member
|
||||
let(:expected_query_count) { 2 }
|
||||
# One query for group with no access and another one per group where the user is not a direct member
|
||||
let(:expected_query_count) { 2 }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when traversal_ids feature flag is enabled' do
|
||||
it_behaves_like 'executes N max member permission queries to the DB' do
|
||||
before do
|
||||
stub_feature_flags(use_traversal_ids: true)
|
||||
described_class.new(groups, user).execute
|
||||
end
|
||||
|
||||
let(:expected_query_count) { 0 }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue