summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue2
-rw-r--r--app/assets/javascripts/graphql_shared/queries/users_search.query.graphql9
-rw-r--r--app/assets/javascripts/layout_nav.js29
-rw-r--r--app/assets/javascripts/merge_request.js12
-rw-r--r--app/assets/javascripts/notes/components/multiline_comment_utils.js10
-rw-r--r--app/assets/javascripts/whats_new/index.js20
-rw-r--r--app/assets/javascripts/whats_new/utils/notification.js17
-rw-r--r--app/assets/stylesheets/framework/animations.scss3
-rw-r--r--app/assets/stylesheets/framework/header.scss11
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss6
-rw-r--r--app/assets/stylesheets/pages/issuable.scss18
-rw-r--r--app/assets/stylesheets/themes/theme_helper.scss29
-rw-r--r--app/controllers/concerns/dependency_proxy/auth.rb43
-rw-r--r--app/controllers/concerns/dependency_proxy/group_access.rb26
-rw-r--r--app/controllers/concerns/dependency_proxy_access.rb24
-rw-r--r--app/controllers/groups/dependency_proxies_controller.rb2
-rw-r--r--app/controllers/groups/dependency_proxy_auth_controller.rb11
-rw-r--r--app/controllers/groups/dependency_proxy_for_containers_controller.rb5
-rw-r--r--app/controllers/jwt_controller.rb3
-rw-r--r--app/controllers/profiles/gpg_keys_controller.rb19
-rw-r--r--app/controllers/projects/jobs_controller.rb2
-rw-r--r--app/controllers/repositories/git_http_controller.rb2
-rw-r--r--app/helpers/issuables_helper.rb6
-rw-r--r--app/helpers/merge_requests_helper.rb19
-rw-r--r--app/models/dependency_proxy/registry.rb9
-rw-r--r--app/models/namespace_onboarding_action.rb8
-rw-r--r--app/models/user_detail.rb2
-rw-r--r--app/services/auth/dependency_proxy_authentication_service.rb43
-rw-r--r--app/services/dependency_proxy/auth_token_service.rb21
-rw-r--r--app/services/onboarding_progress_service.rb11
-rw-r--r--app/services/post_receive_service.rb6
-rw-r--r--app/views/groups/dependency_proxies/show.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml1
-rw-r--r--app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml37
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml10
-rw-r--r--app/views/shared/issuable/_close_reopen_draft_report_toggle.html.haml37
-rw-r--r--changelogs/unreleased/277130-add-a-new-column-called-finding_uuid-into-the-vulnerability_feedba.yml5
-rw-r--r--changelogs/unreleased/281727-feature-ci_job_line_links-default-true.yml5
-rw-r--r--changelogs/unreleased/bump-managed-cluster-apps-v0-36-0.yml5
-rw-r--r--changelogs/unreleased/dblessing_scim_provisioned_user.yml5
-rw-r--r--changelogs/unreleased/gpg-keys-publicly-accessible.yml5
-rw-r--r--changelogs/unreleased/jdb-fix-comment-highlighting-unified-components.yml5
-rw-r--r--changelogs/unreleased/update-internal-ids-last-value-for-epics.yml5
-rw-r--r--config/feature_flags/development/ci_job_line_links.yml2
-rw-r--r--config/feature_flags/development/dependency_proxy_for_private_groups.yml8
-rw-r--r--config/routes/group.rb2
-rw-r--r--config/routes/user.rb5
-rw-r--r--db/migrate/20201123161611_add_provisioned_by_group_to_user_details.rb29
-rw-r--r--db/migrate/20201201163227_add_finding_uuid_to_vulnerability_feedback.rb10
-rw-r--r--db/migrate/20201202081429_update_internal_ids_last_value_for_epics.rb27
-rw-r--r--db/schema_migrations/202011231616111
-rw-r--r--db/schema_migrations/202012011632271
-rw-r--r--db/schema_migrations/202012020814291
-rw-r--r--db/structure.sql9
-rw-r--r--doc/administration/geo/disaster_recovery/index.md1
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql56
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json173
-rw-r--r--doc/api/graphql/reference/index.md10
-rw-r--r--doc/development/documentation/styleguide/index.md2
-rw-r--r--doc/topics/autodevops/requirements.md16
-rw-r--r--doc/user/packages/dependency_proxy/index.md56
-rw-r--r--lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml2
-rw-r--r--locale/gitlab.pot36
-rw-r--r--rubocop/rubocop-migrations.yml1
-rwxr-xr-xscripts/update-workhorse3
-rw-r--r--spec/controllers/groups/dependency_proxy_auth_controller_spec.rb79
-rw-r--r--spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb79
-rw-r--r--spec/controllers/profiles/gpg_keys_controller_spec.rb136
-rw-r--r--spec/controllers/repositories/git_http_controller_spec.rb8
-rw-r--r--spec/factories/gpg_keys.rb5
-rw-r--r--spec/features/groups/dependency_proxy_spec.rb16
-rw-r--r--spec/features/merge_request/close_reopen_report_toggle_spec.rb (renamed from spec/features/issuables/close_reopen_report_toggle_spec.rb)55
-rw-r--r--spec/features/merge_request/merge_request_discussion_lock_spec.rb (renamed from spec/features/issuables/merge_request_discussion_lock_spec.rb)0
-rw-r--r--spec/features/merge_request/user_reopens_merge_request_spec.rb2
-rw-r--r--spec/features/merge_request/user_squashes_merge_request_spec.rb (renamed from spec/features/merge_requests/user_squashes_merge_request_spec.rb)0
-rw-r--r--spec/features/merge_request/user_views_diffs_commit_spec.rb (renamed from spec/features/merge_requests/user_views_diffs_commit_spec.rb)0
-rw-r--r--spec/features/merge_requests/user_sees_empty_state_spec.rb (renamed from spec/features/merge_request/user_sees_empty_state_spec.rb)0
-rw-r--r--spec/frontend/fixtures/static/whats_new_notification.html6
-rw-r--r--spec/frontend/helpers/vuex_action_helper.js41
-rw-r--r--spec/frontend/helpers/vuex_action_helper_spec.js264
-rw-r--r--spec/frontend/notes/components/multiline_comment_utils_spec.js13
-rw-r--r--spec/frontend/whats_new/utils/notification_spec.js55
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb20
-rw-r--r--spec/migrations/update_internal_ids_last_value_for_epics_spec.rb30
-rw-r--r--spec/models/dependency_proxy/registry_spec.rb7
-rw-r--r--spec/models/namespace_onboarding_action_spec.rb8
-rw-r--r--spec/requests/jwt_controller_spec.rb318
-rw-r--r--spec/routing/group_routing_spec.rb4
-rw-r--r--spec/routing/routing_spec.rb24
-rw-r--r--spec/services/auth/dependency_proxy_authentication_service_spec.rb46
-rw-r--r--spec/services/dependency_proxy/auth_token_service_spec.rb37
-rw-r--r--spec/services/onboarding_progress_service_spec.rb19
-rw-r--r--spec/services/post_receive_service_spec.rb13
-rw-r--r--spec/support/helpers/dependency_proxy_helpers.rb7
-rw-r--r--spec/support/helpers/gpg_helpers.rb139
95 files changed, 1923 insertions, 509 deletions
diff --git a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
index 06f436adb8e..6fee40fb061 100644
--- a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
+++ b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
@@ -107,7 +107,7 @@ export default {
v-if="!popoverDismissed"
show
:target="target"
- placement="rightbottom"
+ placement="right"
trigger="manual"
container="viewport"
:css-classes="['suggest-gitlab-ci-yml', 'ml-4']"
diff --git a/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql b/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql
new file mode 100644
index 00000000000..b64ceb8e2c9
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql
@@ -0,0 +1,9 @@
+#import "../fragments/user.fragment.graphql"
+
+query usersSearch($search: String!) {
+ users(search: $search) {
+ nodes {
+ ...User
+ }
+ }
+}
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index 4d2955a8d3d..ab83f1ecc14 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import ContextualSidebar from './contextual_sidebar';
import initFlyOutNav from './fly_out_nav';
+import { setNotification } from './whats_new/utils/notification';
function hideEndFade($scrollingTabs) {
$scrollingTabs.each(function scrollTabsLoop() {
@@ -14,25 +15,17 @@ function hideEndFade($scrollingTabs) {
function initDeferred() {
$(document).trigger('init.scrolling-tabs');
- const whatsNewTriggerEl = document.querySelector('.js-whats-new-trigger');
- if (whatsNewTriggerEl) {
- const storageKey = whatsNewTriggerEl.getAttribute('data-storage-key');
+ const appEl = document.getElementById('whats-new-app');
+ if (!appEl) return;
- $('.header-help').on('show.bs.dropdown', () => {
- const displayNotification = JSON.parse(localStorage.getItem(storageKey));
- if (displayNotification === false) {
- $('.js-whats-new-notification-count').remove();
- }
- });
-
- whatsNewTriggerEl.addEventListener('click', () => {
- import(/* webpackChunkName: 'whatsNewApp' */ '~/whats_new')
- .then(({ default: initWhatsNew }) => {
- initWhatsNew();
- })
- .catch(() => {});
- });
- }
+ setNotification(appEl);
+ document.querySelector('.js-whats-new-trigger').addEventListener('click', () => {
+ import(/* webpackChunkName: 'whatsNewApp' */ '~/whats_new')
+ .then(({ default: initWhatsNew }) => {
+ initWhatsNew(appEl);
+ })
+ .catch(() => {});
+ });
}
export default function initLayoutNav() {
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index fe4e2cee69f..344f8dee5ea 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -102,14 +102,6 @@ MergeRequest.prototype.initMRBtnListeners = function() {
return $('.btn-close, .btn-reopen').on('click', function(e) {
const $this = $(this);
const shouldSubmit = $this.hasClass('btn-comment');
- if ($this.hasClass('js-btn-issue-action')) {
- const url = $this.data('endpoint');
- return axios
- .put(url)
- .then(() => window.location.reload())
- .catch(() => createFlash(__('Something went wrong.')));
- }
-
if (shouldSubmit && $this.data('submitted')) {
return;
}
@@ -171,10 +163,6 @@ MergeRequest.decreaseCounter = function(by = 1) {
MergeRequest.hideCloseButton = function() {
const el = document.querySelector('.merge-request .js-issuable-actions');
- const closeDropdownItem = el.querySelector('li.close-item');
- if (closeDropdownItem) {
- closeDropdownItem.classList.add('hidden');
- }
// Dropdown for mobile screen
el.querySelector('li.js-close-item').classList.add('hidden');
};
diff --git a/app/assets/javascripts/notes/components/multiline_comment_utils.js b/app/assets/javascripts/notes/components/multiline_comment_utils.js
index dbae10c8f6c..2451400e980 100644
--- a/app/assets/javascripts/notes/components/multiline_comment_utils.js
+++ b/app/assets/javascripts/notes/components/multiline_comment_utils.js
@@ -103,9 +103,15 @@ export function getCommentedLines(selectedCommentPosition, diffLines) {
};
}
+ const findLineCodeIndex = line => position => {
+ return [position.line_code, position.left?.line_code, position.right?.line_code].includes(
+ line.line_code,
+ );
+ };
+
const { start, end } = selectedCommentPosition;
- const startLine = diffLines.findIndex(l => l.line_code === start.line_code);
- const endLine = diffLines.findIndex(l => l.line_code === end.line_code);
+ const startLine = diffLines.findIndex(findLineCodeIndex(start));
+ const endLine = diffLines.findIndex(findLineCodeIndex(end));
return { startLine, endLine };
}
diff --git a/app/assets/javascripts/whats_new/index.js b/app/assets/javascripts/whats_new/index.js
index a57c9718156..2b9e7a2815e 100644
--- a/app/assets/javascripts/whats_new/index.js
+++ b/app/assets/javascripts/whats_new/index.js
@@ -1,26 +1,34 @@
import Vue from 'vue';
+import { mapState } from 'vuex';
import App from './components/app.vue';
import store from './store';
+import { getStorageKey, setNotification } from './utils/notification';
let whatsNewApp;
-export default () => {
+export default el => {
if (whatsNewApp) {
store.dispatch('openDrawer');
} else {
- const whatsNewElm = document.getElementById('whats-new-app');
+ const storageKey = getStorageKey(el);
whatsNewApp = new Vue({
- el: whatsNewElm,
+ el,
store,
components: {
App,
},
+ computed: {
+ ...mapState(['open']),
+ },
+ watch: {
+ open() {
+ setNotification(el);
+ },
+ },
render(createElement) {
return createElement('app', {
- props: {
- storageKey: whatsNewElm.getAttribute('data-storage-key'),
- },
+ props: { storageKey },
});
},
});
diff --git a/app/assets/javascripts/whats_new/utils/notification.js b/app/assets/javascripts/whats_new/utils/notification.js
new file mode 100644
index 00000000000..f261a089554
--- /dev/null
+++ b/app/assets/javascripts/whats_new/utils/notification.js
@@ -0,0 +1,17 @@
+export const getStorageKey = appEl => appEl.getAttribute('data-storage-key');
+
+export const setNotification = appEl => {
+ const storageKey = getStorageKey(appEl);
+ const notificationEl = document.querySelector('.header-help');
+ let notificationCountEl = notificationEl.querySelector('.js-whats-new-notification-count');
+
+ if (JSON.parse(localStorage.getItem(storageKey)) === false) {
+ notificationEl.classList.remove('with-notifications');
+ if (notificationCountEl) {
+ notificationCountEl.parentElement.removeChild(notificationCountEl);
+ notificationCountEl = null;
+ }
+ } else {
+ notificationEl.classList.add('with-notifications');
+ }
+};
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index 196fb3a7088..a93c70c75d3 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -103,7 +103,8 @@
@include transition(color);
}
-a {
+a,
+.notification-dot {
@include transition(background-color, color, border);
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 52319d9658b..0286c2f517b 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -556,12 +556,17 @@
border: 1px solid $gray-normal;
}
-.header-user-notification-dot {
+.notification-dot {
background-color: $orange-300;
height: 12px;
width: 12px;
- right: 8px;
- top: -8px;
+ margin-top: -15px;
+ pointer-events: none;
+ visibility: hidden;
+}
+
+.with-notifications .notification-dot {
+ visibility: visible;
}
.with-performance-bar .navbar-gitlab {
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index f357d508d5d..f237d57aa88 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -41,12 +41,6 @@
@include media-breakpoint-down(xs) {
width: 100%;
margin-top: 10px;
-
- > .issue-btn-group {
- > .btn {
- width: 100%;
- }
- }
}
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 57725aa3002..e5528c25e82 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -890,24 +890,6 @@
}
}
-.issuable-close-dropdown {
- .dropdown-menu {
- min-width: 270px;
- left: auto;
- right: 0;
- }
-
- .description {
- .text {
- margin: 0;
- }
- }
-
- .dropdown-toggle > .icon {
- margin: 0 3px;
- }
-}
-
/*
* Following overrides are done to prevent
* legacy dropdown styles from influencing
diff --git a/app/assets/stylesheets/themes/theme_helper.scss b/app/assets/stylesheets/themes/theme_helper.scss
index 85115cfd5d9..417377b514e 100644
--- a/app/assets/stylesheets/themes/theme_helper.scss
+++ b/app/assets/stylesheets/themes/theme_helper.scss
@@ -64,14 +64,20 @@
color: $search-and-nav-links;
> a {
+ .notification-dot {
+ border: 2px solid $nav-svg-color;
+ }
+
+ &.header-help-dropdown-toggle {
+ .notification-dot {
+ background-color: $search-and-nav-links;
+ }
+ }
+
&.header-user-dropdown-toggle {
.header-user-avatar {
border-color: $search-and-nav-links;
}
-
- .header-user-notification-dot {
- border: 2px solid $nav-svg-color;
- }
}
&:hover,
@@ -84,9 +90,14 @@
fill: currentColor;
}
- &.header-user-dropdown-toggle .header-user-notification-dot {
+ .notification-dot {
+ will-change: border-color, background-color;
border-color: $nav-svg-color + 33;
}
+
+ &.header-help-dropdown-toggle .notification-dot {
+ background-color: $white;
+ }
}
}
@@ -101,9 +112,15 @@
}
}
- &.header-user-dropdown-toggle .header-user-notification-dot {
+ .notification-dot {
border-color: $white;
}
+
+ &.header-help-dropdown-toggle {
+ .notification-dot {
+ background-color: $nav-svg-color;
+ }
+ }
}
.impersonated-user,
diff --git a/app/controllers/concerns/dependency_proxy/auth.rb b/app/controllers/concerns/dependency_proxy/auth.rb
new file mode 100644
index 00000000000..22618ca6366
--- /dev/null
+++ b/app/controllers/concerns/dependency_proxy/auth.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module DependencyProxy
+ module Auth
+ extend ActiveSupport::Concern
+
+ included do
+ # We disable `authenticate_user!` since the `DependencyProxy::Auth` performs auth using JWT token
+ skip_before_action :authenticate_user!, raise: false
+ prepend_before_action :authenticate_user_from_jwt_token!
+ end
+
+ def authenticate_user_from_jwt_token!
+ return unless dependency_proxy_for_private_groups?
+
+ authenticate_with_http_token do |token, _|
+ user = user_from_token(token)
+ sign_in(user) if user
+ end
+
+ request_bearer_token! unless current_user
+ end
+
+ private
+
+ def dependency_proxy_for_private_groups?
+ Feature.enabled?(:dependency_proxy_for_private_groups, default_enabled: false)
+ end
+
+ def request_bearer_token!
+ # unfortunately, we cannot use https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token.html#method-i-authentication_request
+ response.headers['WWW-Authenticate'] = ::DependencyProxy::Registry.authenticate_header
+ render plain: '', status: :unauthorized
+ end
+
+ def user_from_token(token)
+ token_payload = DependencyProxy::AuthTokenService.decoded_token_payload(token)
+ User.find(token_payload['user_id'])
+ rescue JWT::DecodeError, JWT::ExpiredSignature, JWT::ImmatureSignature
+ nil
+ end
+ end
+end
diff --git a/app/controllers/concerns/dependency_proxy/group_access.rb b/app/controllers/concerns/dependency_proxy/group_access.rb
new file mode 100644
index 00000000000..2a923d02752
--- /dev/null
+++ b/app/controllers/concerns/dependency_proxy/group_access.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module DependencyProxy
+ module GroupAccess
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :verify_dependency_proxy_enabled!
+ before_action :authorize_read_dependency_proxy!
+ end
+
+ private
+
+ def verify_dependency_proxy_enabled!
+ render_404 unless group.dependency_proxy_feature_available?
+ end
+
+ def authorize_read_dependency_proxy!
+ access_denied! unless can?(current_user, :read_dependency_proxy, group)
+ end
+
+ def authorize_admin_dependency_proxy!
+ access_denied! unless can?(current_user, :admin_dependency_proxy, group)
+ end
+ end
+end
diff --git a/app/controllers/concerns/dependency_proxy_access.rb b/app/controllers/concerns/dependency_proxy_access.rb
deleted file mode 100644
index 5036d0cfce4..00000000000
--- a/app/controllers/concerns/dependency_proxy_access.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# frozen_string_literal: true
-
-module DependencyProxyAccess
- extend ActiveSupport::Concern
-
- included do
- before_action :verify_dependency_proxy_enabled!
- before_action :authorize_read_dependency_proxy!
- end
-
- private
-
- def verify_dependency_proxy_enabled!
- render_404 unless group.dependency_proxy_feature_available?
- end
-
- def authorize_read_dependency_proxy!
- access_denied! unless can?(current_user, :read_dependency_proxy, group)
- end
-
- def authorize_admin_dependency_proxy!
- access_denied! unless can?(current_user, :admin_dependency_proxy, group)
- end
-end
diff --git a/app/controllers/groups/dependency_proxies_controller.rb b/app/controllers/groups/dependency_proxies_controller.rb
index 367dbafdd59..b896b240daf 100644
--- a/app/controllers/groups/dependency_proxies_controller.rb
+++ b/app/controllers/groups/dependency_proxies_controller.rb
@@ -2,7 +2,7 @@
module Groups
class DependencyProxiesController < Groups::ApplicationController
- include DependencyProxyAccess
+ include DependencyProxy::GroupAccess
before_action :authorize_admin_dependency_proxy!, only: :update
before_action :dependency_proxy
diff --git a/app/controllers/groups/dependency_proxy_auth_controller.rb b/app/controllers/groups/dependency_proxy_auth_controller.rb
new file mode 100644
index 00000000000..e3e9bd88e24
--- /dev/null
+++ b/app/controllers/groups/dependency_proxy_auth_controller.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class Groups::DependencyProxyAuthController < ApplicationController
+ include DependencyProxy::Auth
+
+ feature_category :dependency_proxy
+
+ def authenticate
+ render plain: '', status: :ok
+ end
+end
diff --git a/app/controllers/groups/dependency_proxy_for_containers_controller.rb b/app/controllers/groups/dependency_proxy_for_containers_controller.rb
index f46902ef90f..22aea424998 100644
--- a/app/controllers/groups/dependency_proxy_for_containers_controller.rb
+++ b/app/controllers/groups/dependency_proxy_for_containers_controller.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
class Groups::DependencyProxyForContainersController < Groups::ApplicationController
- include DependencyProxyAccess
+ include DependencyProxy::Auth
+ include DependencyProxy::GroupAccess
include SendFileUpload
before_action :ensure_token_granted!
@@ -9,7 +10,7 @@ class Groups::DependencyProxyForContainersController < Groups::ApplicationContro
attr_reader :token
- feature_category :package_registry
+ feature_category :dependency_proxy
def manifest
result = DependencyProxy::PullManifestService.new(image, tag, token).execute
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index 5199bb25c8c..85ee2204324 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -11,7 +11,8 @@ class JwtController < ApplicationController
feature_category :authentication_and_authorization
SERVICES = {
- Auth::ContainerRegistryAuthenticationService::AUDIENCE => Auth::ContainerRegistryAuthenticationService
+ ::Auth::ContainerRegistryAuthenticationService::AUDIENCE => ::Auth::ContainerRegistryAuthenticationService,
+ ::Auth::DependencyProxyAuthenticationService::AUDIENCE => ::Auth::DependencyProxyAuthenticationService
}.freeze
def auth
diff --git a/app/controllers/profiles/gpg_keys_controller.rb b/app/controllers/profiles/gpg_keys_controller.rb
index 7f04927f517..f1d4f9b2cc0 100644
--- a/app/controllers/profiles/gpg_keys_controller.rb
+++ b/app/controllers/profiles/gpg_keys_controller.rb
@@ -2,6 +2,7 @@
class Profiles::GpgKeysController < Profiles::ApplicationController
before_action :set_gpg_key, only: [:destroy, :revoke]
+ skip_before_action :authenticate_user!, only: [:get_keys]
feature_category :users
@@ -39,6 +40,24 @@ class Profiles::GpgKeysController < Profiles::ApplicationController
end
end
+ # Get all gpg keys of a user(params[:username]) in a text format
+ def get_keys
+ if params[:username].present?
+ begin
+ user = UserFinder.new(params[:username]).find_by_username
+ if user.present?
+ render plain: user.gpg_keys.select(&:verified?).map(&:key).join("\n")
+ else
+ render_404
+ end
+ rescue => e
+ render html: e.message
+ end
+ else
+ render_404
+ end
+ end
+
private
def gpg_key_params
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 1f998e0083a..4610756192a 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -15,7 +15,7 @@ class Projects::JobsController < Projects::ApplicationController
before_action :authorize_create_proxy_build!, only: :proxy_websocket_authorize
before_action :verify_proxy_request!, only: :proxy_websocket_authorize
before_action do
- push_frontend_feature_flag(:ci_job_line_links, @project)
+ push_frontend_feature_flag(:ci_job_line_links, @project, default_enabled: true)
end
before_action only: :index do
frontend_experimentation_tracking_data(:jobs_empty_state, 'click_button')
diff --git a/app/controllers/repositories/git_http_controller.rb b/app/controllers/repositories/git_http_controller.rb
index 639e403d797..3cf0a23b7f6 100644
--- a/app/controllers/repositories/git_http_controller.rb
+++ b/app/controllers/repositories/git_http_controller.rb
@@ -80,6 +80,8 @@ module Repositories
return if Gitlab::Database.read_only?
return unless repo_type.project?
+ OnboardingProgressService.new(project.namespace).execute(action: :git_read)
+
if Feature.enabled?(:project_statistics_sync, project, default_enabled: true)
Projects::FetchStatisticsIncrementService.new(project).execute
else
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 1f289265916..547977c01a9 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -319,12 +319,6 @@ module IssuablesHelper
issuable_path(issuable, close_reopen_params(issuable, :reopen))
end
- def toggle_draft_issuable_path(issuable)
- wip_event = issuable.work_in_progress? ? 'unwip' : 'wip'
-
- issuable_path(issuable, { merge_request: { wip_event: wip_event } })
- end
-
def issuable_path(issuable, *options)
polymorphic_path(issuable, *options)
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 9cb7edbaeb6..35ceddada5f 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -39,19 +39,6 @@ module MergeRequestsHelper
end
end
- def ci_build_details_path(merge_request)
- build_url = merge_request.source_project.ci_service.build_page(merge_request.diff_head_sha, merge_request.source_branch)
- return unless build_url
-
- parsed_url = URI.parse(build_url)
-
- unless parsed_url.userinfo.blank?
- parsed_url.userinfo = ''
- end
-
- parsed_url.to_s
- end
-
def merge_path_description(merge_request, separator)
if merge_request.for_fork?
"Project:Branches: #{@merge_request.source_project_path}:#{@merge_request.source_branch} #{separator} #{@merge_request.target_project.full_path}:#{@merge_request.target_branch}"
@@ -166,6 +153,12 @@ module MergeRequestsHelper
current_user.fork_of(project)
end
end
+
+ def toggle_draft_merge_request_path(issuable)
+ wip_event = issuable.work_in_progress? ? 'unwip' : 'wip'
+
+ issuable_path(issuable, { merge_request: { wip_event: wip_event } })
+ end
end
MergeRequestsHelper.prepend_if_ee('EE::MergeRequestsHelper')
diff --git a/app/models/dependency_proxy/registry.rb b/app/models/dependency_proxy/registry.rb
index 471d5be2600..6492acf325a 100644
--- a/app/models/dependency_proxy/registry.rb
+++ b/app/models/dependency_proxy/registry.rb
@@ -1,8 +1,9 @@
# frozen_string_literal: true
class DependencyProxy::Registry
- AUTH_URL = 'https://auth.docker.io'.freeze
- LIBRARY_URL = 'https://registry-1.docker.io/v2'.freeze
+ AUTH_URL = 'https://auth.docker.io'
+ LIBRARY_URL = 'https://registry-1.docker.io/v2'
+ PROXY_AUTH_URL = Gitlab::Utils.append_path(Gitlab.config.gitlab.url, "jwt/auth")
class << self
def auth_url(image)
@@ -17,6 +18,10 @@ class DependencyProxy::Registry
"#{LIBRARY_URL}/#{image_path(image)}/blobs/#{blob_sha}"
end
+ def authenticate_header
+ "Bearer realm=\"#{PROXY_AUTH_URL}\",service=\"#{::Auth::DependencyProxyAuthenticationService::AUDIENCE}\""
+ end
+
private
def image_path(image)
diff --git a/app/models/namespace_onboarding_action.rb b/app/models/namespace_onboarding_action.rb
index 7c3ab051d93..e1121279e2e 100644
--- a/app/models/namespace_onboarding_action.rb
+++ b/app/models/namespace_onboarding_action.rb
@@ -1,10 +1,14 @@
# frozen_string_literal: true
class NamespaceOnboardingAction < ApplicationRecord
- belongs_to :namespace
+ belongs_to :namespace, optional: false
+
+ validates :action, presence: true
ACTIONS = {
- subscription_created: 1
+ subscription_created: 1,
+ git_write: 2,
+ git_read: 4
}.freeze
enum action: ACTIONS
diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb
index 9674f9a41da..ef799b01452 100644
--- a/app/models/user_detail.rb
+++ b/app/models/user_detail.rb
@@ -31,3 +31,5 @@ class UserDetail < ApplicationRecord
self.bio = '' if bio_changed? && bio.nil?
end
end
+
+UserDetail.prepend_if_ee('EE::UserDetail')
diff --git a/app/services/auth/dependency_proxy_authentication_service.rb b/app/services/auth/dependency_proxy_authentication_service.rb
new file mode 100644
index 00000000000..1b8c16b7c79
--- /dev/null
+++ b/app/services/auth/dependency_proxy_authentication_service.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Auth
+ class DependencyProxyAuthenticationService < BaseService
+ AUDIENCE = 'dependency_proxy'
+ HMAC_KEY = 'gitlab-dependency-proxy'
+ DEFAULT_EXPIRE_TIME = 1.minute
+
+ def execute(authentication_abilities:)
+ return error('dependency proxy not enabled', 404) unless ::Gitlab.config.dependency_proxy.enabled
+ return error('access forbidden', 403) unless current_user
+
+ { token: authorized_token.encoded }
+ end
+
+ class << self
+ include ::Gitlab::Utils::StrongMemoize
+
+ def secret
+ strong_memoize(:secret) do
+ OpenSSL::HMAC.hexdigest(
+ 'sha256',
+ ::Settings.attr_encrypted_db_key_base,
+ HMAC_KEY
+ )
+ end
+ end
+
+ def token_expire_at
+ Time.current + Gitlab::CurrentSettings.container_registry_token_expire_delay.minutes
+ end
+ end
+
+ private
+
+ def authorized_token
+ JSONWebToken::HMACToken.new(self.class.secret).tap do |token|
+ token['user_id'] = current_user.id
+ token.expire_time = self.class.token_expire_at
+ end
+ end
+ end
+end
diff --git a/app/services/dependency_proxy/auth_token_service.rb b/app/services/dependency_proxy/auth_token_service.rb
new file mode 100644
index 00000000000..16279ed12b0
--- /dev/null
+++ b/app/services/dependency_proxy/auth_token_service.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module DependencyProxy
+ class AuthTokenService < DependencyProxy::BaseService
+ attr_reader :token
+
+ def initialize(token)
+ @token = token
+ end
+
+ def execute
+ JSONWebToken::HMACToken.decode(token, ::Auth::DependencyProxyAuthenticationService.secret).first
+ end
+
+ class << self
+ def decoded_token_payload(token)
+ self.new(token).execute
+ end
+ end
+ end
+end
diff --git a/app/services/onboarding_progress_service.rb b/app/services/onboarding_progress_service.rb
new file mode 100644
index 00000000000..c45edcaaf33
--- /dev/null
+++ b/app/services/onboarding_progress_service.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class OnboardingProgressService
+ def initialize(namespace)
+ @namespace = namespace
+ end
+
+ def execute(action:)
+ NamespaceOnboardingAction.create_action(@namespace, action)
+ end
+end
diff --git a/app/services/post_receive_service.rb b/app/services/post_receive_service.rb
index 79b613f6a88..bd9588844ad 100644
--- a/app/services/post_receive_service.rb
+++ b/app/services/post_receive_service.rb
@@ -40,6 +40,8 @@ class PostReceiveService
response.add_basic_message(redirect_message)
response.add_basic_message(project_created_message)
+
+ record_onboarding_progress
end
response
@@ -90,6 +92,10 @@ class PostReceiveService
banner&.message
end
+
+ def record_onboarding_progress
+ NamespaceOnboardingAction.create_action(project.namespace, :git_write)
+ end
end
PostReceiveService.prepend_if_ee('EE::PostReceiveService')
diff --git a/app/views/groups/dependency_proxies/show.html.haml b/app/views/groups/dependency_proxies/show.html.haml
index ff1312eb763..abbfb2d3b91 100644
--- a/app/views/groups/dependency_proxies/show.html.haml
+++ b/app/views/groups/dependency_proxies/show.html.haml
@@ -7,7 +7,7 @@
- link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('user/packages/dependency_proxy/index') }
= _('Create a local proxy for storing frequently used upstream images. %{link_start}Learn more%{link_end} about dependency proxies.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
-- if @group.public?
+- if Feature.enabled?(:dependency_proxy_for_private_groups, default_enabled: false) || @group.public?
- if can?(current_user, :admin_dependency_proxy, @group)
= form_for(@dependency_proxy, method: :put, url: group_dependency_proxy_path(@group)) do |f|
.form-group
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 794d1589172..8aba9426ec0 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -74,6 +74,7 @@
%span.gl-sr-only
= s_('Nav|Help')
= sprite_icon('question')
+ %span.notification-dot.rounded-circle.gl-absolute
= sprite_icon('chevron-down', css_class: 'caret-down')
.dropdown-menu.dropdown-menu-right
= render 'layouts/header/help_dropdown'
diff --git a/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml b/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
new file mode 100644
index 00000000000..3a8629b3b6e
--- /dev/null
+++ b/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
@@ -0,0 +1,37 @@
+- display_issuable_type = issuable_display_type(@merge_request)
+- button_action_class = @merge_request.closed? ? 'btn-default' : 'btn-warning btn-warning-secondary'
+- button_class = "btn gl-button #{!@merge_request.closed? && 'js-draft-toggle-button'}"
+- toggle_class = "btn gl-button dropdown-toggle"
+
+.float-left.btn-group.gl-ml-3.gl-display-none.gl-display-md-flex
+ = link_to @merge_request.closed? ? reopen_issuable_path(@merge_request) : toggle_draft_merge_request_path(@merge_request), method: :put, class: "#{button_class} #{button_action_class}" do
+ - if @merge_request.closed?
+ = _('Reopen')
+ = display_issuable_type
+ - else
+ = @merge_request.work_in_progress? ? _('Mark as ready') : _('Mark as draft')
+
+ - if !@merge_request.closed? || !issuable_author_is_current_user(@merge_request)
+ = button_tag type: 'button', class: "#{toggle_class} #{button_action_class}", data: { 'toggle' => 'dropdown' } do
+ %span.gl-sr-only= _('Toggle dropdown')
+ = sprite_icon "angle-down", size: 12
+
+ %ul.dropdown-menu.dropdown-menu-right
+ - if @merge_request.open?
+ %li
+ = link_to close_issuable_path(@merge_request), method: :put do
+ .description
+ %strong.title
+ = _('Close')
+ = display_issuable_type
+
+ - unless issuable_author_is_current_user(@merge_request)
+ - unless @merge_request.closed?
+ %li.divider.droplab-item-ignore
+
+ %li
+ %a{ href: new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request)) }
+ .description
+ %strong.title= _('Report abuse')
+ %p.text.gl-mb-0
+ = _('Report %{display_issuable_type} that are abusive, inappropriate or spam.') % { display_issuable_type: display_issuable_type.pluralize }
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index 5b30b6e3379..f7cc15cec5a 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -25,8 +25,8 @@
= sprite_icon('chevron-double-lg-left')
.detail-page-header-actions.js-issuable-actions
- .clearfix.issue-btn-group.dropdown
- %button.gl-button.btn.btn-default.float-left.gl-display-md-none{ type: "button", data: { toggle: "dropdown" } }
+ .clearfix.dropdown
+ %button.gl-button.btn.btn-default.float-left.gl-display-md-none.gl-w-full{ type: "button", data: { toggle: "dropdown" } }
Options
= sprite_icon('chevron-down', css_class: 'gl-text-gray-500')
.dropdown-menu.dropdown-menu-right
@@ -35,12 +35,12 @@
%li= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
- if @merge_request.opened?
%li
- = link_to @merge_request.work_in_progress? ? _('Mark as ready') : _('Mark as draft'), toggle_draft_issuable_path(@merge_request), method: :put, class: "js-draft-toggle-button"
+ = link_to @merge_request.work_in_progress? ? _('Mark as ready') : _('Mark as draft'), toggle_draft_merge_request_path(@merge_request), method: :put, class: "js-draft-toggle-button"
%li{ class: [merge_request_button_visibility(@merge_request, true), 'js-close-item'] }
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request'
- if can_reopen_merge_request
%li{ class: merge_request_button_visibility(@merge_request, false) }
- = link_to 'Reopen', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
+ = link_to 'Reopen', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, title: 'Reopen merge request'
- unless @merge_request.merged? || current_user == @merge_request.author
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request))
@@ -48,6 +48,6 @@
= link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "d-none d-md-block btn gl-button btn-grouped js-issuable-edit qa-edit-button"
- if can_update_merge_request && !are_close_and_open_buttons_hidden
- = render 'shared/issuable/close_reopen_draft_report_toggle', issuable: @merge_request
+ = render 'projects/merge_requests/close_reopen_draft_report_toggle'
- elsif !@merge_request.merged?
= link_to _('Report abuse'), new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request)), class: 'gl-display-none gl-display-md-block gl-button btn btn-warning-secondary float-right gl-ml-3', title: _('Report abuse')
diff --git a/app/views/shared/issuable/_close_reopen_draft_report_toggle.html.haml b/app/views/shared/issuable/_close_reopen_draft_report_toggle.html.haml
deleted file mode 100644
index 250e1516318..00000000000
--- a/app/views/shared/issuable/_close_reopen_draft_report_toggle.html.haml
+++ /dev/null
@@ -1,37 +0,0 @@
-- display_issuable_type = issuable_display_type(issuable)
-- button_action_class = issuable.closed? ? 'btn-default' : 'btn-warning btn-warning-secondary'
-- button_class = "btn gl-button #{!issuable.closed? && 'js-draft-toggle-button'}"
-- toggle_class = "btn gl-button dropdown-toggle"
-
-.float-left.btn-group.gl-ml-3.issuable-close-dropdown.d-none.d-md-inline-flex.js-issuable-close-dropdown
- = link_to issuable.closed? ? reopen_issuable_path(issuable) : toggle_draft_issuable_path(issuable), method: :put, class: "#{button_class} #{button_action_class}" do
- - if issuable.closed?
- = _('Reopen')
- = display_issuable_type
- - else
- = issuable.work_in_progress? ? _('Mark as ready') : _('Mark as draft')
-
- - if !issuable.closed? || !issuable_author_is_current_user(issuable)
- = button_tag type: 'button', class: "#{toggle_class} #{button_action_class}", data: { 'toggle' => 'dropdown' } do
- %span.sr-only= _('Toggle dropdown')
- = sprite_icon "angle-down", size: 12
-
- %ul.js-issuable-close-menu.dropdown-menu.dropdown-menu-right
- - if issuable.open?
- %li
- = link_to close_issuable_path(issuable), method: :put do
- .description
- %strong.title
- = _('Close')
- = display_issuable_type
-
- - unless issuable_author_is_current_user(issuable)
- - unless issuable.closed?
- %li.divider.droplab-item-ignore
-
- %li.report-item
- %a.report-abuse-link{ href: new_abuse_report_path(user_id: issuable.author.id, ref_url: merge_request_url(issuable)) }
- .description
- %strong.title= _('Report abuse')
- %p.text
- = _('Report %{display_issuable_type} that are abusive, inappropriate or spam.') % { display_issuable_type: display_issuable_type.pluralize }
diff --git a/changelogs/unreleased/277130-add-a-new-column-called-finding_uuid-into-the-vulnerability_feedba.yml b/changelogs/unreleased/277130-add-a-new-column-called-finding_uuid-into-the-vulnerability_feedba.yml
new file mode 100644
index 00000000000..06c4318aaa1
--- /dev/null
+++ b/changelogs/unreleased/277130-add-a-new-column-called-finding_uuid-into-the-vulnerability_feedba.yml
@@ -0,0 +1,5 @@
+---
+title: Add new column `finding_uuid` into `vulnerability_feedback` table
+merge_request: 48923
+author:
+type: changed
diff --git a/changelogs/unreleased/281727-feature-ci_job_line_links-default-true.yml b/changelogs/unreleased/281727-feature-ci_job_line_links-default-true.yml
new file mode 100644
index 00000000000..3243afcdbab
--- /dev/null
+++ b/changelogs/unreleased/281727-feature-ci_job_line_links-default-true.yml
@@ -0,0 +1,5 @@
+---
+title: Render http and https URLs as clickable links in Job logs
+merge_request: 48758
+author: Łukasz Groszkowski @falxcerebri
+type: added
diff --git a/changelogs/unreleased/bump-managed-cluster-apps-v0-36-0.yml b/changelogs/unreleased/bump-managed-cluster-apps-v0-36-0.yml
new file mode 100644
index 00000000000..76351175ada
--- /dev/null
+++ b/changelogs/unreleased/bump-managed-cluster-apps-v0-36-0.yml
@@ -0,0 +1,5 @@
+---
+title: Bumps Managed-Cluster-Applications CI template to v0.36.0, which upgrades Runner
+merge_request: 48444
+author:
+type: changed
diff --git a/changelogs/unreleased/dblessing_scim_provisioned_user.yml b/changelogs/unreleased/dblessing_scim_provisioned_user.yml
new file mode 100644
index 00000000000..b95e6f36607
--- /dev/null
+++ b/changelogs/unreleased/dblessing_scim_provisioned_user.yml
@@ -0,0 +1,5 @@
+---
+title: Mark SCIM-created accounts as provisioned by group
+merge_request: 48483
+author:
+type: added
diff --git a/changelogs/unreleased/gpg-keys-publicly-accessible.yml b/changelogs/unreleased/gpg-keys-publicly-accessible.yml
new file mode 100644
index 00000000000..654419464bd
--- /dev/null
+++ b/changelogs/unreleased/gpg-keys-publicly-accessible.yml
@@ -0,0 +1,5 @@
+---
+title: Add an URL to get user's GPG key if registerd
+merge_request: 48321
+author: Shimura Rin @blackenedgold
+type: added
diff --git a/changelogs/unreleased/jdb-fix-comment-highlighting-unified-components.yml b/changelogs/unreleased/jdb-fix-comment-highlighting-unified-components.yml
new file mode 100644
index 00000000000..1c08f1e9a76
--- /dev/null
+++ b/changelogs/unreleased/jdb-fix-comment-highlighting-unified-components.yml
@@ -0,0 +1,5 @@
+---
+title: Fix comment highlighting for unified diff components
+merge_request: 49061
+author:
+type: fixed
diff --git a/changelogs/unreleased/update-internal-ids-last-value-for-epics.yml b/changelogs/unreleased/update-internal-ids-last-value-for-epics.yml
new file mode 100644
index 00000000000..3b2de770efb
--- /dev/null
+++ b/changelogs/unreleased/update-internal-ids-last-value-for-epics.yml
@@ -0,0 +1,5 @@
+---
+title: Fix last_value record in internal_ids for epics
+merge_request: 48988
+author:
+type: fixed
diff --git a/config/feature_flags/development/ci_job_line_links.yml b/config/feature_flags/development/ci_job_line_links.yml
index 458c0afb6d2..e14dd82833e 100644
--- a/config/feature_flags/development/ci_job_line_links.yml
+++ b/config/feature_flags/development/ci_job_line_links.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/281727
milestone: '13.6'
type: development
group: group::continuous integration
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/dependency_proxy_for_private_groups.yml b/config/feature_flags/development/dependency_proxy_for_private_groups.yml
new file mode 100644
index 00000000000..60dc1b6f928
--- /dev/null
+++ b/config/feature_flags/development/dependency_proxy_for_private_groups.yml
@@ -0,0 +1,8 @@
+---
+name: dependency_proxy_for_private_groups
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46042
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/276777
+milestone: '13.7'
+type: development
+group: group::package
+default_enabled: false
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 3b52aae52e2..38c04369d2f 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -125,7 +125,7 @@ end
# Dependency proxy for containers
# Because docker adds v2 prefix to URI this need to be outside of usual group routes
scope format: false do
- get 'v2', to: proc { [200, {}, ['']] } # rubocop:disable Cop/PutGroupRoutesUnderScope
+ get 'v2' => 'groups/dependency_proxy_auth#authenticate' # rubocop:disable Cop/PutGroupRoutesUnderScope
constraints image: Gitlab::PathRegex.container_image_regex, sha: Gitlab::PathRegex.container_image_blob_sha_regex do
get 'v2/*group_id/dependency_proxy/containers/*image/manifests/*tag' => 'groups/dependency_proxy_for_containers#manifest' # rubocop:todo Cop/PutGroupRoutesUnderScope
diff --git a/config/routes/user.rb b/config/routes/user.rb
index 63329277e33..7af4bf2ac2a 100644
--- a/config/routes/user.rb
+++ b/config/routes/user.rb
@@ -54,9 +54,12 @@ scope(constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }) d
end
constraints(::Constraints::UserUrlConstrainer.new) do
- # Get all keys of user
+ # Get all SSH keys of user
get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }
+ # Get all GPG keys of user
+ get ':username.gpg' => 'profiles/gpg_keys#get_keys', constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }
+
scope(path: ':username',
as: :user,
constraints: { username: Gitlab::PathRegex.root_namespace_route_regex },
diff --git a/db/migrate/20201123161611_add_provisioned_by_group_to_user_details.rb b/db/migrate/20201123161611_add_provisioned_by_group_to_user_details.rb
new file mode 100644
index 00000000000..6e4d0e84509
--- /dev/null
+++ b/db/migrate/20201123161611_add_provisioned_by_group_to_user_details.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+class AddProvisionedByGroupToUserDetails < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ INDEX_NAME = 'index_user_details_on_provisioned_by_group_id'
+
+ disable_ddl_transaction!
+
+ def up
+ unless column_exists?(:user_details, :provisioned_by_group_id)
+ with_lock_retries { add_column(:user_details, :provisioned_by_group_id, :integer, limit: 8) }
+ end
+
+ add_concurrent_index :user_details, :provisioned_by_group_id, name: INDEX_NAME
+ add_concurrent_foreign_key :user_details, :namespaces, column: :provisioned_by_group_id, on_delete: :nullify
+ end
+
+ def down
+ with_lock_retries { remove_foreign_key_without_error :user_details, column: :provisioned_by_group_id }
+
+ remove_concurrent_index_by_name :user_details, INDEX_NAME
+
+ if column_exists?(:user_details, :provisioned_by_group_id)
+ with_lock_retries { remove_column(:user_details, :provisioned_by_group_id) }
+ end
+ end
+end
diff --git a/db/migrate/20201201163227_add_finding_uuid_to_vulnerability_feedback.rb b/db/migrate/20201201163227_add_finding_uuid_to_vulnerability_feedback.rb
new file mode 100644
index 00000000000..a2e13806000
--- /dev/null
+++ b/db/migrate/20201201163227_add_finding_uuid_to_vulnerability_feedback.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class AddFindingUuidToVulnerabilityFeedback < ActiveRecord::Migration[6.0]
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ add_column :vulnerability_feedback, :finding_uuid, :uuid
+ end
+end
diff --git a/db/migrate/20201202081429_update_internal_ids_last_value_for_epics.rb b/db/migrate/20201202081429_update_internal_ids_last_value_for_epics.rb
new file mode 100644
index 00000000000..7f6aefde7da
--- /dev/null
+++ b/db/migrate/20201202081429_update_internal_ids_last_value_for_epics.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class UpdateInternalIdsLastValueForEpics < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def up
+ ApplicationRecord.connection.execute(<<-SQL.squish)
+ UPDATE internal_ids
+ SET last_value = epics_max_iids.maximum_iid
+ FROM
+ (
+ SELECT
+ MAX(epics.iid) AS maximum_iid,
+ epics.group_id AS epics_group_id
+ FROM epics
+ GROUP BY epics.group_id
+ ) epics_max_iids
+ WHERE internal_ids.last_value < epics_max_iids.maximum_iid
+ AND namespace_id = epics_max_iids.epics_group_id
+ AND internal_ids.usage = 4
+ SQL
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/schema_migrations/20201123161611 b/db/schema_migrations/20201123161611
new file mode 100644
index 00000000000..bcd28f2b4da
--- /dev/null
+++ b/db/schema_migrations/20201123161611
@@ -0,0 +1 @@
+9d69938cda6db1510ed17d087cc1a582af1e5482d65e4fb457e34011e09c3469 \ No newline at end of file
diff --git a/db/schema_migrations/20201201163227 b/db/schema_migrations/20201201163227
new file mode 100644
index 00000000000..0366850ee2f
--- /dev/null
+++ b/db/schema_migrations/20201201163227
@@ -0,0 +1 @@
+cc978ac56ed177575706436c52125b51915dff97a20ed47ae0c7b16caa837313 \ No newline at end of file
diff --git a/db/schema_migrations/20201202081429 b/db/schema_migrations/20201202081429
new file mode 100644
index 00000000000..2a8e170c0ff
--- /dev/null
+++ b/db/schema_migrations/20201202081429
@@ -0,0 +1 @@
+cbc6bfa122167e9a46edaa14351a73eeb10586fa0eb82f231c792384c9d7986c \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 2a0b85144f2..94f92b2af16 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -16998,6 +16998,7 @@ CREATE TABLE user_details (
cached_markdown_version integer,
webauthn_xid text,
other_role text,
+ provisioned_by_group_id bigint,
CONSTRAINT check_245664af82 CHECK ((char_length(webauthn_xid) <= 100)),
CONSTRAINT check_b132136b01 CHECK ((char_length(other_role) <= 100))
);
@@ -17376,7 +17377,8 @@ CREATE TABLE vulnerability_feedback (
merge_request_id integer,
comment_author_id integer,
comment text,
- comment_timestamp timestamp with time zone
+ comment_timestamp timestamp with time zone,
+ finding_uuid uuid
);
CREATE SEQUENCE vulnerability_feedback_id_seq
@@ -22472,6 +22474,8 @@ CREATE INDEX index_user_custom_attributes_on_key_and_value ON user_custom_attrib
CREATE UNIQUE INDEX index_user_custom_attributes_on_user_id_and_key ON user_custom_attributes USING btree (user_id, key);
+CREATE INDEX index_user_details_on_provisioned_by_group_id ON user_details USING btree (provisioned_by_group_id);
+
CREATE UNIQUE INDEX index_user_details_on_user_id ON user_details USING btree (user_id);
CREATE INDEX index_user_highest_roles_on_user_id_and_highest_access_level ON user_highest_roles USING btree (user_id, highest_access_level);
@@ -23067,6 +23071,9 @@ ALTER TABLE ONLY project_features
ALTER TABLE ONLY ci_pipelines
ADD CONSTRAINT fk_190998ef09 FOREIGN KEY (external_pull_request_id) REFERENCES external_pull_requests(id) ON DELETE SET NULL;
+ALTER TABLE ONLY user_details
+ ADD CONSTRAINT fk_190e4fcc88 FOREIGN KEY (provisioned_by_group_id) REFERENCES namespaces(id) ON DELETE SET NULL;
+
ALTER TABLE ONLY vulnerabilities
ADD CONSTRAINT fk_1d37cddf91 FOREIGN KEY (epic_id) REFERENCES epics(id) ON DELETE SET NULL;
diff --git a/doc/administration/geo/disaster_recovery/index.md b/doc/administration/geo/disaster_recovery/index.md
index d1b9578ccce..8344b59a59f 100644
--- a/doc/administration/geo/disaster_recovery/index.md
+++ b/doc/administration/geo/disaster_recovery/index.md
@@ -133,6 +133,7 @@ Note the following when promoting a secondary:
```
1. Promote the **secondary** node to the **primary** node.
+
DANGER: **Warning:**
In GitLab 13.2 and 13.3, promoting a secondary node to a primary while the
secondary is paused fails. Do not pause replication before promoting a
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index b5f6f828f5a..88e787c0577 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -14144,6 +14144,7 @@ type Mutation {
namespaceIncreaseStorageTemporarily(input: NamespaceIncreaseStorageTemporarilyInput!): NamespaceIncreaseStorageTemporarilyPayload
oncallScheduleCreate(input: OncallScheduleCreateInput!): OncallScheduleCreatePayload
oncallScheduleDestroy(input: OncallScheduleDestroyInput!): OncallScheduleDestroyPayload
+ oncallScheduleUpdate(input: OncallScheduleUpdateInput!): OncallScheduleUpdatePayload
pipelineCancel(input: PipelineCancelInput!): PipelineCancelPayload
pipelineDestroy(input: PipelineDestroyInput!): PipelineDestroyPayload
pipelineRetry(input: PipelineRetryInput!): PipelineRetryPayload
@@ -14839,6 +14840,61 @@ type OncallScheduleDestroyPayload {
}
"""
+Autogenerated input type of OncallScheduleUpdate
+"""
+input OncallScheduleUpdateInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The description of the on-call schedule
+ """
+ description: String
+
+ """
+ The on-call schedule internal ID to update
+ """
+ iid: String!
+
+ """
+ The name of the on-call schedule
+ """
+ name: String
+
+ """
+ The project to update the on-call schedule in
+ """
+ projectPath: ID!
+
+ """
+ The timezone of the on-call schedule
+ """
+ timezone: String
+}
+
+"""
+Autogenerated return type of OncallScheduleUpdate
+"""
+type OncallScheduleUpdatePayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Errors encountered during execution of the mutation.
+ """
+ errors: [String!]!
+
+ """
+ The on-call schedule
+ """
+ oncallSchedule: IncidentManagementOncallSchedule
+}
+
+"""
Represents a package
"""
type Package {
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 35480fce34d..228c7d1b37c 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -41159,6 +41159,33 @@
"deprecationReason": null
},
{
+ "name": "oncallScheduleUpdate",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "OncallScheduleUpdateInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "OncallScheduleUpdatePayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "pipelineCancel",
"description": null,
"args": [
@@ -44105,6 +44132,152 @@
"possibleTypes": null
},
{
+ "kind": "INPUT_OBJECT",
+ "name": "OncallScheduleUpdateInput",
+ "description": "Autogenerated input type of OncallScheduleUpdate",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "projectPath",
+ "description": "The project to update the on-call schedule in",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "iid",
+ "description": "The on-call schedule internal ID to update",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "name",
+ "description": "The name of the on-call schedule",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "description",
+ "description": "The description of the on-call schedule",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "timezone",
+ "description": "The timezone of the on-call schedule",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "OncallScheduleUpdatePayload",
+ "description": "Autogenerated return type of OncallScheduleUpdate",
+ "fields": [
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "errors",
+ "description": "Errors encountered during execution of the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "oncallSchedule",
+ "description": "The on-call schedule",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "IncidentManagementOncallSchedule",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
"kind": "OBJECT",
"name": "Package",
"description": "Represents a package",
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index a1b9535cd6e..eb3661854d8 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -2272,6 +2272,16 @@ Autogenerated return type of OncallScheduleDestroy.
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `oncallSchedule` | IncidentManagementOncallSchedule | The on-call schedule |
+### OncallScheduleUpdatePayload
+
+Autogenerated return type of OncallScheduleUpdate.
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
+| `oncallSchedule` | IncidentManagementOncallSchedule | The on-call schedule |
+
### Package
Represents a package.
diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md
index 99b041ba5c7..73c83c614b5 100644
--- a/doc/development/documentation/styleguide/index.md
+++ b/doc/development/documentation/styleguide/index.md
@@ -587,7 +587,7 @@ tenses, words, and phrases:
<!-- vale gitlab.Simplicity = NO -->
- Avoid words like _easily_, _simply_, _handy_, and _useful._ If the user
doesn't find the process to be these things, we lose their trust.
-<!-- vale gitlab.Simplicity = NO -->
+<!-- vale gitlab.Simplicity = YES -->
### Word usage clarifications
diff --git a/doc/topics/autodevops/requirements.md b/doc/topics/autodevops/requirements.md
index c3ae2a53b60..2f457829e11 100644
--- a/doc/topics/autodevops/requirements.md
+++ b/doc/topics/autodevops/requirements.md
@@ -6,8 +6,9 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Requirements for Auto DevOps
-You can set up Auto DevOps for [Kubernetes](#auto-devops-requirements-for-kubernetes)
-or [Amazon Elastic Container Service (ECS)](#auto-devops-requirements-for-amazon-ecs).
+You can set up Auto DevOps for [Kubernetes](#auto-devops-requirements-for-kubernetes),
+[Amazon Elastic Container Service (ECS)](#auto-devops-requirements-for-amazon-ecs),
+or [Amazon Cloud Compute](#auto-devops-requirements-for-amazon-ecs).
For more information about Auto DevOps, see [the main Auto DevOps page](index.md)
or the [quick start guide](quick_start_guide.md).
@@ -140,3 +141,14 @@ it on its own. This template is designed to be used with Auto DevOps only. It ma
unexpectedly causing your pipeline to fail if included on its own. Also, the job
names within this template may also change. Do not override these jobs' names in your
own pipeline, as the override stops working when the name changes.
+
+## Auto DevOps requirements for Amazon EC2
+
+[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216008) in GitLab 13.6.
+
+You can target [AWS EC2](../../ci/cloud_deployment/index.md)
+as a deployment platform instead of Kubernetes. To use Auto DevOps with AWS EC2, you must add a
+specific environment variable.
+
+For more details, see [Custom build job for Auto DevOps](../../ci/cloud_deployment/index.md#custom-build-job-for-auto-devops)
+for deployments to AWS EC2.
diff --git a/doc/user/packages/dependency_proxy/index.md b/doc/user/packages/dependency_proxy/index.md
index 8205820b0e3..46da5f64658 100644
--- a/doc/user/packages/dependency_proxy/index.md
+++ b/doc/user/packages/dependency_proxy/index.md
@@ -8,6 +8,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7934) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.11.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/273655) to [GitLab Core](https://about.gitlab.com/pricing/) in GitLab 13.6.
+> - [Support for private groups](https://gitlab.com/gitlab-org/gitlab/-/issues/11582) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.7.
+> - Anonymous access to images in public groups is no longer available starting in [GitLab Premium](https://about.gitlab.com/pricing/) 13.7.
The GitLab Dependency Proxy is a local proxy you can use for your frequently-accessed
upstream images.
@@ -17,9 +19,7 @@ upstream image from a registry, acting as a pull-through cache.
## Prerequisites
-To use the Dependency Proxy:
-
-- Your group must be public. Authentication for private groups is [not supported yet](https://gitlab.com/gitlab-org/gitlab/-/issues/11582).
+The Dependency Proxy must be [enabled by an administrator](../../../administration/packages/dependency_proxy.md).
### Supported images and packages
@@ -58,6 +58,56 @@ Prerequisites:
- Docker Hub must be available. Follow [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/241639)
for progress on accessing images when Docker Hub is down.
+### Authenticate with the Dependency Proxy
+
+Because the Dependency Proxy is storing Docker images in a space associated with your group,
+you must authenticate against the Dependency Proxy.
+
+Follow the [instructions for using images from a private registry](../../../ci/docker/using_docker_images.md#define-an-image-from-a-private-container-registry),
+but instead of using `registry.example.com:5000`, use your GitLab domain with no port `gitlab.example.com`.
+
+For example, to manually log in:
+
+```shell
+docker login gitlab.example.com --username my_username --password my_password
+```
+
+You can authenticate using:
+
+- Your GitLab username and password.
+- A [personal access token](../../../user/profile/personal_access_tokens.md) with the scope set to `read_registry` and `write_registry`.
+
+#### Authenticate within CI/CD
+
+To work with the Dependency Proxy in [GitLab CI/CD](../../../ci/README.md), you can use
+`CI_REGISTRY_USER` and `CI_REGISTRY_PASSWORD`.
+
+```shell
+docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" gitlab.example.com
+```
+
+You can use other [predefined variables](../../../ci/variables/predefined_variables.md)
+to further generalize your CI script. For example:
+
+```yaml
+# .gitlab-ci.yml
+
+dependency-proxy-pull-master:
+ # Official docker image.
+ image: docker:latest
+ stage: build
+ services:
+ - docker:dind
+ before_script:
+ - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_SERVER_HOST":"$CI_SERVER_PORT"
+ script:
+ - docker pull "$CI_SERVER_HOST":"$CI_SERVER_PORT"/groupname/dependency_proxy/containers/alpine:latest
+```
+
+You can also use [custom environment variables](../../../ci/variables/README.md#custom-environment-variables) to store and access your personal access token or other valid credentials.
+
+### Store a Docker image in Dependency Proxy cache
+
To store a Docker image in Dependency Proxy storage:
1. Go to your group's **Packages & Registries > Dependency Proxy**.
diff --git a/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml b/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml
index 3f62d92ad13..23dfeda31cc 100644
--- a/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml
@@ -1,6 +1,6 @@
apply:
stage: deploy
- image: "registry.gitlab.com/gitlab-org/cluster-integration/cluster-applications:v0.34.1"
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/cluster-applications:v0.36.0"
environment:
name: production
variables:
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 7a2e5473490..fcf397e3162 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -19085,18 +19085,39 @@ msgstr ""
msgid "OnCallSchedules|Add a schedule"
msgstr ""
+msgid "OnCallSchedules|Add rotation"
+msgstr ""
+
msgid "OnCallSchedules|Add schedule"
msgstr ""
msgid "OnCallSchedules|Create on-call schedules in GitLab"
msgstr ""
+msgid "OnCallSchedules|Failed to add rotation"
+msgstr ""
+
msgid "OnCallSchedules|Failed to add schedule"
msgstr ""
+msgid "OnCallSchedules|Rotation length"
+msgstr ""
+
+msgid "OnCallSchedules|Rotation name cannot be empty"
+msgstr ""
+
+msgid "OnCallSchedules|Rotation participants cannot be empty"
+msgstr ""
+
+msgid "OnCallSchedules|Rotation start date cannot be empty"
+msgstr ""
+
msgid "OnCallSchedules|Route alerts directly to specific members of your team"
msgstr ""
+msgid "OnCallSchedules|Select participant"
+msgstr ""
+
msgid "OnCallSchedules|Select timezone"
msgstr ""
@@ -19414,6 +19435,9 @@ msgstr ""
msgid "Owner"
msgstr ""
+msgid "PST"
+msgstr ""
+
msgid "Package Registry"
msgstr ""
@@ -25622,9 +25646,6 @@ msgstr ""
msgid "Something went wrong, unable to search projects"
msgstr ""
-msgid "Something went wrong."
-msgstr ""
-
msgid "Something went wrong. Please try again."
msgstr ""
@@ -26042,6 +26063,9 @@ msgstr ""
msgid "Starts at (UTC)"
msgstr ""
+msgid "Starts on"
+msgstr ""
+
msgid "State your message to activate"
msgstr ""
@@ -31405,6 +31429,9 @@ msgstr ""
msgid "You have insufficient permissions to remove this HTTP integration"
msgstr ""
+msgid "You have insufficient permissions to update an on-call schedule for this project"
+msgstr ""
+
msgid "You have insufficient permissions to update this HTTP integration"
msgstr ""
@@ -31920,6 +31947,9 @@ msgstr ""
msgid "assign yourself"
msgstr ""
+msgid "at"
+msgstr ""
+
msgid "at risk"
msgstr ""
diff --git a/rubocop/rubocop-migrations.yml b/rubocop/rubocop-migrations.yml
index 5a5a78bf7f0..c175638ca2d 100644
--- a/rubocop/rubocop-migrations.yml
+++ b/rubocop/rubocop-migrations.yml
@@ -25,6 +25,7 @@ Migration/UpdateLargeTable:
- :project_authorizations
- :projects
- :project_ci_cd_settings
+ - :project_settings
- :project_features
- :push_event_payloads
- :resource_label_events
diff --git a/scripts/update-workhorse b/scripts/update-workhorse
index 0955f6a671a..3ada37e70a1 100755
--- a/scripts/update-workhorse
+++ b/scripts/update-workhorse
@@ -2,6 +2,7 @@
set -e
WORKHORSE_DIR=workhorse/
WORKHORSE_REF="v$(cat GITLAB_WORKHORSE_VERSION)"
+WORKHORSE_URL=${GITLAB_WORKHORSE_URL:-https://gitlab.com/gitlab-org/gitlab-workhorse.git}
if [ $# -gt 1 ] || ([ $# = 1 ] && [ x$1 != xcheck ]); then
echo "Usage: update-workhorse [check]"
@@ -15,7 +16,7 @@ if [ -n "$clean" ] ; then
exit 1
fi
-git fetch https://gitlab.com/gitlab-org/gitlab-workhorse.git "$WORKHORSE_REF"
+git fetch "$WORKHORSE_URL" "$WORKHORSE_REF"
git rm -rf --quiet -- "$WORKHORSE_DIR"
git read-tree --prefix="$WORKHORSE_DIR" -u FETCH_HEAD
diff --git a/spec/controllers/groups/dependency_proxy_auth_controller_spec.rb b/spec/controllers/groups/dependency_proxy_auth_controller_spec.rb
new file mode 100644
index 00000000000..857e0570621
--- /dev/null
+++ b/spec/controllers/groups/dependency_proxy_auth_controller_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::DependencyProxyAuthController do
+ include DependencyProxyHelpers
+
+ describe 'GET #authenticate' do
+ subject { get :authenticate }
+
+ context 'feature flag disabled' do
+ before do
+ stub_feature_flags(dependency_proxy_for_private_groups: false)
+ end
+
+ it 'returns successfully', :aggregate_failures do
+ subject
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+
+ context 'without JWT' do
+ it 'returns unauthorized with oauth realm', :aggregate_failures do
+ subject
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ expect(response.headers['WWW-Authenticate']).to eq DependencyProxy::Registry.authenticate_header
+ end
+ end
+
+ context 'with valid JWT' do
+ let_it_be(:user) { create(:user) }
+ let(:jwt) { build_jwt(user) }
+ let(:token_header) { "Bearer #{jwt.encoded}" }
+
+ before do
+ request.headers['HTTP_AUTHORIZATION'] = token_header
+ end
+
+ it { is_expected.to have_gitlab_http_status(:success) }
+ end
+
+ context 'with invalid JWT' do
+ context 'bad user' do
+ let(:jwt) { build_jwt(double('bad_user', id: 999)) }
+ let(:token_header) { "Bearer #{jwt.encoded}" }
+
+ before do
+ request.headers['HTTP_AUTHORIZATION'] = token_header
+ end
+
+ it { is_expected.to have_gitlab_http_status(:not_found) }
+ end
+
+ context 'token with no user id' do
+ let(:token_header) { "Bearer #{build_jwt.encoded}" }
+
+ before do
+ request.headers['HTTP_AUTHORIZATION'] = token_header
+ end
+
+ it { is_expected.to have_gitlab_http_status(:not_found) }
+ end
+
+ context 'expired token' do
+ let_it_be(:user) { create(:user) }
+ let(:jwt) { build_jwt(user, expire_time: Time.zone.now - 1.hour) }
+ let(:token_header) { "Bearer #{jwt.encoded}" }
+
+ before do
+ request.headers['HTTP_AUTHORIZATION'] = token_header
+ end
+
+ it { is_expected.to have_gitlab_http_status(:unauthorized) }
+ end
+ end
+ end
+end
diff --git a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
index 615b56ff22f..87956cc7287 100644
--- a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
+++ b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
@@ -3,8 +3,77 @@
require 'spec_helper'
RSpec.describe Groups::DependencyProxyForContainersController do
+ include HttpBasicAuthHelpers
+ include DependencyProxyHelpers
+
+ let_it_be(:user) { create(:user) }
let(:group) { create(:group) }
let(:token_response) { { status: :success, token: 'abcd1234' } }
+ let(:jwt) { build_jwt(user) }
+ let(:token_header) { "Bearer #{jwt.encoded}" }
+
+ shared_examples 'without a token' do
+ before do
+ request.headers['HTTP_AUTHORIZATION'] = nil
+ end
+
+ context 'feature flag disabled' do
+ before do
+ stub_feature_flags(dependency_proxy_for_private_groups: false)
+ end
+
+ it { is_expected.to have_gitlab_http_status(:ok) }
+ end
+
+ it { is_expected.to have_gitlab_http_status(:unauthorized) }
+ end
+
+ shared_examples 'feature flag disabled with private group' do
+ before do
+ stub_feature_flags(dependency_proxy_for_private_groups: false)
+ end
+
+ it 'redirects', :aggregate_failures do
+ group.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ expect(response.location).to end_with(new_user_session_path)
+ end
+ end
+
+ shared_examples 'without permission' do
+ context 'with invalid user' do
+ before do
+ user = double('bad_user', id: 999)
+ token_header = "Bearer #{build_jwt(user).encoded}"
+ request.headers['HTTP_AUTHORIZATION'] = token_header
+ end
+
+ it { is_expected.to have_gitlab_http_status(:not_found) }
+ end
+
+ context 'with valid user that does not have access' do
+ let(:group) { create(:group, :private) }
+
+ before do
+ user = double('bad_user', id: 999)
+ token_header = "Bearer #{build_jwt(user).encoded}"
+ request.headers['HTTP_AUTHORIZATION'] = token_header
+ end
+
+ it { is_expected.to have_gitlab_http_status(:not_found) }
+ end
+
+ context 'when user is not found' do
+ before do
+ allow(User).to receive(:find).and_return(nil)
+ end
+
+ it { is_expected.to have_gitlab_http_status(:unauthorized) }
+ end
+ end
shared_examples 'not found when disabled' do
context 'feature disabled' do
@@ -27,6 +96,8 @@ RSpec.describe Groups::DependencyProxyForContainersController do
allow_next_instance_of(DependencyProxy::RequestTokenService) do |instance|
allow(instance).to receive(:execute).and_return(token_response)
end
+
+ request.headers['HTTP_AUTHORIZATION'] = token_header
end
describe 'GET #manifest' do
@@ -46,6 +117,10 @@ RSpec.describe Groups::DependencyProxyForContainersController do
enable_dependency_proxy
end
+ it_behaves_like 'without a token'
+ it_behaves_like 'without permission'
+ it_behaves_like 'feature flag disabled with private group'
+
context 'remote token request fails' do
let(:token_response) do
{
@@ -113,6 +188,10 @@ RSpec.describe Groups::DependencyProxyForContainersController do
enable_dependency_proxy
end
+ it_behaves_like 'without a token'
+ it_behaves_like 'without permission'
+ it_behaves_like 'feature flag disabled with private group'
+
context 'remote blob request fails' do
let(:blob_response) do
{
diff --git a/spec/controllers/profiles/gpg_keys_controller_spec.rb b/spec/controllers/profiles/gpg_keys_controller_spec.rb
new file mode 100644
index 00000000000..1860bb0c93b
--- /dev/null
+++ b/spec/controllers/profiles/gpg_keys_controller_spec.rb
@@ -0,0 +1,136 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Profiles::GpgKeysController do
+ let(:user) { create(:user, email: GpgHelpers::User1.emails[0]) }
+
+ describe 'POST #create' do
+ before do
+ sign_in(user)
+ end
+
+ it 'creates a new key' do
+ expect do
+ post :create, params: { gpg_key: build(:gpg_key).attributes }
+ end.to change { GpgKey.count }.by(1)
+ end
+ end
+
+ describe "#get_keys" do
+ describe "non existent user" do
+ it "does not generally work" do
+ get :get_keys, params: { username: 'not-existent' }
+
+ expect(response).not_to be_successful
+ end
+ end
+
+ describe "user with no keys" do
+ it "does generally work" do
+ get :get_keys, params: { username: user.username }
+
+ expect(response).to be_successful
+ end
+
+ it "renders all keys separated with a new line" do
+ get :get_keys, params: { username: user.username }
+
+ expect(response.body).to eq("")
+ end
+
+ it "responds with text/plain content type" do
+ get :get_keys, params: { username: user.username }
+
+ expect(response.content_type).to eq("text/plain")
+ end
+ end
+
+ describe "user with keys" do
+ let!(:gpg_key) { create(:gpg_key, user: user) }
+ let!(:another_gpg_key) { create(:another_gpg_key, user: user) }
+
+ describe "while signed in" do
+ before do
+ sign_in(user)
+ end
+
+ it "does generally work" do
+ get :get_keys, params: { username: user.username }
+
+ expect(response).to be_successful
+ end
+
+ it "renders all verified keys separated with a new line" do
+ get :get_keys, params: { username: user.username }
+
+ expect(response.body).not_to eq('')
+ expect(response.body).to eq(user.gpg_keys.select(&:verified?).map(&:key).join("\n"))
+
+ expect(response.body).to include(gpg_key.key)
+ expect(response.body).to include(another_gpg_key.key)
+ end
+
+ it "responds with text/plain content type" do
+ get :get_keys, params: { username: user.username }
+
+ expect(response.content_type).to eq("text/plain")
+ end
+ end
+
+ describe 'when logged out' do
+ before do
+ sign_out(user)
+ end
+
+ it "still does generally work" do
+ get :get_keys, params: { username: user.username }
+
+ expect(response).to be_successful
+ end
+
+ it "renders all verified keys separated with a new line" do
+ get :get_keys, params: { username: user.username }
+
+ expect(response.body).not_to eq('')
+ expect(response.body).to eq(user.gpg_keys.map(&:key).join("\n"))
+
+ expect(response.body).to include(gpg_key.key)
+ expect(response.body).to include(another_gpg_key.key)
+ end
+
+ it "responds with text/plain content type" do
+ get :get_keys, params: { username: user.username }
+
+ expect(response.content_type).to eq("text/plain")
+ end
+ end
+
+ describe 'when revoked' do
+ before do
+ sign_in(user)
+ another_gpg_key.revoke
+ end
+
+ it "doesn't render revoked keys" do
+ get :get_keys, params: { username: user.username }
+
+ expect(response.body).not_to eq('')
+
+ expect(response.body).to include(gpg_key.key)
+ expect(response.body).not_to include(another_gpg_key.key)
+ end
+
+ it "doesn't render revoked keys for non-authorized users" do
+ sign_out(user)
+ get :get_keys, params: { username: user.username }
+
+ expect(response.body).not_to eq('')
+
+ expect(response.body).to include(gpg_key.key)
+ expect(response.body).not_to include(another_gpg_key.key)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb
index 307c5b4725a..1cebb6a4804 100644
--- a/spec/controllers/repositories/git_http_controller_spec.rb
+++ b/spec/controllers/repositories/git_http_controller_spec.rb
@@ -167,6 +167,14 @@ RSpec.describe Repositories::GitHttpController do
Projects::DailyStatisticsFinder.new(container).total_fetch_count
}.from(0).to(1)
end
+
+ it 'records a namespace onboarding progress action' do
+ expect_next_instance_of(OnboardingProgressService) do |service|
+ expect(service).to receive(:execute).with(action: :git_read)
+ end
+
+ send_request
+ end
end
end
end
diff --git a/spec/factories/gpg_keys.rb b/spec/factories/gpg_keys.rb
index 9f321643174..5967d9ba9d3 100644
--- a/spec/factories/gpg_keys.rb
+++ b/spec/factories/gpg_keys.rb
@@ -10,5 +10,10 @@ FactoryBot.define do
factory :gpg_key_with_subkeys do
key { GpgHelpers::User1.public_key_with_extra_signing_key }
end
+
+ factory :another_gpg_key do
+ key { GpgHelpers::User1.public_key2 }
+ user
+ end
end
end
diff --git a/spec/features/groups/dependency_proxy_spec.rb b/spec/features/groups/dependency_proxy_spec.rb
index 9bbfdc488fb..51371ddc532 100644
--- a/spec/features/groups/dependency_proxy_spec.rb
+++ b/spec/features/groups/dependency_proxy_spec.rb
@@ -79,13 +79,19 @@ RSpec.describe 'Group Dependency Proxy' do
sign_in(developer)
end
- context 'group is private' do
- let(:group) { create(:group, :private) }
+ context 'feature flag is disabled' do
+ before do
+ stub_feature_flags(dependency_proxy_for_private_groups: false)
+ end
- it 'informs user that feature is only available for public groups' do
- visit path
+ context 'group is private' do
+ let(:group) { create(:group, :private) }
- expect(page).to have_content('Dependency proxy feature is limited to public groups for now.')
+ it 'informs user that feature is only available for public groups' do
+ visit path
+
+ expect(page).to have_content('Dependency proxy feature is limited to public groups for now.')
+ end
end
end
diff --git a/spec/features/issuables/close_reopen_report_toggle_spec.rb b/spec/features/merge_request/close_reopen_report_toggle_spec.rb
index 30cabce8fa2..8a4277d87c9 100644
--- a/spec/features/issuables/close_reopen_report_toggle_spec.rb
+++ b/spec/features/merge_request/close_reopen_report_toggle_spec.rb
@@ -7,44 +7,6 @@ RSpec.describe 'Issuables Close/Reopen/Report toggle' do
let(:user) { create(:user) }
- shared_examples 'an issuable close/reopen/report toggle' do
- let(:container) { find('.issuable-close-dropdown') }
- let(:human_model_name) { issuable.model_name.human.downcase }
-
- it 'shows toggle' do
- expect(page).to have_button("Close #{human_model_name}")
- expect(page).to have_selector('.issuable-close-dropdown')
- end
-
- it 'opens a dropdown when toggle is clicked' do
- container.find('.dropdown-toggle').click
-
- expect(container).to have_selector('.dropdown-menu')
- expect(container).to have_content("Close #{human_model_name}")
- expect(container).to have_content('Report abuse')
- expect(container).to have_content("Report #{human_model_name.pluralize} that are abusive, inappropriate or spam.")
-
- if issuable.is_a?(MergeRequest)
- page.within('.js-issuable-close-dropdown') do
- expect(page).to have_link('Close merge request')
- end
- else
- expect(container).to have_selector('.close-item.droplab-item-selected')
- end
-
- expect(container).to have_selector('.report-item')
- expect(container).not_to have_selector('.report-item.droplab-item-selected')
- expect(container).not_to have_selector('.reopen-item')
- end
-
- it 'links to Report Abuse' do
- container.find('.dropdown-toggle').click
- container.find('.report-abuse-link').click
-
- expect(page).to have_content('Report abuse to admin')
- end
- end
-
context 'on a merge request' do
let(:container) { find('.detail-page-header-actions') }
let(:project) { create(:project, :repository) }
@@ -60,7 +22,22 @@ RSpec.describe 'Issuables Close/Reopen/Report toggle' do
visit project_merge_request_path(project, issuable)
end
- it_behaves_like 'an issuable close/reopen/report toggle'
+ context 'close/reopen/report toggle' do
+ it 'opens a dropdown when toggle is clicked' do
+ click_button 'Toggle dropdown'
+
+ expect(container).to have_link("Close merge request")
+ expect(container).to have_link('Report abuse')
+ expect(container).to have_text("Report merge requests that are abusive, inappropriate or spam.")
+ end
+
+ it 'links to Report Abuse' do
+ click_button 'Toggle dropdown'
+ click_link 'Report abuse'
+
+ expect(page).to have_content('Report abuse to admin')
+ end
+ end
context 'when the merge request is open' do
let(:issuable) { create(:merge_request, :opened, source_project: project) }
diff --git a/spec/features/issuables/merge_request_discussion_lock_spec.rb b/spec/features/merge_request/merge_request_discussion_lock_spec.rb
index 4e0265839f6..4e0265839f6 100644
--- a/spec/features/issuables/merge_request_discussion_lock_spec.rb
+++ b/spec/features/merge_request/merge_request_discussion_lock_spec.rb
diff --git a/spec/features/merge_request/user_reopens_merge_request_spec.rb b/spec/features/merge_request/user_reopens_merge_request_spec.rb
index 4a05a3be59a..7cb8ca280cc 100644
--- a/spec/features/merge_request/user_reopens_merge_request_spec.rb
+++ b/spec/features/merge_request/user_reopens_merge_request_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'User reopens a merge requests', :js do
end
it 'reopens a merge request' do
- find('.js-issuable-close-dropdown .dropdown-toggle').click
+ find('.detail-page-header .dropdown-toggle').click
click_link('Reopen merge request', match: :first)
diff --git a/spec/features/merge_requests/user_squashes_merge_request_spec.rb b/spec/features/merge_request/user_squashes_merge_request_spec.rb
index 84964bd0637..84964bd0637 100644
--- a/spec/features/merge_requests/user_squashes_merge_request_spec.rb
+++ b/spec/features/merge_request/user_squashes_merge_request_spec.rb
diff --git a/spec/features/merge_requests/user_views_diffs_commit_spec.rb b/spec/features/merge_request/user_views_diffs_commit_spec.rb
index cf92603972e..cf92603972e 100644
--- a/spec/features/merge_requests/user_views_diffs_commit_spec.rb
+++ b/spec/features/merge_request/user_views_diffs_commit_spec.rb
diff --git a/spec/features/merge_request/user_sees_empty_state_spec.rb b/spec/features/merge_requests/user_sees_empty_state_spec.rb
index ac07b31731d..ac07b31731d 100644
--- a/spec/features/merge_request/user_sees_empty_state_spec.rb
+++ b/spec/features/merge_requests/user_sees_empty_state_spec.rb
diff --git a/spec/frontend/fixtures/static/whats_new_notification.html b/spec/frontend/fixtures/static/whats_new_notification.html
new file mode 100644
index 00000000000..30d5eea91cc
--- /dev/null
+++ b/spec/frontend/fixtures/static/whats_new_notification.html
@@ -0,0 +1,6 @@
+<div class='whats-new-notification-fixture-root'>
+ <div class='app' data-storage-key='storage-key'></div>
+ <div class='header-help'>
+ <div class='js-whats-new-notification-count'></div>
+ </div>
+</div>
diff --git a/spec/frontend/helpers/vuex_action_helper.js b/spec/frontend/helpers/vuex_action_helper.js
index 6c3569a2247..64dd3888d47 100644
--- a/spec/frontend/helpers/vuex_action_helper.js
+++ b/spec/frontend/helpers/vuex_action_helper.js
@@ -4,7 +4,7 @@ const noop = () => {};
* Helper for testing action with expected mutations inspired in
* https://vuex.vuejs.org/en/testing.html
*
- * @param {Function} action to be tested
+ * @param {(Function|Object)} action to be tested, or object of named parameters
* @param {Object} payload will be provided to the action
* @param {Object} state will be provided to the action
* @param {Array} [expectedMutations=[]] mutations expected to be committed
@@ -39,15 +39,42 @@ const noop = () => {};
* [], // expected actions
* ).then(done)
* .catch(done.fail);
+ *
+ * @example
+ * await testAction({
+ * action: actions.actionName,
+ * payload: { deleteListId: 1 },
+ * state: { lists: [1, 2, 3] },
+ * expectedMutations: [ { type: types.MUTATION} ],
+ * expectedActions: [],
+ * })
*/
export default (
- action,
- payload,
- state,
- expectedMutations = [],
- expectedActions = [],
- done = noop,
+ actionArg,
+ payloadArg,
+ stateArg,
+ expectedMutationsArg = [],
+ expectedActionsArg = [],
+ doneArg = noop,
) => {
+ let action = actionArg;
+ let payload = payloadArg;
+ let state = stateArg;
+ let expectedMutations = expectedMutationsArg;
+ let expectedActions = expectedActionsArg;
+ let done = doneArg;
+
+ if (typeof actionArg !== 'function') {
+ ({
+ action,
+ payload,
+ state,
+ expectedMutations = [],
+ expectedActions = [],
+ done = noop,
+ } = actionArg);
+ }
+
const mutations = [];
const actions = [];
diff --git a/spec/frontend/helpers/vuex_action_helper_spec.js b/spec/frontend/helpers/vuex_action_helper_spec.js
index 61d05762a04..4d7bf21820a 100644
--- a/spec/frontend/helpers/vuex_action_helper_spec.js
+++ b/spec/frontend/helpers/vuex_action_helper_spec.js
@@ -1,166 +1,174 @@
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
-import testAction from './vuex_action_helper';
-
-describe('VueX test helper (testAction)', () => {
- let originalExpect;
- let assertion;
- let mock;
- const noop = () => {};
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- /**
- * In order to test the helper properly, we need to overwrite the Jest
- * `expect` helper. We test that the testAction helper properly passes the
- * dispatched actions/committed mutations to the Jest helper.
- */
- originalExpect = expect;
- assertion = null;
- global.expect = actual => ({
- toEqual: () => {
- originalExpect(actual).toEqual(assertion);
- },
- });
- });
+import testActionFn from './vuex_action_helper';
- afterEach(() => {
- mock.restore();
- global.expect = originalExpect;
- });
+const testActionFnWithOptionsArg = (...args) => {
+ const [action, payload, state, expectedMutations, expectedActions, done] = args;
+ return testActionFn({ action, payload, state, expectedMutations, expectedActions, done });
+};
- it('properly passes state and payload to action', () => {
- const exampleState = { FOO: 12, BAR: 3 };
- const examplePayload = { BAZ: 73, BIZ: 55 };
+describe.each([testActionFn, testActionFnWithOptionsArg])(
+ 'VueX test helper (testAction)',
+ testAction => {
+ let originalExpect;
+ let assertion;
+ let mock;
+ const noop = () => {};
- const action = ({ state }, payload) => {
- originalExpect(state).toEqual(exampleState);
- originalExpect(payload).toEqual(examplePayload);
- };
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ /**
+ * In order to test the helper properly, we need to overwrite the Jest
+ * `expect` helper. We test that the testAction helper properly passes the
+ * dispatched actions/committed mutations to the Jest helper.
+ */
+ originalExpect = expect;
+ assertion = null;
+ global.expect = actual => ({
+ toEqual: () => {
+ originalExpect(actual).toEqual(assertion);
+ },
+ });
+ });
- assertion = { mutations: [], actions: [] };
+ afterEach(() => {
+ mock.restore();
+ global.expect = originalExpect;
+ });
- testAction(action, examplePayload, exampleState);
- });
+ it('properly passes state and payload to action', () => {
+ const exampleState = { FOO: 12, BAR: 3 };
+ const examplePayload = { BAZ: 73, BIZ: 55 };
- describe('given a sync action', () => {
- it('mocks committing mutations', () => {
- const action = ({ commit }) => {
- commit('MUTATION');
+ const action = ({ state }, payload) => {
+ originalExpect(state).toEqual(exampleState);
+ originalExpect(payload).toEqual(examplePayload);
};
- assertion = { mutations: [{ type: 'MUTATION' }], actions: [] };
+ assertion = { mutations: [], actions: [] };
- testAction(action, null, {}, assertion.mutations, assertion.actions, noop);
+ testAction(action, examplePayload, exampleState);
});
- it('mocks dispatching actions', () => {
- const action = ({ dispatch }) => {
- dispatch('ACTION');
- };
+ describe('given a sync action', () => {
+ it('mocks committing mutations', () => {
+ const action = ({ commit }) => {
+ commit('MUTATION');
+ };
- assertion = { actions: [{ type: 'ACTION' }], mutations: [] };
+ assertion = { mutations: [{ type: 'MUTATION' }], actions: [] };
- testAction(action, null, {}, assertion.mutations, assertion.actions, noop);
- });
+ testAction(action, null, {}, assertion.mutations, assertion.actions, noop);
+ });
- it('works with done callback once finished', done => {
- assertion = { mutations: [], actions: [] };
+ it('mocks dispatching actions', () => {
+ const action = ({ dispatch }) => {
+ dispatch('ACTION');
+ };
- testAction(noop, null, {}, assertion.mutations, assertion.actions, done);
- });
+ assertion = { actions: [{ type: 'ACTION' }], mutations: [] };
- it('returns a promise', done => {
- assertion = { mutations: [], actions: [] };
+ testAction(action, null, {}, assertion.mutations, assertion.actions, noop);
+ });
- testAction(noop, null, {}, assertion.mutations, assertion.actions)
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('given an async action (returning a promise)', () => {
- let lastError;
- const data = { FOO: 'BAR' };
-
- const asyncAction = ({ commit, dispatch }) => {
- dispatch('ACTION');
-
- return axios
- .get(TEST_HOST)
- .catch(error => {
- commit('ERROR');
- lastError = error;
- throw error;
- })
- .then(() => {
- commit('SUCCESS');
- return data;
- });
- };
+ it('works with done callback once finished', done => {
+ assertion = { mutations: [], actions: [] };
- beforeEach(() => {
- lastError = null;
+ testAction(noop, null, {}, assertion.mutations, assertion.actions, done);
+ });
+
+ it('returns a promise', done => {
+ assertion = { mutations: [], actions: [] };
+
+ testAction(noop, null, {}, assertion.mutations, assertion.actions)
+ .then(done)
+ .catch(done.fail);
+ });
});
- it('works with done callback once finished', done => {
- mock.onGet(TEST_HOST).replyOnce(200, 42);
+ describe('given an async action (returning a promise)', () => {
+ let lastError;
+ const data = { FOO: 'BAR' };
- assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] };
+ const asyncAction = ({ commit, dispatch }) => {
+ dispatch('ACTION');
- testAction(asyncAction, null, {}, assertion.mutations, assertion.actions, done);
- });
+ return axios
+ .get(TEST_HOST)
+ .catch(error => {
+ commit('ERROR');
+ lastError = error;
+ throw error;
+ })
+ .then(() => {
+ commit('SUCCESS');
+ return data;
+ });
+ };
- it('returns original data of successful promise while checking actions/mutations', done => {
- mock.onGet(TEST_HOST).replyOnce(200, 42);
+ beforeEach(() => {
+ lastError = null;
+ });
- assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] };
+ it('works with done callback once finished', done => {
+ mock.onGet(TEST_HOST).replyOnce(200, 42);
- testAction(asyncAction, null, {}, assertion.mutations, assertion.actions)
- .then(res => {
- originalExpect(res).toEqual(data);
- done();
- })
- .catch(done.fail);
- });
+ assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] };
+
+ testAction(asyncAction, null, {}, assertion.mutations, assertion.actions, done);
+ });
+
+ it('returns original data of successful promise while checking actions/mutations', done => {
+ mock.onGet(TEST_HOST).replyOnce(200, 42);
- it('returns original error of rejected promise while checking actions/mutations', done => {
- mock.onGet(TEST_HOST).replyOnce(500, '');
+ assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] };
- assertion = { mutations: [{ type: 'ERROR' }], actions: [{ type: 'ACTION' }] };
+ testAction(asyncAction, null, {}, assertion.mutations, assertion.actions)
+ .then(res => {
+ originalExpect(res).toEqual(data);
+ done();
+ })
+ .catch(done.fail);
+ });
- testAction(asyncAction, null, {}, assertion.mutations, assertion.actions)
- .then(done.fail)
- .catch(error => {
- originalExpect(error).toBe(lastError);
- done();
- });
+ it('returns original error of rejected promise while checking actions/mutations', done => {
+ mock.onGet(TEST_HOST).replyOnce(500, '');
+
+ assertion = { mutations: [{ type: 'ERROR' }], actions: [{ type: 'ACTION' }] };
+
+ testAction(asyncAction, null, {}, assertion.mutations, assertion.actions)
+ .then(done.fail)
+ .catch(error => {
+ originalExpect(error).toBe(lastError);
+ done();
+ });
+ });
});
- });
- it('works with async actions not returning promises', done => {
- const data = { FOO: 'BAR' };
+ it('works with async actions not returning promises', done => {
+ const data = { FOO: 'BAR' };
- const asyncAction = ({ commit, dispatch }) => {
- dispatch('ACTION');
+ const asyncAction = ({ commit, dispatch }) => {
+ dispatch('ACTION');
- axios
- .get(TEST_HOST)
- .then(() => {
- commit('SUCCESS');
- return data;
- })
- .catch(error => {
- commit('ERROR');
- throw error;
- });
- };
+ axios
+ .get(TEST_HOST)
+ .then(() => {
+ commit('SUCCESS');
+ return data;
+ })
+ .catch(error => {
+ commit('ERROR');
+ throw error;
+ });
+ };
- mock.onGet(TEST_HOST).replyOnce(200, 42);
+ mock.onGet(TEST_HOST).replyOnce(200, 42);
- assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] };
+ assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] };
- testAction(asyncAction, null, {}, assertion.mutations, assertion.actions, done);
- });
-});
+ testAction(asyncAction, null, {}, assertion.mutations, assertion.actions, done);
+ });
+ },
+);
diff --git a/spec/frontend/notes/components/multiline_comment_utils_spec.js b/spec/frontend/notes/components/multiline_comment_utils_spec.js
index af4394cc648..99b33e7cd5f 100644
--- a/spec/frontend/notes/components/multiline_comment_utils_spec.js
+++ b/spec/frontend/notes/components/multiline_comment_utils_spec.js
@@ -34,8 +34,17 @@ describe('Multiline comment utilities', () => {
expect(getSymbol(type)).toEqual(result);
});
});
- describe('getCommentedLines', () => {
- const diffLines = [{ line_code: '1' }, { line_code: '2' }, { line_code: '3' }];
+ const inlineDiffLines = [{ line_code: '1' }, { line_code: '2' }, { line_code: '3' }];
+ const parallelDiffLines = inlineDiffLines.map(line => ({
+ left: { ...line },
+ right: { ...line },
+ }));
+
+ describe.each`
+ view | diffLines
+ ${'inline'} | ${inlineDiffLines}
+ ${'parallel'} | ${parallelDiffLines}
+ `('getCommentedLines $view view', ({ diffLines }) => {
it('returns a default object when `selectedCommentPosition` is not provided', () => {
expect(getCommentedLines(undefined, diffLines)).toEqual({ startLine: 4, endLine: 4 });
});
diff --git a/spec/frontend/whats_new/utils/notification_spec.js b/spec/frontend/whats_new/utils/notification_spec.js
new file mode 100644
index 00000000000..e3e390f4394
--- /dev/null
+++ b/spec/frontend/whats_new/utils/notification_spec.js
@@ -0,0 +1,55 @@
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+import { setNotification, getStorageKey } from '~/whats_new/utils/notification';
+
+describe('~/whats_new/utils/notification', () => {
+ useLocalStorageSpy();
+
+ let wrapper;
+
+ const findNotificationEl = () => wrapper.querySelector('.header-help');
+ const findNotificationCountEl = () => wrapper.querySelector('.js-whats-new-notification-count');
+ const getAppEl = () => wrapper.querySelector('.app');
+
+ beforeEach(() => {
+ loadFixtures('static/whats_new_notification.html');
+ wrapper = document.querySelector('.whats-new-notification-fixture-root');
+ });
+
+ afterEach(() => {
+ wrapper.remove();
+ });
+
+ describe('setNotification', () => {
+ const subject = () => setNotification(getAppEl());
+
+ it("when storage key doesn't exist it adds notifications class", () => {
+ const notificationEl = findNotificationEl();
+
+ expect(notificationEl.classList).not.toContain('with-notifications');
+
+ subject();
+
+ expect(findNotificationCountEl()).toExist();
+ expect(notificationEl.classList).toContain('with-notifications');
+ });
+
+ it('removes class and count element when storage key is true', () => {
+ const notificationEl = findNotificationEl();
+ notificationEl.classList.add('with-notifications');
+ localStorage.setItem('storage-key', 'false');
+
+ expect(findNotificationCountEl()).toExist();
+
+ subject();
+
+ expect(findNotificationCountEl()).not.toExist();
+ expect(notificationEl.classList).not.toContain('with-notifications');
+ });
+ });
+
+ describe('getStorageKey', () => {
+ it('retrieves the storage key data attribute from the el', () => {
+ expect(getStorageKey(getAppEl())).toBe('storage-key');
+ });
+ });
+});
diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
index 153dc19335b..377e2c43a72 100644
--- a/spec/helpers/merge_requests_helper_spec.rb
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -6,26 +6,6 @@ RSpec.describe MergeRequestsHelper do
include ActionView::Helpers::UrlHelper
include ProjectForksHelper
- describe 'ci_build_details_path' do
- let(:project) { create(:project) }
- let(:merge_request) { MergeRequest.new }
- let(:ci_service) { CiService.new }
- let(:last_commit) { Ci::Pipeline.new({}) }
-
- before do
- allow(merge_request).to receive(:source_project).and_return(project)
- allow(merge_request).to receive(:last_commit).and_return(last_commit)
- allow(project).to receive(:ci_service).and_return(ci_service)
- allow(last_commit).to receive(:sha).and_return('12d65c')
- end
-
- it 'does not include api credentials in a link' do
- allow(ci_service)
- .to receive(:build_page).and_return("http://secretuser:secretpass@jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c")
- expect(helper.ci_build_details_path(merge_request)).not_to match("secret")
- end
- end
-
describe '#state_name_with_icon' do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/migrations/update_internal_ids_last_value_for_epics_spec.rb b/spec/migrations/update_internal_ids_last_value_for_epics_spec.rb
new file mode 100644
index 00000000000..b53db5db0f8
--- /dev/null
+++ b/spec/migrations/update_internal_ids_last_value_for_epics_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20201202081429_update_internal_ids_last_value_for_epics.rb')
+
+RSpec.describe UpdateInternalIdsLastValueForEpics, :migration, schema: 20201124185639 do
+ let(:namespaces) { table(:namespaces) }
+ let(:users) { table(:users) }
+ let(:epics) { table(:epics) }
+ let(:internal_ids) { table(:internal_ids) }
+
+ let!(:author) { users.create!(name: 'test', email: 'test@example.com', projects_limit: 0) }
+ let!(:group1) { namespaces.create!(type: 'Group', name: 'group1', path: 'group1') }
+ let!(:group2) { namespaces.create!(type: 'Group', name: 'group2', path: 'group2') }
+ let!(:group3) { namespaces.create!(type: 'Group', name: 'group3', path: 'group3') }
+ let!(:epic_last_value1) { internal_ids.create!(usage: 4, last_value: 5, namespace_id: group1.id) }
+ let!(:epic_last_value2) { internal_ids.create!(usage: 4, last_value: 5, namespace_id: group2.id) }
+ let!(:epic_last_value3) { internal_ids.create!(usage: 4, last_value: 5, namespace_id: group3.id) }
+ let!(:epic_1) { epics.create!(iid: 110, title: 'from epic 1', group_id: group1.id, author_id: author.id, title_html: 'any') }
+ let!(:epic_2) { epics.create!(iid: 5, title: 'from epic 1', group_id: group2.id, author_id: author.id, title_html: 'any') }
+ let!(:epic_3) { epics.create!(iid: 3, title: 'from epic 1', group_id: group3.id, author_id: author.id, title_html: 'any') }
+
+ it 'updates out of sync internal_ids last_value' do
+ migrate!
+
+ expect(internal_ids.find_by(usage: 4, namespace_id: group1.id).last_value).to eq(110)
+ expect(internal_ids.find_by(usage: 4, namespace_id: group2.id).last_value).to eq(5)
+ expect(internal_ids.find_by(usage: 4, namespace_id: group3.id).last_value).to eq(5)
+ end
+end
diff --git a/spec/models/dependency_proxy/registry_spec.rb b/spec/models/dependency_proxy/registry_spec.rb
index 5bfa75a2eed..a888ee2b7f7 100644
--- a/spec/models/dependency_proxy/registry_spec.rb
+++ b/spec/models/dependency_proxy/registry_spec.rb
@@ -54,4 +54,11 @@ RSpec.describe DependencyProxy::Registry, type: :model do
end
end
end
+
+ describe '#authenticate_header' do
+ it 'returns the OAuth realm and service header' do
+ expect(described_class.authenticate_header)
+ .to eq("Bearer realm=\"#{Gitlab.config.gitlab.url}/jwt/auth\",service=\"dependency_proxy\"")
+ end
+ end
end
diff --git a/spec/models/namespace_onboarding_action_spec.rb b/spec/models/namespace_onboarding_action_spec.rb
index 40ff965c134..70dcb989b32 100644
--- a/spec/models/namespace_onboarding_action_spec.rb
+++ b/spec/models/namespace_onboarding_action_spec.rb
@@ -5,7 +5,13 @@ require 'spec_helper'
RSpec.describe NamespaceOnboardingAction do
let(:namespace) { build(:namespace) }
- it { is_expected.to belong_to :namespace }
+ describe 'associations' do
+ it { is_expected.to belong_to(:namespace).required }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:action) }
+ end
describe '.completed?' do
let(:action) { :subscription_created }
diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb
index fe6c0f0a556..e154e691d5f 100644
--- a/spec/requests/jwt_controller_spec.rb
+++ b/spec/requests/jwt_controller_spec.rb
@@ -5,13 +5,13 @@ require 'spec_helper'
RSpec.describe JwtController do
include_context 'parsed logs'
- let(:service) { double(execute: {}) }
- let(:service_class) { double(new: service) }
- let(:service_name) { 'test' }
+ let(:service) { double(execute: {} ) }
+ let(:service_class) { Auth::ContainerRegistryAuthenticationService }
+ let(:service_name) { 'container_registry' }
let(:parameters) { { service: service_name } }
before do
- stub_const('JwtController::SERVICES', service_name => service_class)
+ allow(service_class).to receive(:new).and_return(service)
end
shared_examples 'user logging' do
@@ -22,194 +22,266 @@ RSpec.describe JwtController do
end
end
- context 'existing service' do
- subject! { get '/jwt/auth', params: parameters }
+ context 'authenticating against container registry' do
+ context 'existing service' do
+ subject! { get '/jwt/auth', params: parameters }
- it { expect(response).to have_gitlab_http_status(:ok) }
+ it { expect(response).to have_gitlab_http_status(:ok) }
- context 'returning custom http code' do
- let(:service) { double(execute: { http_status: 505 }) }
+ context 'returning custom http code' do
+ let(:service) { double(execute: { http_status: 505 }) }
- it { expect(response).to have_gitlab_http_status(:http_version_not_supported) }
+ it { expect(response).to have_gitlab_http_status(:http_version_not_supported) }
+ end
end
- end
- context 'when using authenticated request' do
- shared_examples 'rejecting a blocked user' do
- context 'with blocked user' do
- let(:user) { create(:user, :blocked) }
+ context 'when using authenticated request' do
+ shared_examples 'rejecting a blocked user' do
+ context 'with blocked user' do
+ let(:user) { create(:user, :blocked) }
- it 'rejects the request as unauthorized' do
- expect(response).to have_gitlab_http_status(:unauthorized)
- expect(response.body).to include('HTTP Basic: Access denied')
+ it 'rejects the request as unauthorized' do
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ expect(response.body).to include('HTTP Basic: Access denied')
+ end
end
end
- end
- context 'using CI token' do
- let(:user) { create(:user) }
- let(:build) { create(:ci_build, :running, user: user) }
- let(:project) { build.project }
- let(:headers) { { authorization: credentials('gitlab-ci-token', build.token) } }
+ context 'using CI token' do
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build, :running, user: user) }
+ let(:project) { build.project }
+ let(:headers) { { authorization: credentials('gitlab-ci-token', build.token) } }
- context 'project with enabled CI' do
- subject! { get '/jwt/auth', params: parameters, headers: headers }
-
- it { expect(service_class).to have_received(:new).with(project, user, ActionController::Parameters.new(parameters).permit!) }
+ context 'project with enabled CI' do
+ subject! { get '/jwt/auth', params: parameters, headers: headers }
- it_behaves_like 'user logging'
- end
+ it { expect(service_class).to have_received(:new).with(project, user, ActionController::Parameters.new(parameters).permit!) }
- context 'project with disabled CI' do
- before do
- project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
+ it_behaves_like 'user logging'
end
- subject! { get '/jwt/auth', params: parameters, headers: headers }
+ context 'project with disabled CI' do
+ before do
+ project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
+ end
- it { expect(response).to have_gitlab_http_status(:unauthorized) }
- end
+ subject! { get '/jwt/auth', params: parameters, headers: headers }
- context 'using deploy tokens' do
- let(:deploy_token) { create(:deploy_token, read_registry: true, projects: [project]) }
- let(:headers) { { authorization: credentials(deploy_token.username, deploy_token.token) } }
+ it { expect(response).to have_gitlab_http_status(:unauthorized) }
+ end
- subject! { get '/jwt/auth', params: parameters, headers: headers }
+ context 'using deploy tokens' do
+ let(:deploy_token) { create(:deploy_token, read_registry: true, projects: [project]) }
+ let(:headers) { { authorization: credentials(deploy_token.username, deploy_token.token) } }
- it 'authenticates correctly' do
- expect(response).to have_gitlab_http_status(:ok)
- expect(service_class).to have_received(:new).with(nil, deploy_token, ActionController::Parameters.new(parameters).permit!)
- end
+ subject! { get '/jwt/auth', params: parameters, headers: headers }
- it 'does not log a user' do
- expect(log_data.keys).not_to include(%w(username user_id))
+ it 'authenticates correctly' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(service_class).to have_received(:new).with(nil, deploy_token, ActionController::Parameters.new(parameters).permit!)
+ end
+
+ it 'does not log a user' do
+ expect(log_data.keys).not_to include(%w(username user_id))
+ end
end
- end
- context 'using personal access tokens' do
- let(:pat) { create(:personal_access_token, user: user, scopes: ['read_registry']) }
- let(:headers) { { authorization: credentials('personal_access_token', pat.token) } }
+ context 'using personal access tokens' do
+ let(:pat) { create(:personal_access_token, user: user, scopes: ['read_registry']) }
+ let(:headers) { { authorization: credentials('personal_access_token', pat.token) } }
- before do
- stub_container_registry_config(enabled: true)
+ before do
+ stub_container_registry_config(enabled: true)
+ end
+
+ subject! { get '/jwt/auth', params: parameters, headers: headers }
+
+ it 'authenticates correctly' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters).permit!)
+ end
+
+ it_behaves_like 'rejecting a blocked user'
+ it_behaves_like 'user logging'
end
+ end
+
+ context 'using User login' do
+ let(:user) { create(:user) }
+ let(:headers) { { authorization: credentials(user.username, user.password) } }
subject! { get '/jwt/auth', params: parameters, headers: headers }
- it 'authenticates correctly' do
- expect(response).to have_gitlab_http_status(:ok)
- expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters).permit!)
- end
+ it { expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters).permit!) }
it_behaves_like 'rejecting a blocked user'
- it_behaves_like 'user logging'
- end
- end
-
- context 'using User login' do
- let(:user) { create(:user) }
- let(:headers) { { authorization: credentials(user.username, user.password) } }
- subject! { get '/jwt/auth', params: parameters, headers: headers }
+ context 'when passing a flat array of scopes' do
+ # We use this trick to make rails to generate a query_string:
+ # scope=scope1&scope=scope2
+ # It works because :scope and 'scope' are the same as string, but different objects
+ let(:parameters) do
+ {
+ :service => service_name,
+ :scope => 'scope1',
+ 'scope' => 'scope2'
+ }
+ end
- it { expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters).permit!) }
+ let(:service_parameters) do
+ ActionController::Parameters.new({ service: service_name, scopes: %w(scope1 scope2) }).permit!
+ end
- it_behaves_like 'rejecting a blocked user'
+ it { expect(service_class).to have_received(:new).with(nil, user, service_parameters) }
- context 'when passing a flat array of scopes' do
- # We use this trick to make rails to generate a query_string:
- # scope=scope1&scope=scope2
- # It works because :scope and 'scope' are the same as string, but different objects
- let(:parameters) do
- {
- :service => service_name,
- :scope => 'scope1',
- 'scope' => 'scope2'
- }
+ it_behaves_like 'user logging'
end
- let(:service_parameters) do
- ActionController::Parameters.new({ service: service_name, scopes: %w(scope1 scope2) }).permit!
+ context 'when user has 2FA enabled' do
+ let(:user) { create(:user, :two_factor) }
+
+ context 'without personal token' do
+ it 'rejects the authorization attempt' do
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
+ end
+ end
+
+ context 'with personal token' do
+ let(:access_token) { create(:personal_access_token, user: user) }
+ let(:headers) { { authorization: credentials(user.username, access_token.token) } }
+
+ it 'accepts the authorization attempt' do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
end
- it { expect(service_class).to have_received(:new).with(nil, user, service_parameters) }
+ it 'does not cause session based checks to be activated' do
+ expect(Gitlab::Session).not_to receive(:with_session)
+
+ get '/jwt/auth', params: parameters, headers: headers
- it_behaves_like 'user logging'
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
- context 'when user has 2FA enabled' do
- let(:user) { create(:user, :two_factor) }
+ context 'using invalid login' do
+ let(:headers) { { authorization: credentials('invalid', 'password') } }
- context 'without personal token' do
+ context 'when internal auth is enabled' do
it 'rejects the authorization attempt' do
+ get '/jwt/auth', params: parameters, headers: headers
+
expect(response).to have_gitlab_http_status(:unauthorized)
- expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
+ expect(response.body).not_to include('You must use a personal access token with \'api\' scope for Git over HTTP')
end
end
- context 'with personal token' do
- let(:access_token) { create(:personal_access_token, user: user) }
- let(:headers) { { authorization: credentials(user.username, access_token.token) } }
+ context 'when internal auth is disabled' do
+ it 'rejects the authorization attempt with personal access token message' do
+ allow_next_instance_of(ApplicationSetting) do |instance|
+ allow(instance).to receive(:password_authentication_enabled_for_git?) { false }
+ end
+ get '/jwt/auth', params: parameters, headers: headers
- it 'accepts the authorization attempt' do
- expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
end
end
end
+ end
- it 'does not cause session based checks to be activated' do
- expect(Gitlab::Session).not_to receive(:with_session)
-
- get '/jwt/auth', params: parameters, headers: headers
+ context 'when using unauthenticated request' do
+ it 'accepts the authorization attempt' do
+ get '/jwt/auth', params: parameters
expect(response).to have_gitlab_http_status(:ok)
end
+
+ it 'allows read access' do
+ expect(service).to receive(:execute).with(authentication_abilities: Gitlab::Auth.read_only_authentication_abilities)
+
+ get '/jwt/auth', params: parameters
+ end
end
- context 'using invalid login' do
- let(:headers) { { authorization: credentials('invalid', 'password') } }
+ context 'unknown service' do
+ subject! { get '/jwt/auth', params: { service: 'unknown' } }
- context 'when internal auth is enabled' do
- it 'rejects the authorization attempt' do
- get '/jwt/auth', params: parameters, headers: headers
+ it { expect(response).to have_gitlab_http_status(:not_found) }
+ end
- expect(response).to have_gitlab_http_status(:unauthorized)
- expect(response.body).not_to include('You must use a personal access token with \'api\' scope for Git over HTTP')
- end
- end
+ def credentials(login, password)
+ ActionController::HttpAuthentication::Basic.encode_credentials(login, password)
+ end
+ end
- context 'when internal auth is disabled' do
- it 'rejects the authorization attempt with personal access token message' do
- allow_next_instance_of(ApplicationSetting) do |instance|
- allow(instance).to receive(:password_authentication_enabled_for_git?) { false }
- end
- get '/jwt/auth', params: parameters, headers: headers
+ context 'authenticating against dependency proxy' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :private, group: group) }
+ let_it_be(:group_deploy_token) { create(:deploy_token, :group, groups: [group]) }
+ let_it_be(:project_deploy_token) { create(:deploy_token, :project, projects: [project]) }
+ let_it_be(:service_name) { 'dependency_proxy' }
+ let(:headers) { { authorization: credentials(credential_user, credential_password) } }
+ let(:params) { { account: credential_user, client_id: 'docker', offline_token: true, service: service_name } }
+
+ before do
+ stub_config(dependency_proxy: { enabled: true })
+ end
- expect(response).to have_gitlab_http_status(:unauthorized)
- expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
- end
+ subject { get '/jwt/auth', params: params, headers: headers }
+
+ shared_examples 'with valid credentials' do
+ it 'returns token successfully' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(json_response['token']).to be_present
end
end
- end
- context 'when using unauthenticated request' do
- it 'accepts the authorization attempt' do
- get '/jwt/auth', params: parameters
+ context 'with personal access token' do
+ let(:credential_user) { nil }
+ let(:credential_password) { personal_access_token.token }
- expect(response).to have_gitlab_http_status(:ok)
+ it_behaves_like 'with valid credentials'
end
- it 'allows read access' do
- expect(service).to receive(:execute).with(authentication_abilities: Gitlab::Auth.read_only_authentication_abilities)
+ context 'with user credentials token' do
+ let(:credential_user) { user.username }
+ let(:credential_password) { user.password }
- get '/jwt/auth', params: parameters
+ it_behaves_like 'with valid credentials'
end
- end
- context 'unknown service' do
- subject! { get '/jwt/auth', params: { service: 'unknown' } }
+ context 'with group deploy token' do
+ let(:credential_user) { group_deploy_token.username }
+ let(:credential_password) { group_deploy_token.token }
- it { expect(response).to have_gitlab_http_status(:not_found) }
+ it_behaves_like 'with valid credentials'
+ end
+
+ context 'with project deploy token' do
+ let(:credential_user) { project_deploy_token.username }
+ let(:credential_password) { project_deploy_token.token }
+
+ it_behaves_like 'with valid credentials'
+ end
+
+ context 'with invalid credentials' do
+ let(:credential_user) { 'foo' }
+ let(:credential_password) { 'bar' }
+
+ it 'returns unauthorized' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
end
def credentials(login, password)
diff --git a/spec/routing/group_routing_spec.rb b/spec/routing/group_routing_spec.rb
index f4d5ccc81b6..f171c2faf5e 100644
--- a/spec/routing/group_routing_spec.rb
+++ b/spec/routing/group_routing_spec.rb
@@ -81,6 +81,10 @@ RSpec.describe "Groups", "routing" do
end
describe 'dependency proxy for containers' do
+ it 'routes to #authenticate' do
+ expect(get('/v2')).to route_to('groups/dependency_proxy_auth#authenticate')
+ end
+
context 'image name without namespace' do
it 'routes to #manifest' do
expect(get('/v2/gitlabhq/dependency_proxy/containers/ruby/manifests/2.3.6'))
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index 73d20cad4dd..0f931e7cc9e 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -179,6 +179,30 @@ RSpec.describe Profiles::KeysController, "routing" do
end
end
+# keys GET /gpg_keys gpg_keys#index
+# key POST /gpg_keys gpg_keys#create
+# PUT /gpg_keys/:id gpg_keys#revoke
+# DELETE /gpg_keys/:id gpg_keys#desroy
+RSpec.describe Profiles::GpgKeysController, "routing" do
+ it "to #index" do
+ expect(get("/profile/gpg_keys")).to route_to('profiles/gpg_keys#index')
+ end
+
+ it "to #create" do
+ expect(post("/profile/gpg_keys")).to route_to('profiles/gpg_keys#create')
+ end
+
+ it "to #destroy" do
+ expect(delete("/profile/gpg_keys/1")).to route_to('profiles/gpg_keys#destroy', id: '1')
+ end
+
+ it "to #get_keys" do
+ allow_any_instance_of(::Constraints::UserUrlConstrainer).to receive(:matches?).and_return(true)
+
+ expect(get("/foo.gpg")).to route_to('profiles/gpg_keys#get_keys', username: 'foo')
+ end
+end
+
# emails GET /emails(.:format) emails#index
# POST /keys(.:format) emails#create
# DELETE /keys/:id(.:format) keys#destroy
diff --git a/spec/services/auth/dependency_proxy_authentication_service_spec.rb b/spec/services/auth/dependency_proxy_authentication_service_spec.rb
new file mode 100644
index 00000000000..ba50149f53a
--- /dev/null
+++ b/spec/services/auth/dependency_proxy_authentication_service_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Auth::DependencyProxyAuthenticationService do
+ let_it_be(:user) { create(:user) }
+ let(:service) { Auth::DependencyProxyAuthenticationService.new(nil, user) }
+
+ before do
+ stub_config(dependency_proxy: { enabled: true })
+ end
+
+ describe '#execute' do
+ subject { service.execute(authentication_abilities: nil) }
+
+ context 'dependency proxy is not enabled' do
+ before do
+ stub_config(dependency_proxy: { enabled: false })
+ end
+
+ it 'returns not found' do
+ result = subject
+
+ expect(result[:http_status]).to eq(404)
+ expect(result[:message]).to eq('dependency proxy not enabled')
+ end
+ end
+
+ context 'without a user' do
+ let(:user) { nil }
+
+ it 'returns forbidden' do
+ result = subject
+
+ expect(result[:http_status]).to eq(403)
+ expect(result[:message]).to eq('access forbidden')
+ end
+ end
+
+ context 'with a user' do
+ it 'returns a token' do
+ expect(subject[:token]).not_to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/services/dependency_proxy/auth_token_service_spec.rb b/spec/services/dependency_proxy/auth_token_service_spec.rb
new file mode 100644
index 00000000000..4b96f9d75a9
--- /dev/null
+++ b/spec/services/dependency_proxy/auth_token_service_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe DependencyProxy::AuthTokenService do
+ include DependencyProxyHelpers
+
+ describe '.decoded_token_payload' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:token) { build_jwt(user) }
+
+ subject { described_class.decoded_token_payload(token.encoded) }
+
+ it 'returns the user' do
+ result = subject
+
+ expect(result['user_id']).to eq(user.id)
+ end
+
+ it 'raises an error if the token is expired' do
+ travel_to(Time.zone.now + Auth::DependencyProxyAuthenticationService.token_expire_at + 1.minute) do
+ expect { subject }.to raise_error(JWT::ExpiredSignature)
+ end
+ end
+
+ it 'raises an error if decoding fails' do
+ allow(JWT).to receive(:decode).and_raise(JWT::DecodeError)
+
+ expect { subject }.to raise_error(JWT::DecodeError)
+ end
+
+ it 'raises an error if signature is immature' do
+ allow(JWT).to receive(:decode).and_raise(JWT::ImmatureSignature)
+
+ expect { subject }.to raise_error(JWT::ImmatureSignature)
+ end
+ end
+end
diff --git a/spec/services/onboarding_progress_service_spec.rb b/spec/services/onboarding_progress_service_spec.rb
new file mode 100644
index 00000000000..edf40dfeed1
--- /dev/null
+++ b/spec/services/onboarding_progress_service_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe OnboardingProgressService do
+ describe '#execute' do
+ let_it_be(:namespace) { build(:namespace) }
+ let(:action) { :namespace_action }
+
+ subject(:execute_service) { described_class.new(namespace).execute(action: action) }
+
+ it 'records a namespace onboarding progress action' do
+ expect(NamespaceOnboardingAction).to receive(:create_action)
+ .with(namespace, :namespace_action)
+
+ subject
+ end
+ end
+end
diff --git a/spec/services/post_receive_service_spec.rb b/spec/services/post_receive_service_spec.rb
index 7c4b7f51cc3..4e303bfc20a 100644
--- a/spec/services/post_receive_service_spec.rb
+++ b/spec/services/post_receive_service_spec.rb
@@ -45,6 +45,12 @@ RSpec.describe PostReceiveService do
it 'does not return error' do
expect(subject).to be_empty
end
+
+ it 'does not record a namespace onboarding progress action' do
+ expect(NamespaceOnboardingAction).not_to receive(:create_action)
+
+ subject
+ end
end
context 'when repository is nil' do
@@ -80,6 +86,13 @@ RSpec.describe PostReceiveService do
expect(response.reference_counter_decreased).to be(true)
end
+
+ it 'records a namespace onboarding progress action' do
+ expect(NamespaceOnboardingAction).to receive(:create_action)
+ .with(project.namespace, :git_write)
+
+ subject
+ end
end
context 'with Project' do
diff --git a/spec/support/helpers/dependency_proxy_helpers.rb b/spec/support/helpers/dependency_proxy_helpers.rb
index 545b9d1f4d0..0074cfb7931 100644
--- a/spec/support/helpers/dependency_proxy_helpers.rb
+++ b/spec/support/helpers/dependency_proxy_helpers.rb
@@ -25,6 +25,13 @@ module DependencyProxyHelpers
.to_return(status: status, body: body)
end
+ def build_jwt(user = nil, expire_time: nil)
+ JSONWebToken::HMACToken.new(::Auth::DependencyProxyAuthenticationService.secret).tap do |jwt|
+ jwt['user_id'] = user.id if user
+ jwt.expire_time = expire_time || jwt.issued_at + 1.minute
+ end
+ end
+
private
def registry
diff --git a/spec/support/helpers/gpg_helpers.rb b/spec/support/helpers/gpg_helpers.rb
index f4df1cf601c..389e5818dbe 100644
--- a/spec/support/helpers/gpg_helpers.rb
+++ b/spec/support/helpers/gpg_helpers.rb
@@ -144,6 +144,145 @@ module GpgHelpers
'5F7EA3981A5845B141ABD522CCFBE19F00AC8B1D'
end
+ def secret_key2
+ <<~KEY.strip
+ -----BEGIN PGP PRIVATE KEY BLOCK-----
+
+ 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
+ 4f6xMfFu2A==
+ =3YL6
+ -----END PGP PRIVATE KEY BLOCK-----
+ KEY
+ end
+
+ def public_key2
+ <<~KEY.strip
+ -----BEGIN PGP PUBLIC KEY BLOCK-----
+
+ mQGNBF+7O0oBDADvRto4K9PT83Lbyp/qaMPIzBbXHB6ljdDoyb+Pn2UrHk9MhB5v
+ bTgBv+rctOabmimPPalcyaxOQ1GtrYizo1l33YQZupSvaOoStVLWqnBx8eKKcUv8
+ QucS3S2qFhj9G0tdHW7RW2BGrSwEM09d2xFsFKKAj/4RTTU5idYWrvB24DNcrBh+
+ iKsoa+rmJf1bwL6Mn9f9NwzundG16qibY/UwMlltQriWaVMn2AKVuu6HrX9pe3g5
+ Er2Szjc7DZitt6eAy3PmuWHXzDCCvsO7iPxXlywY49hLhDen3/Warwn1pSbp+im4
+ /0oJExLZBSS1xHbRSQoR6matF0+V/6TQz8Yo3g8z9HgyEtn1V7QJo3PoNrnEl73e
+ 9yslTqVtzba0Q132oRoO7eEYf82KrPOmVGj6Q9LpSXFLfsl3GlPgoBxRZXpT62CV
+ 3rGalIa2yKmcBQtyICjR1+PTIAJcVIPyr92xTo4RfLwVFW0czX7LM2H0FT2Ksj7L
+ U450ewBz8N6bFDMAEQEAAbQtTmFubmllIEJlcm5oYXJkIDxuYW5uaWUuYmVybmhh
+ cmRAZXhhbXBsZS5jb20+iQHUBBMBCgA+FiEExEem9r/Zzvj7NxeFVxYlqTAkEXkF
+ Al+7O0oCGwMFCQPCZwAFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQVxYlqTAk
+ EXk9xwv/WlJJGJ+QyGeJAhySG3z3bQnFwb2CusF2LbwcAETDgbkfopkkf34Vbb9A
+ 7kM7peZ7Va0Edsg09XdkBUAdaqKQn78HiZJC5n0grXcj1c67AdssYm9TGVM6AC3K
+ 3Vm3wVV0X+ng31rdDpjfIqfYDAvwhMc8H/MHs/dCRSIxEGWK8UKhWLUrX+wN+HNM
+ VbzWPGwoTMWiDa/ofA9INhqN+u+mJkTaP+a4R3LTgL5hp+kUDOaBNc0rqH7vgj+0
+ 37NTL8vox18J4qgNbRIsywclMYBJDwfA4w1phtsMu1BKPiOu2kue18fyGDtboXUP
+ FOJjf5OEwJsu+MFogWeAVuHN/eeiqOAFCYW+TT6Ehc6BnJ8vWCMSDgs3t6i94gNZ
+ tvEty2EAheHEBD1alU4c6S3VENdh5q2KkWIVFxgNtungo03eAVfjUhMjrrEu0LC/
+ Rizo7Me0kG7rfdn9oIwp4MTn7Cst1wGEWdi9UO4NJf1C+P9rFQuGhMaj+8gb1uBd
+ jPG8WOOauQGNBF+7O0oBDADhzNAvjiphKHsa4O5s3BePLQ+DJz+KrS8f9mb66to/
+ w9BlUtnm/L4gVgiIYqGhH7TSDaGhvIDMf3iKKBnKrWeBe0W8cdq3FlzWC/AHUahE
+ FxFm0l6nq0pOIiAVQ58IPaB/0a5YCY7tU2yfw8llZUN8dWJ7cSsBGpa6Q9/9y4x5
+ /9VPDPduXRv22KCfDbHXuFS79ubmueFfrOa1CLXRhCy3dUXCyePUYuwxixXJRTJQ
+ Jm+A6c8TFIL+cji7IEzzDAiNexfGzEfu+Qj1/9PzX8aIn6C5Tf4qB1pcGa4uYr8K
+ 1aCENcVt6+GA5gMdcplYXmtA212RyPqQmnJIjxDdS7AJYcivqG2qF5CvqzKY5/A+
+ e9+GLyRM36P8LpB8+XHMoYNMNmOl5KX6WZ1tRw/xxgv1iKX3PcqdnoFwsOCNVpTW
+ lxvjsyve8VQUplORSakIhfKh1VWu7j8AKXWe9S3zMYQDq5G8VrTOVb1pPvPgiNxo
+ 9u1OXi2H9UTXhCWYZ6FIe2UAEQEAAYkBvAQYAQoAJhYhBMRHpva/2c74+zcXhVcW
+ JakwJBF5BQJfuztKAhsMBQkDwmcAAAoJEFcWJakwJBF5T/ML/3Ml7+493hQuoC9O
+ 3HOANkimc0pGxILVeJmJmnfbMDJ71fU84h2+xAyk2PZc48wVYKju9THJzdRk+XBP
+ O+G6mSBupSt53JIYb5NijotNTmJmHYpG1yb+9FjDEFWTlxK1mr5wjSUxlGWa/O46
+ XjxzCSEUP1SknLWbTOucV8KOmPWL3DupvGINIIQxe5eJ9SMjlHvUn4rq8sd11FT2
+ bQrd+xMx8gP5cearPqB7qVRlHjtOKn29gTV90kIwamRke8KxSoJh+xT057aKI2+M
+ Cu7RC8TgThmUVCWgwUzXlsw1Qe8ySc6CmjIBftfolQYPDSq1u8RSBAB+t2Xwprvd
+ edr9SQihzBk5GCGBJ/npEcgF2jk26sJqoXYbvyQGtqSDQ925oP7OstyOE4FTH7sQ
+ mBvP01Ikdgwkm0cthLSpWY4QI+09Aeg+rZ80EtfvvAKquDGA33no8YGnn+epeLqy
+ scIh4WG3bIoHk9JlFCcwIp9U65IfR1fTcvlTdzZN4f6xMfFu2A==
+ =RAwd
+ -----END PGP PUBLIC KEY BLOCK-----
+ KEY
+ end
+
+ def fingerprint2
+ 'C447A6F6BFD9CEF8FB371785571625A930241179'
+ end
+
def names
['Nannie Bernhard']
end