Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/admin/users/components/users_table.vue61
-rw-r--r--app/assets/javascripts/admin/users/graphql/queries/get_users_group_counts.query.graphql8
-rw-r--r--app/assets/javascripts/admin/users/index.js9
-rw-r--r--app/graphql/types/user_interface.rb3
-rw-r--r--app/helpers/webpack_helper.rb4
-rw-r--r--app/views/layouts/_head.html.haml2
-rw-r--r--changelogs/unreleased/234005-extend-conantoken-expiration.yml5
-rw-r--r--changelogs/unreleased/276215-admin-users-show-group-count.yml5
-rw-r--r--changelogs/unreleased/default-csp.yml5
-rw-r--r--changelogs/unreleased/sh-upgrade-grape-path-helpers.yml5
-rw-r--r--config/feature_flags/development/find_remote_root_refs_inmemory.yml2
-rw-r--r--config/feature_flags/development/user_group_counts.yml8
-rw-r--r--config/metrics/counts_28d/20210216175004_g_analytics_merge_request_monthly.yml2
-rw-r--r--config/metrics/counts_all/20210216174902_g_analytics_merge_request.yml5
-rw-r--r--doc/api/graphql/reference/index.md8
-rw-r--r--doc/development/usage_ping/dictionary.md6
-rw-r--r--lib/gitlab/conan_token.rb3
-rw-r--r--lib/gitlab/content_security_policy/config_loader.rb48
-rw-r--r--locale/gitlab.pot3
-rw-r--r--spec/features/admin/users/users_spec.rb26
-rw-r--r--spec/features/projects/new_project_from_template_spec.rb26
-rw-r--r--spec/frontend/admin/users/components/users_table_spec.js87
-rw-r--r--spec/frontend/admin/users/mock_data.js15
-rw-r--r--spec/lib/gitlab/conan_token_spec.rb4
-rw-r--r--spec/lib/gitlab/content_security_policy/config_loader_spec.rb29
-rw-r--r--spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb2
-rw-r--r--spec/views/layouts/_head.html.haml_spec.rb6
30 files changed, 347 insertions, 48 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 7053c91ad7a..5f653fbfc26 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-7ad4fcea44bc4c1a8682d77ad6b46aeacb8c6e1d
+4f0cd9404f31511f5051e49b363adc06aa3ec365
diff --git a/Gemfile b/Gemfile
index b228e42aeea..925deada095 100644
--- a/Gemfile
+++ b/Gemfile
@@ -18,7 +18,7 @@ gem 'default_value_for', '~> 3.4.0'
gem 'pg', '~> 1.1'
gem 'rugged', '~> 1.1'
-gem 'grape-path-helpers', '~> 1.6.1'
+gem 'grape-path-helpers', '~> 1.6.3'
gem 'faraday', '~> 1.0'
gem 'marginalia', '~> 1.10.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index a703d29bc4e..0efe172d134 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -541,7 +541,7 @@ GEM
grape-entity (0.7.1)
activesupport (>= 4.0)
multi_json (>= 1.3.2)
- grape-path-helpers (1.6.1)
+ grape-path-helpers (1.6.3)
activesupport
grape (~> 1.3)
rake (> 12)
@@ -1471,7 +1471,7 @@ DEPENDENCIES
gpgme (~> 2.0.19)
grape (~> 1.5.2)
grape-entity (~> 0.7.1)
- grape-path-helpers (~> 1.6.1)
+ grape-path-helpers (~> 1.6.3)
grape_logging (~> 1.7)
graphiql-rails (~> 1.4.10)
graphlient (~> 0.4.0)
diff --git a/app/assets/javascripts/admin/users/components/users_table.vue b/app/assets/javascripts/admin/users/components/users_table.vue
index 4b5d12b3a5f..ab7bc65075d 100644
--- a/app/assets/javascripts/admin/users/components/users_table.vue
+++ b/app/assets/javascripts/admin/users/components/users_table.vue
@@ -1,7 +1,10 @@
<script>
-import { GlTable } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { GlSkeletonLoader, GlTable } from '@gitlab/ui';
+import createFlash from '~/flash';
+import { convertNodeIdsFromGraphQLIds } from '~/graphql_shared/utils';
+import { s__, __ } from '~/locale';
import UserDate from '~/vue_shared/components/user_date.vue';
+import getUsersGroupCountsQuery from '../graphql/queries/get_users_group_counts.query.graphql';
import UserActions from './user_actions.vue';
import UserAvatar from './user_avatar.vue';
@@ -11,6 +14,7 @@ const thWidthClass = (width) => `gl-w-${width}p ${DEFAULT_TH_CLASSES}`;
export default {
components: {
+ GlSkeletonLoader,
GlTable,
UserAvatar,
UserActions,
@@ -26,6 +30,45 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ groupCounts: [],
+ };
+ },
+ apollo: {
+ groupCounts: {
+ query: getUsersGroupCountsQuery,
+ variables() {
+ return {
+ usernames: this.users.map((user) => user.username),
+ };
+ },
+ update(data) {
+ const nodes = data?.users?.nodes || [];
+ const parsedIds = convertNodeIdsFromGraphQLIds(nodes);
+
+ return parsedIds.reduce((acc, { id, groupCount }) => {
+ acc[id] = groupCount || 0;
+ return acc;
+ }, {});
+ },
+ error(error) {
+ createFlash({
+ message: this.$options.i18n.groupCountFetchError,
+ captureError: true,
+ error,
+ });
+ },
+ skip() {
+ return !this.users.length;
+ },
+ },
+ },
+ i18n: {
+ groupCountFetchError: s__(
+ 'AdminUsers|Could not load user group counts. Please refresh the page to try again.',
+ ),
+ },
fields: [
{
key: 'name',
@@ -38,6 +81,11 @@ export default {
thClass: thWidthClass(10),
},
{
+ key: 'groupCount',
+ label: __('Groups'),
+ thClass: thWidthClass(10),
+ },
+ {
key: 'createdAt',
label: __('Created on'),
thClass: thWidthClass(15),
@@ -50,7 +98,7 @@ export default {
{
key: 'settings',
label: '',
- thClass: thWidthClass(20),
+ thClass: thWidthClass(10),
},
],
};
@@ -77,6 +125,13 @@ export default {
<user-date :date="lastActivityOn" show-never />
</template>
+ <template #cell(groupCount)="{ item: { id } }">
+ <div :data-testid="`user-group-count-${id}`">
+ <gl-skeleton-loader v-if="$apollo.loading" :width="40" :lines="1" />
+ <span v-else>{{ groupCounts[id] }}</span>
+ </div>
+ </template>
+
<template #cell(projectsCount)="{ item: { id, projectsCount } }">
<div :data-testid="`user-project-count-${id}`">{{ projectsCount }}</div>
</template>
diff --git a/app/assets/javascripts/admin/users/graphql/queries/get_users_group_counts.query.graphql b/app/assets/javascripts/admin/users/graphql/queries/get_users_group_counts.query.graphql
new file mode 100644
index 00000000000..0d8e199f16e
--- /dev/null
+++ b/app/assets/javascripts/admin/users/graphql/queries/get_users_group_counts.query.graphql
@@ -0,0 +1,8 @@
+query getUsersGroupCounts($usernames: [String!]) {
+ users(usernames: $usernames) {
+ nodes {
+ id
+ groupCount
+ }
+ }
+}
diff --git a/app/assets/javascripts/admin/users/index.js b/app/assets/javascripts/admin/users/index.js
index eceae8a8674..54c8edc080b 100644
--- a/app/assets/javascripts/admin/users/index.js
+++ b/app/assets/javascripts/admin/users/index.js
@@ -1,7 +1,15 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import AdminUsersApp from './components/app.vue';
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+});
+
export const initAdminUsersApp = (el = document.querySelector('#js-admin-users-app')) => {
if (!el) {
return false;
@@ -11,6 +19,7 @@ export const initAdminUsersApp = (el = document.querySelector('#js-admin-users-a
return new Vue({
el,
+ apolloProvider,
render: (createElement) =>
createElement(AdminUsersApp, {
props: {
diff --git a/app/graphql/types/user_interface.rb b/app/graphql/types/user_interface.rb
index bc6e11d3ab9..e5abc033155 100644
--- a/app/graphql/types/user_interface.rb
+++ b/app/graphql/types/user_interface.rb
@@ -64,8 +64,7 @@ module Types
description: 'Group memberships of the user.'
field :group_count,
resolver: Resolvers::Users::GroupCountResolver,
- description: 'Group count for the user.',
- feature_flag: :user_group_counts
+ description: 'Group count for the user.'
field :status,
type: Types::UserStatusType,
null: true,
diff --git a/app/helpers/webpack_helper.rb b/app/helpers/webpack_helper.rb
index 170e3c45a21..90b8a8e94b0 100644
--- a/app/helpers/webpack_helper.rb
+++ b/app/helpers/webpack_helper.rb
@@ -5,6 +5,10 @@ module WebpackHelper
javascript_include_tag(*webpack_entrypoint_paths(bundle))
end
+ def webpack_preload_asset_tag(asset, options = {})
+ preload_link_tag(Gitlab::Webpack::Manifest.asset_paths(asset).first, options)
+ end
+
def webpack_controller_bundle_tags
chunks = []
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 6694ad5968a..8aa163a26ec 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -32,6 +32,8 @@
- if page_canonical_link
%link{ rel: 'canonical', href: page_canonical_link }
+ = webpack_preload_asset_tag("monaco")
+
= favicon_link_tag favicon, id: 'favicon', data: { original_href: favicon }, type: 'image/png'
= render 'layouts/startup_css'
diff --git a/changelogs/unreleased/234005-extend-conantoken-expiration.yml b/changelogs/unreleased/234005-extend-conantoken-expiration.yml
new file mode 100644
index 00000000000..daa92bd27c9
--- /dev/null
+++ b/changelogs/unreleased/234005-extend-conantoken-expiration.yml
@@ -0,0 +1,5 @@
+---
+title: Change conan token expiration from 1 hour to 24 hours
+merge_request: 60763
+author:
+type: changed
diff --git a/changelogs/unreleased/276215-admin-users-show-group-count.yml b/changelogs/unreleased/276215-admin-users-show-group-count.yml
new file mode 100644
index 00000000000..7750bee17fe
--- /dev/null
+++ b/changelogs/unreleased/276215-admin-users-show-group-count.yml
@@ -0,0 +1,5 @@
+---
+title: Show total group counts in admin users table
+merge_request: 60998
+author:
+type: added
diff --git a/changelogs/unreleased/default-csp.yml b/changelogs/unreleased/default-csp.yml
new file mode 100644
index 00000000000..5ae79627dd8
--- /dev/null
+++ b/changelogs/unreleased/default-csp.yml
@@ -0,0 +1,5 @@
+---
+title: Enable Content-Security-Policy header by default
+merge_request: 56923
+author:
+type: other
diff --git a/changelogs/unreleased/sh-upgrade-grape-path-helpers.yml b/changelogs/unreleased/sh-upgrade-grape-path-helpers.yml
new file mode 100644
index 00000000000..464b6807b3b
--- /dev/null
+++ b/changelogs/unreleased/sh-upgrade-grape-path-helpers.yml
@@ -0,0 +1,5 @@
+---
+title: Update grape-path-helpers to v1.6.3
+merge_request: 61196
+author:
+type: performance
diff --git a/config/feature_flags/development/find_remote_root_refs_inmemory.yml b/config/feature_flags/development/find_remote_root_refs_inmemory.yml
index 18e2e2b366a..c78eadceaad 100644
--- a/config/feature_flags/development/find_remote_root_refs_inmemory.yml
+++ b/config/feature_flags/development/find_remote_root_refs_inmemory.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329664
milestone: '13.12'
type: development
group: group::gitaly
-default_enabled: true
+default_enabled: false
diff --git a/config/feature_flags/development/user_group_counts.yml b/config/feature_flags/development/user_group_counts.yml
deleted file mode 100644
index 98798ea3ead..00000000000
--- a/config/feature_flags/development/user_group_counts.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: user_group_counts
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44069/
-rollout_issue_url:
-milestone: '13.6'
-type: development
-group: group::compliance
-default_enabled: false
diff --git a/config/metrics/counts_28d/20210216175004_g_analytics_merge_request_monthly.yml b/config/metrics/counts_28d/20210216175004_g_analytics_merge_request_monthly.yml
index b7567777135..c0fd0561ee4 100644
--- a/config/metrics/counts_28d/20210216175004_g_analytics_merge_request_monthly.yml
+++ b/config/metrics/counts_28d/20210216175004_g_analytics_merge_request_monthly.yml
@@ -6,7 +6,7 @@ product_stage: manage
product_group: group::optimize
product_category:
value_type: number
-status: data_available
+status: removed
time_frame: 28d
data_source: redis_hll
distribution:
diff --git a/config/metrics/counts_all/20210216174902_g_analytics_merge_request.yml b/config/metrics/counts_all/20210216174902_g_analytics_merge_request.yml
index fef77d5c3ac..bbd3bfca2b2 100644
--- a/config/metrics/counts_all/20210216174902_g_analytics_merge_request.yml
+++ b/config/metrics/counts_all/20210216174902_g_analytics_merge_request.yml
@@ -6,8 +6,9 @@ product_stage: manage
product_group: group::optimize
product_category:
value_type: number
-status: data_available
-time_frame: all
+status: removed
+time_frame: 7d
+data_source: redis_hll
data_source:
distribution:
- ce
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 3626fe428b1..6aa1c50ded1 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -9844,7 +9844,7 @@ A user assigned to a merge request.
| <a id="mergerequestassigneebot"></a>`bot` | [`Boolean!`](#boolean) | Indicates if the user is a bot. |
| <a id="mergerequestassigneecallouts"></a>`callouts` | [`UserCalloutConnection`](#usercalloutconnection) | User callouts that belong to the user. (see [Connections](#connections)) |
| <a id="mergerequestassigneeemail"></a>`email` **{warning-solid}** | [`String`](#string) | **Deprecated** in 13.7. This was renamed. Use: [`User.publicEmail`](#userpublicemail). |
-| <a id="mergerequestassigneegroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. Available only when feature flag `user_group_counts` is enabled. |
+| <a id="mergerequestassigneegroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. |
| <a id="mergerequestassigneegroupmemberships"></a>`groupMemberships` | [`GroupMemberConnection`](#groupmemberconnection) | Group memberships of the user. (see [Connections](#connections)) |
| <a id="mergerequestassigneeid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="mergerequestassigneelocation"></a>`location` | [`String`](#string) | The location of the user. |
@@ -10050,7 +10050,7 @@ A user assigned to a merge request as a reviewer.
| <a id="mergerequestreviewerbot"></a>`bot` | [`Boolean!`](#boolean) | Indicates if the user is a bot. |
| <a id="mergerequestreviewercallouts"></a>`callouts` | [`UserCalloutConnection`](#usercalloutconnection) | User callouts that belong to the user. (see [Connections](#connections)) |
| <a id="mergerequestrevieweremail"></a>`email` **{warning-solid}** | [`String`](#string) | **Deprecated** in 13.7. This was renamed. Use: [`User.publicEmail`](#userpublicemail). |
-| <a id="mergerequestreviewergroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. Available only when feature flag `user_group_counts` is enabled. |
+| <a id="mergerequestreviewergroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. |
| <a id="mergerequestreviewergroupmemberships"></a>`groupMemberships` | [`GroupMemberConnection`](#groupmemberconnection) | Group memberships of the user. (see [Connections](#connections)) |
| <a id="mergerequestreviewerid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="mergerequestreviewerlocation"></a>`location` | [`String`](#string) | The location of the user. |
@@ -12679,7 +12679,7 @@ Core represention of a GitLab user.
| <a id="usercorebot"></a>`bot` | [`Boolean!`](#boolean) | Indicates if the user is a bot. |
| <a id="usercorecallouts"></a>`callouts` | [`UserCalloutConnection`](#usercalloutconnection) | User callouts that belong to the user. (see [Connections](#connections)) |
| <a id="usercoreemail"></a>`email` **{warning-solid}** | [`String`](#string) | **Deprecated** in 13.7. This was renamed. Use: [`User.publicEmail`](#userpublicemail). |
-| <a id="usercoregroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. Available only when feature flag `user_group_counts` is enabled. |
+| <a id="usercoregroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. |
| <a id="usercoregroupmemberships"></a>`groupMemberships` | [`GroupMemberConnection`](#groupmemberconnection) | Group memberships of the user. (see [Connections](#connections)) |
| <a id="usercoreid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="usercorelocation"></a>`location` | [`String`](#string) | The location of the user. |
@@ -15346,7 +15346,7 @@ Implementations:
| <a id="userbot"></a>`bot` | [`Boolean!`](#boolean) | Indicates if the user is a bot. |
| <a id="usercallouts"></a>`callouts` | [`UserCalloutConnection`](#usercalloutconnection) | User callouts that belong to the user. (see [Connections](#connections)) |
| <a id="useremail"></a>`email` **{warning-solid}** | [`String`](#string) | **Deprecated** in 13.7. This was renamed. Use: [`User.publicEmail`](#userpublicemail). |
-| <a id="usergroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. Available only when feature flag `user_group_counts` is enabled. |
+| <a id="usergroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. |
| <a id="usergroupmemberships"></a>`groupMemberships` | [`GroupMemberConnection`](#groupmemberconnection) | Group memberships of the user. (see [Connections](#connections)) |
| <a id="userid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="userlocation"></a>`location` | [`String`](#string) | The location of the user. |
diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md
index d63fbf96336..094d5cce21b 100644
--- a/doc/development/usage_ping/dictionary.md
+++ b/doc/development/usage_ping/dictionary.md
@@ -114,7 +114,7 @@ Unique visitors to /groups/:group/-/analytics/merge_request_analytics
Group: `group::optimize`
-Status: `data_available`
+Status: `removed`
Tiers: `free`
@@ -7410,7 +7410,7 @@ Missing description
Group: `group::optimize`
-Status: `data_available`
+Status: `removed`
Tiers: `free`
@@ -7422,7 +7422,7 @@ Missing description
Group: `group::optimize`
-Status: `data_available`
+Status: `removed`
Tiers:
diff --git a/lib/gitlab/conan_token.rb b/lib/gitlab/conan_token.rb
index c3d90aa78fb..d0560807f45 100644
--- a/lib/gitlab/conan_token.rb
+++ b/lib/gitlab/conan_token.rb
@@ -8,6 +8,7 @@
module Gitlab
class ConanToken
HMAC_KEY = 'gitlab-conan-packages'
+ CONAN_TOKEN_EXPIRE_TIME = 1.day.freeze
attr_reader :access_token_id, :user_id
@@ -57,7 +58,7 @@ module Gitlab
JSONWebToken::HMACToken.new(self.class.secret).tap do |token|
token['access_token'] = access_token_id
token['user_id'] = user_id
- token.expire_time = token.issued_at + 1.hour
+ token.expire_time = token.issued_at + CONAN_TOKEN_EXPIRE_TIME
end
end
end
diff --git a/lib/gitlab/content_security_policy/config_loader.rb b/lib/gitlab/content_security_policy/config_loader.rb
index ff844645b11..6f6147f0f32 100644
--- a/lib/gitlab/content_security_policy/config_loader.rb
+++ b/lib/gitlab/content_security_policy/config_loader.rb
@@ -8,11 +8,33 @@ module Gitlab
media_src object_src report_uri script_src style_src worker_src).freeze
def self.default_settings_hash
- {
- 'enabled' => false,
+ settings_hash = {
+ 'enabled' => true,
'report_only' => false,
- 'directives' => DIRECTIVES.each_with_object({}) { |directive, hash| hash[directive] = nil }
+ 'directives' => {
+ 'default_src' => "'self'",
+ 'base_uri' => "'self'",
+ 'child_src' => "'none'",
+ 'connect_src' => "'self'",
+ 'font_src' => "'self'",
+ 'form_action' => "'self' https: http:",
+ 'frame_ancestors' => "'self'",
+ 'frame_src' => "'self' https://www.recaptcha.net/ https://content.googleapis.com https://content-compute.googleapis.com https://content-cloudbilling.googleapis.com https://content-cloudresourcemanager.googleapis.com",
+ 'img_src' => "'self' data: blob: http: https:",
+ 'manifest_src' => "'self'",
+ 'media_src' => "'self'",
+ 'script_src' => "'strict-dynamic' 'self' 'unsafe-inline' 'unsafe-eval' https://www.recaptcha.net https://apis.google.com",
+ 'style_src' => "'self' 'unsafe-inline'",
+ 'worker_src' => "'self'",
+ 'object_src' => "'none'",
+ 'report_uri' => nil
+ }
}
+
+ allow_webpack_dev_server(settings_hash) if Rails.env.development?
+ allow_cdn(settings_hash) if ENV['GITLAB_CDN_HOST'].present?
+
+ settings_hash
end
def initialize(csp_directives)
@@ -38,6 +60,26 @@ module Gitlab
arguments.strip.split(' ').map(&:strip)
end
+
+ def self.allow_webpack_dev_server(settings_hash)
+ secure = Settings.webpack.dev_server['https']
+ host_and_port = "#{Settings.webpack.dev_server['host']}:#{Settings.webpack.dev_server['port']}"
+ http_url = "#{secure ? 'https' : 'http'}://#{host_and_port}"
+ ws_url = "#{secure ? 'wss' : 'ws'}://#{host_and_port}"
+
+ append_to_directive(settings_hash, 'connect_src', "#{http_url} #{ws_url}")
+ end
+
+ def self.allow_cdn(settings_hash)
+ cdn_host = ENV['GITLAB_CDN_HOST']
+
+ append_to_directive(settings_hash, 'script_src', cdn_host)
+ append_to_directive(settings_hash, 'style_src', cdn_host)
+ end
+
+ def self.append_to_directive(settings_hash, directive, text)
+ settings_hash['directives'][directive] = "#{settings_hash['directives'][directive]} #{text}".strip
+ end
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b8dde3291b8..085d16b7bd5 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2528,6 +2528,9 @@ msgstr ""
msgid "AdminUsers|Cohorts"
msgstr ""
+msgid "AdminUsers|Could not load user group counts. Please refresh the page to try again."
+msgstr ""
+
msgid "AdminUsers|Deactivate"
msgstr ""
diff --git a/spec/features/admin/users/users_spec.rb b/spec/features/admin/users/users_spec.rb
index e38376b0741..36907d4aa60 100644
--- a/spec/features/admin/users/users_spec.rb
+++ b/spec/features/admin/users/users_spec.rb
@@ -547,6 +547,32 @@ RSpec.describe 'Admin::Users' do
end
end
+ # TODO: Move to main GET /admin/users block once feature flag is removed. Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/290737
+ context 'with vue_admin_users feature flag enabled', :js do
+ before do
+ stub_feature_flags(vue_admin_users: true)
+ end
+
+ describe 'GET /admin/users' do
+ context 'user group count', :js do
+ before do
+ group = create(:group)
+ group.add_developer(current_user)
+ project = create(:project, group: create(:group))
+ project.add_reporter(current_user)
+ end
+
+ it 'displays count of the users authorized groups' do
+ visit admin_users_path
+
+ wait_for_requests
+
+ expect(page.find("[data-testid='user-group-count-#{current_user.id}']").text).to eq("2")
+ end
+ end
+ end
+ end
+
def click_user_dropdown_toggle(user_id)
page.within("[data-testid='user-actions-#{user_id}']") do
find("[data-testid='dropdown-toggle']").click
diff --git a/spec/features/projects/new_project_from_template_spec.rb b/spec/features/projects/new_project_from_template_spec.rb
new file mode 100644
index 00000000000..1c8647d859a
--- /dev/null
+++ b/spec/features/projects/new_project_from_template_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'New project from template', :js do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+
+ visit new_project_path
+ end
+
+ context 'create from template' do
+ before do
+ page.find('a[href="#create_from_template"]').click
+ wait_for_requests
+ end
+
+ it 'shows template tabs' do
+ page.within('#create-from-template-pane') do
+ expect(page).to have_link('Built-in', href: '#built-in')
+ end
+ end
+ end
+end
diff --git a/spec/frontend/admin/users/components/users_table_spec.js b/spec/frontend/admin/users/components/users_table_spec.js
index 424b0deebd3..708c9e1979e 100644
--- a/spec/frontend/admin/users/components/users_table_spec.js
+++ b/spec/frontend/admin/users/components/users_table_spec.js
@@ -1,16 +1,36 @@
-import { GlTable } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { GlTable, GlSkeletonLoader } from '@gitlab/ui';
+import { createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import AdminUserActions from '~/admin/users/components/user_actions.vue';
import AdminUserAvatar from '~/admin/users/components/user_avatar.vue';
import AdminUsersTable from '~/admin/users/components/users_table.vue';
+import getUsersGroupCountsQuery from '~/admin/users/graphql/queries/get_users_group_counts.query.graphql';
+import createFlash from '~/flash';
import AdminUserDate from '~/vue_shared/components/user_date.vue';
-import { users, paths } from '../mock_data';
+import { users, paths, createGroupCountResponse } from '../mock_data';
+
+jest.mock('~/flash');
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
describe('AdminUsersTable component', () => {
let wrapper;
+ const user = users[0];
+ const createFetchGroupCount = (data) =>
+ jest.fn().mockResolvedValue(createGroupCountResponse(data));
+ const fetchGroupCountsLoading = jest.fn().mockResolvedValue(new Promise(() => {}));
+ const fetchGroupCountsError = jest.fn().mockRejectedValue(new Error('Network error'));
+ const fetchGroupCountsResponse = createFetchGroupCount([{ id: user.id, groupCount: 5 }]);
+
+ const findUserGroupCount = (id) => wrapper.findByTestId(`user-group-count-${id}`);
+ const findUserGroupCountLoader = (id) => findUserGroupCount(id).find(GlSkeletonLoader);
const getCellByLabel = (trIdx, label) => {
return wrapper
.find(GlTable)
@@ -20,8 +40,16 @@ describe('AdminUsersTable component', () => {
.find(`[data-label="${label}"][role="cell"]`);
};
- const initComponent = (props = {}) => {
- wrapper = mount(AdminUsersTable, {
+ function createMockApolloProvider(resolverMock) {
+ const requestHandlers = [[getUsersGroupCountsQuery, resolverMock]];
+
+ return createMockApollo(requestHandlers);
+ }
+
+ const initComponent = (props = {}, resolverMock = fetchGroupCountsResponse) => {
+ wrapper = mountExtended(AdminUsersTable, {
+ localVue,
+ apolloProvider: createMockApolloProvider(resolverMock),
propsData: {
users,
paths,
@@ -36,8 +64,6 @@ describe('AdminUsersTable component', () => {
});
describe('when there are users', () => {
- const user = users[0];
-
beforeEach(() => {
initComponent();
});
@@ -69,4 +95,51 @@ describe('AdminUsersTable component', () => {
expect(wrapper.text()).toContain('No users found');
});
});
+
+ describe('group counts', () => {
+ describe('when fetching the data', () => {
+ beforeEach(() => {
+ initComponent({}, fetchGroupCountsLoading);
+ });
+
+ it('renders a loader for each user', () => {
+ expect(findUserGroupCountLoader(user.id).exists()).toBe(true);
+ });
+ });
+
+ describe('when the data has been fetched', () => {
+ beforeEach(() => {
+ initComponent();
+ });
+
+ it("renders the user's group count", () => {
+ expect(findUserGroupCount(user.id).text()).toBe('5');
+ });
+
+ describe("and a user's group count is null", () => {
+ beforeEach(() => {
+ initComponent({}, createFetchGroupCount([{ id: user.id, groupCount: null }]));
+ });
+
+ it("renders the user's group count as 0", () => {
+ expect(findUserGroupCount(user.id).text()).toBe('0');
+ });
+ });
+ });
+
+ describe('when there is an error while fetching the data', () => {
+ beforeEach(() => {
+ initComponent({}, fetchGroupCountsError);
+ });
+
+ it('creates a flash message and captures the error', () => {
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'Could not load user group counts. Please refresh the page to try again.',
+ captureError: true,
+ error: expect.any(Error),
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/admin/users/mock_data.js b/spec/frontend/admin/users/mock_data.js
index c3918ef5173..4689ab36773 100644
--- a/spec/frontend/admin/users/mock_data.js
+++ b/spec/frontend/admin/users/mock_data.js
@@ -10,7 +10,7 @@ export const users = [
'https://secure.gravatar.com/avatar/054f062d8b1a42b123f17e13a173cda8?s=80\\u0026d=identicon',
badges: [
{ text: 'Admin', variant: 'success' },
- { text: "It's you!", variant: null },
+ { text: "It's you!", variant: 'muted' },
],
projectsCount: 0,
actions: [],
@@ -31,3 +31,16 @@ export const paths = {
deleteWithContributions: '/admin/users/id',
adminUser: '/admin/users/id',
};
+
+export const createGroupCountResponse = (groupCounts) => ({
+ data: {
+ users: {
+ nodes: groupCounts.map(({ id, groupCount }) => ({
+ id: `gid://gitlab/User/${id}`,
+ groupCount,
+ __typename: 'UserCore',
+ })),
+ __typename: 'UserCoreConnection',
+ },
+ },
+});
diff --git a/spec/lib/gitlab/conan_token_spec.rb b/spec/lib/gitlab/conan_token_spec.rb
index 00683cf6e47..b6180f69044 100644
--- a/spec/lib/gitlab/conan_token_spec.rb
+++ b/spec/lib/gitlab/conan_token_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe Gitlab::ConanToken do
JSONWebToken::HMACToken.new(jwt_secret).tap do |jwt|
jwt['access_token'] = access_token_id
jwt['user_id'] = user_id || user_id
- jwt.expire_time = expire_time || jwt.issued_at + 1.hour
+ jwt.expire_time = expire_time || jwt.issued_at + ::Gitlab::ConanToken::CONAN_TOKEN_EXPIRE_TIME
end
end
@@ -75,7 +75,7 @@ RSpec.describe Gitlab::ConanToken do
it 'returns nil for expired JWT' do
jwt = build_jwt(access_token_id: 123,
user_id: 456,
- expire_time: Time.zone.now - 2.hours)
+ expire_time: Time.zone.now - (::Gitlab::ConanToken::CONAN_TOKEN_EXPIRE_TIME + 1.hour))
expect(described_class.decode(jwt.encoded)).to be_nil
end
diff --git a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
index a94fd6acd32..41a6c06f9c9 100644
--- a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
+++ b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
@@ -20,15 +20,34 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
end
describe '.default_settings_hash' do
- it 'returns empty defaults' do
+ it 'returns defaults for all keys' do
settings = described_class.default_settings_hash
- expect(settings['enabled']).to be_falsey
+ expect(settings['enabled']).to be_truthy
expect(settings['report_only']).to be_falsey
- described_class::DIRECTIVES.each do |directive|
- expect(settings['directives'].has_key?(directive)).to be_truthy
- expect(settings['directives'][directive]).to be_nil
+ directives = settings['directives']
+ directive_names = (described_class::DIRECTIVES - ['report_uri'])
+ directive_names.each do |directive|
+ expect(directives.has_key?(directive)).to be_truthy
+ expect(directives[directive]).to be_truthy
+ end
+
+ expect(directives.has_key?('report_uri')).to be_truthy
+ expect(directives['report_uri']).to be_nil
+ end
+
+ context 'when GITLAB_CDN_HOST is set' do
+ before do
+ stub_env('GITLAB_CDN_HOST', 'https://example.com')
+ end
+
+ it 'adds GITLAB_CDN_HOST to CSP' do
+ settings = described_class.default_settings_hash
+ directives = settings['directives']
+
+ expect(directives['script_src']).to eq("'strict-dynamic' 'self' 'unsafe-inline' 'unsafe-eval' https://www.recaptcha.net https://apis.google.com https://example.com")
+ expect(directives['style_src']).to eq("'self' 'unsafe-inline' https://example.com")
end
end
end
diff --git a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb
index 1afe8eafa5f..c938c6432fe 100644
--- a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb
@@ -106,7 +106,7 @@ RSpec.shared_examples 'conan authenticate endpoint' do
expect(payload['user_id']).to eq(personal_access_token.user_id)
duration = payload['exp'] - payload['iat']
- expect(duration).to eq(1.hour)
+ expect(duration).to eq(::Gitlab::ConanToken::CONAN_TOKEN_EXPIRE_TIME)
end
end
end
diff --git a/spec/views/layouts/_head.html.haml_spec.rb b/spec/views/layouts/_head.html.haml_spec.rb
index 6752bdc8337..ef0bd97cbcf 100644
--- a/spec/views/layouts/_head.html.haml_spec.rb
+++ b/spec/views/layouts/_head.html.haml_spec.rb
@@ -62,6 +62,12 @@ RSpec.describe 'layouts/_head' do
expect(rendered).to match('<link rel="stylesheet" media="print" href="/stylesheets/highlight/themes/solarised-light.css" />')
end
+ it 'preloads Monaco' do
+ render
+
+ expect(rendered).to match('<link rel="preload" href="/assets/webpack/monaco.chunk.js" as="script" type="text/javascript">')
+ end
+
context 'when an asset_host is set and snowplow url is set' do
let(:asset_host) { 'http://test.host' }
let(:snowplow_collector_hostname) { 'www.snow.plow' }