Commit 894555b6 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 3eb195b4 358dff7b
...@@ -102,14 +102,6 @@ MergeRequest.prototype.initMRBtnListeners = function() { ...@@ -102,14 +102,6 @@ MergeRequest.prototype.initMRBtnListeners = function() {
return $('.btn-close, .btn-reopen').on('click', function(e) { return $('.btn-close, .btn-reopen').on('click', function(e) {
const $this = $(this); const $this = $(this);
const shouldSubmit = $this.hasClass('btn-comment'); 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')) { if (shouldSubmit && $this.data('submitted')) {
return; return;
} }
...@@ -171,10 +163,6 @@ MergeRequest.decreaseCounter = function(by = 1) { ...@@ -171,10 +163,6 @@ MergeRequest.decreaseCounter = function(by = 1) {
MergeRequest.hideCloseButton = function() { MergeRequest.hideCloseButton = function() {
const el = document.querySelector('.merge-request .js-issuable-actions'); 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 // Dropdown for mobile screen
el.querySelector('li.js-close-item').classList.add('hidden'); el.querySelector('li.js-close-item').classList.add('hidden');
}; };
......
...@@ -103,9 +103,15 @@ export function getCommentedLines(selectedCommentPosition, diffLines) { ...@@ -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 { start, end } = selectedCommentPosition;
const startLine = diffLines.findIndex(l => l.line_code === start.line_code); const startLine = diffLines.findIndex(findLineCodeIndex(start));
const endLine = diffLines.findIndex(l => l.line_code === end.line_code); const endLine = diffLines.findIndex(findLineCodeIndex(end));
return { startLine, endLine }; return { startLine, endLine };
} }
...@@ -41,12 +41,6 @@ ...@@ -41,12 +41,6 @@
@include media-breakpoint-down(xs) { @include media-breakpoint-down(xs) {
width: 100%; width: 100%;
margin-top: 10px; margin-top: 10px;
> .issue-btn-group {
> .btn {
width: 100%;
}
}
} }
} }
......
...@@ -890,24 +890,6 @@ ...@@ -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 * Following overrides are done to prevent
* legacy dropdown styles from influencing * legacy dropdown styles from influencing
......
# 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
# 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
# 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
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
module Groups module Groups
class DependencyProxiesController < Groups::ApplicationController class DependencyProxiesController < Groups::ApplicationController
include DependencyProxyAccess include DependencyProxy::GroupAccess
before_action :authorize_admin_dependency_proxy!, only: :update before_action :authorize_admin_dependency_proxy!, only: :update
before_action :dependency_proxy before_action :dependency_proxy
......
# frozen_string_literal: true
class Groups::DependencyProxyAuthController < ApplicationController
include DependencyProxy::Auth
feature_category :dependency_proxy
def authenticate
render plain: '', status: :ok
end
end
# frozen_string_literal: true # frozen_string_literal: true
class Groups::DependencyProxyForContainersController < Groups::ApplicationController class Groups::DependencyProxyForContainersController < Groups::ApplicationController
include DependencyProxyAccess include DependencyProxy::Auth
include DependencyProxy::GroupAccess
include SendFileUpload include SendFileUpload
before_action :ensure_token_granted! before_action :ensure_token_granted!
...@@ -9,7 +10,7 @@ class Groups::DependencyProxyForContainersController < Groups::ApplicationContro ...@@ -9,7 +10,7 @@ class Groups::DependencyProxyForContainersController < Groups::ApplicationContro
attr_reader :token attr_reader :token
feature_category :package_registry feature_category :dependency_proxy
def manifest def manifest
result = DependencyProxy::PullManifestService.new(image, tag, token).execute result = DependencyProxy::PullManifestService.new(image, tag, token).execute
......
...@@ -11,7 +11,8 @@ class JwtController < ApplicationController ...@@ -11,7 +11,8 @@ class JwtController < ApplicationController
feature_category :authentication_and_authorization feature_category :authentication_and_authorization
SERVICES = { SERVICES = {
Auth::ContainerRegistryAuthenticationService::AUDIENCE => Auth::ContainerRegistryAuthenticationService ::Auth::ContainerRegistryAuthenticationService::AUDIENCE => ::Auth::ContainerRegistryAuthenticationService,
::Auth::DependencyProxyAuthenticationService::AUDIENCE => ::Auth::DependencyProxyAuthenticationService
}.freeze }.freeze
def auth def auth
......
...@@ -15,7 +15,7 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -15,7 +15,7 @@ class Projects::JobsController < Projects::ApplicationController
before_action :authorize_create_proxy_build!, only: :proxy_websocket_authorize before_action :authorize_create_proxy_build!, only: :proxy_websocket_authorize
before_action :verify_proxy_request!, only: :proxy_websocket_authorize before_action :verify_proxy_request!, only: :proxy_websocket_authorize
before_action do 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 end
before_action only: :index do before_action only: :index do
frontend_experimentation_tracking_data(:jobs_empty_state, 'click_button') frontend_experimentation_tracking_data(:jobs_empty_state, 'click_button')
......
...@@ -319,12 +319,6 @@ module IssuablesHelper ...@@ -319,12 +319,6 @@ module IssuablesHelper
issuable_path(issuable, close_reopen_params(issuable, :reopen)) issuable_path(issuable, close_reopen_params(issuable, :reopen))
end 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) def issuable_path(issuable, *options)
polymorphic_path(issuable, *options) polymorphic_path(issuable, *options)
end end
......
...@@ -39,19 +39,6 @@ module MergeRequestsHelper ...@@ -39,19 +39,6 @@ module MergeRequestsHelper
end end
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) def merge_path_description(merge_request, separator)
if merge_request.for_fork? 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}" "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 ...@@ -166,6 +153,12 @@ module MergeRequestsHelper
current_user.fork_of(project) current_user.fork_of(project)
end end
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 end
MergeRequestsHelper.prepend_if_ee('EE::MergeRequestsHelper') MergeRequestsHelper.prepend_if_ee('EE::MergeRequestsHelper')
# frozen_string_literal: true # frozen_string_literal: true
class DependencyProxy::Registry class DependencyProxy::Registry
AUTH_URL = 'https://auth.docker.io'.freeze AUTH_URL = 'https://auth.docker.io'
LIBRARY_URL = 'https://registry-1.docker.io/v2'.freeze LIBRARY_URL = 'https://registry-1.docker.io/v2'
PROXY_AUTH_URL = Gitlab::Utils.append_path(Gitlab.config.gitlab.url, "jwt/auth")
class << self class << self
def auth_url(image) def auth_url(image)
...@@ -17,6 +18,10 @@ class DependencyProxy::Registry ...@@ -17,6 +18,10 @@ class DependencyProxy::Registry
"#{LIBRARY_URL}/#{image_path(image)}/blobs/#{blob_sha}" "#{LIBRARY_URL}/#{image_path(image)}/blobs/#{blob_sha}"
end end
def authenticate_header
"Bearer realm=\"#{PROXY_AUTH_URL}\",service=\"#{::Auth::DependencyProxyAuthenticationService::AUDIENCE}\""
end
private private
def image_path(image) def image_path(image)
......
# frozen_string_literal: true # frozen_string_literal: true
class NamespaceOnboardingAction < ApplicationRecord class NamespaceOnboardingAction < ApplicationRecord
belongs_to :namespace belongs_to :namespace, optional: false
validates :action, presence: true
ACTIONS = { ACTIONS = {
subscription_created: 1 subscription_created: 1,
git_write: 2
}.freeze }.freeze
enum action: ACTIONS enum action: ACTIONS
......
...@@ -31,3 +31,5 @@ class UserDetail < ApplicationRecord ...@@ -31,3 +31,5 @@ class UserDetail < ApplicationRecord
self.bio = '' if bio_changed? && bio.nil? self.bio = '' if bio_changed? && bio.nil?
end end
end end
UserDetail.prepend_if_ee('EE::UserDetail')
# 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
# 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
...@@ -40,6 +40,8 @@ class PostReceiveService ...@@ -40,6 +40,8 @@ class PostReceiveService
response.add_basic_message(redirect_message) response.add_basic_message(redirect_message)
response.add_basic_message(project_created_message) response.add_basic_message(project_created_message)
record_onboarding_progress
end end
response response
...@@ -90,6 +92,10 @@ class PostReceiveService ...@@ -90,6 +92,10 @@ class PostReceiveService
banner&.message banner&.message
end end
def record_onboarding_progress
NamespaceOnboardingAction.create_action(project.namespace, :git_write)
end
end end
PostReceiveService.prepend_if_ee('EE::PostReceiveService') PostReceiveService.prepend_if_ee('EE::PostReceiveService')
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
- link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('user/packages/dependency_proxy/index') } - 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 } = _('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) - if can?(current_user, :admin_dependency_proxy, @group)
= form_for(@dependency_proxy, method: :put, url: group_dependency_proxy_path(@group)) do |f| = form_for(@dependency_proxy, method: :put, url: group_dependency_proxy_path(@group)) do |f|
.form-group .form-group
......
- display_issuable_type = issuable_display_type(issuable) - display_issuable_type = issuable_display_type(@merge_request)
- button_action_class = issuable.closed? ? 'btn-default' : 'btn-warning btn-warning-secondary' - button_action_class = @merge_request.closed? ? 'btn-default' : 'btn-warning btn-warning-secondary'
- button_class = "btn gl-button #{!issuable.closed? && 'js-draft-toggle-button'}" - button_class = "btn gl-button #{!@merge_request.closed? && 'js-draft-toggle-button'}"
- toggle_class = "btn gl-button dropdown-toggle" - 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 .float-left.btn-group.gl-ml-3.gl-display-none.gl-display-md-flex
= link_to issuable.closed? ? reopen_issuable_path(issuable) : toggle_draft_issuable_path(issuable), method: :put, class: "#{button_class} #{button_action_class}" do = 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 issuable.closed? - if @merge_request.closed?
= _('Reopen') = _('Reopen')
= display_issuable_type = display_issuable_type
- else - else
= issuable.work_in_progress? ? _('Mark as ready') : _('Mark as draft') = @merge_request.work_in_progress? ? _('Mark as ready') : _('Mark as draft')
- if !issuable.closed? || !issuable_author_is_current_user(issuable) - 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 = button_tag type: 'button', class: "#{toggle_class} #{button_action_class}", data: { 'toggle' => 'dropdown' } do
%span.sr-only= _('Toggle dropdown') %span.gl-sr-only= _('Toggle dropdown')
= sprite_icon "angle-down", size: 12 = sprite_icon "angle-down", size: 12
%ul.js-issuable-close-menu.dropdown-menu.dropdown-menu-right %ul.dropdown-menu.dropdown-menu-right
- if issuable.open? - if @merge_request.open?
%li %li
= link_to close_issuable_path(issuable), method: :put do = link_to close_issuable_path(@merge_request), method: :put do
.description .description
%strong.title %strong.title
= _('Close') = _('Close')
= display_issuable_type = display_issuable_type
- unless issuable_author_is_current_user(issuable) - unless issuable_author_is_current_user(@merge_request)
- unless issuable.closed? - unless @merge_request.closed?
%li.divider.droplab-item-ignore %li.divider.droplab-item-ignore
%li.report-item %li
%a.report-abuse-link{ href: new_abuse_report_path(user_id: issuable.author.id, ref_url: merge_request_url(issuable)) } %a{ href: new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request)) }
.description .description
%strong.title= _('Report abuse') %strong.title= _('Report abuse')
%p.text %p.text.gl-mb-0
= _('Report %{display_issuable_type} that are abusive, inappropriate or spam.') % { display_issuable_type: display_issuable_type.pluralize } = _('Report %{display_issuable_type} that are abusive, inappropriate or spam.') % { display_issuable_type: display_issuable_type.pluralize }
...@@ -25,8 +25,8 @@ ...@@ -25,8 +25,8 @@
= sprite_icon('chevron-double-lg-left') = sprite_icon('chevron-double-lg-left')
.detail-page-header-actions.js-issuable-actions .detail-page-header-actions.js-issuable-actions
.clearfix.issue-btn-group.dropdown .clearfix.dropdown
%button.gl-button.btn.btn-default.float-left.gl-display-md-none{ type: "button", data: { toggle: "dropdown" } } %button.gl-button.btn.btn-default.float-left.gl-display-md-none.gl-w-full{ type: "button", data: { toggle: "dropdown" } }
Options Options
= sprite_icon('chevron-down', css_class: 'gl-text-gray-500') = sprite_icon('chevron-down', css_class: 'gl-text-gray-500')
.dropdown-menu.dropdown-menu-right .dropdown-menu.dropdown-menu-right
...@@ -35,12 +35,12 @@ ...@@ -35,12 +35,12 @@
%li= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) %li= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
- if @merge_request.opened? - if @merge_request.opened?
%li %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'] } %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' = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request'
- if can_reopen_merge_request - if can_reopen_merge_request
%li{ class: merge_request_button_visibility(@merge_request, false) } %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 - 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)) %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 @@ ...@@ -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" = 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 - 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? - 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') = 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')
---
title: Render http and https URLs as clickable links in Job logs
merge_request: 48758
author: Łukasz Groszkowski @falxcerebri
type: added
---
title: Mark SCIM-created accounts as provisioned by group
merge_request: 48483
author:
type: added
---
title: Fix comment highlighting for unified diff components
merge_request: 49061
author:
type: fixed
...@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/281727 ...@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/281727
milestone: '13.6' milestone: '13.6'
type: development type: development
group: group::continuous integration group: group::continuous integration
default_enabled: false default_enabled: true
---
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
...@@ -125,7 +125,7 @@ end ...@@ -125,7 +125,7 @@ end
# Dependency proxy for containers # Dependency proxy for containers
# Because docker adds v2 prefix to URI this need to be outside of usual group routes # Because docker adds v2 prefix to URI this need to be outside of usual group routes
scope format: false do 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 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 get 'v2/*group_id/dependency_proxy/containers/*image/manifests/*tag' => 'groups/dependency_proxy_for_containers#manifest' # rubocop:todo Cop/PutGroupRoutesUnderScope
......
# 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
9d69938cda6db1510ed17d087cc1a582af1e5482d65e4fb457e34011e09c3469
\ No newline at end of file
...@@ -16998,6 +16998,7 @@ CREATE TABLE user_details ( ...@@ -16998,6 +16998,7 @@ CREATE TABLE user_details (
cached_markdown_version integer, cached_markdown_version integer,
webauthn_xid text, webauthn_xid text,
other_role text, other_role text,
provisioned_by_group_id bigint,
CONSTRAINT check_245664af82 CHECK ((char_length(webauthn_xid) <= 100)), CONSTRAINT check_245664af82 CHECK ((char_length(webauthn_xid) <= 100)),
CONSTRAINT check_b132136b01 CHECK ((char_length(other_role) <= 100)) CONSTRAINT check_b132136b01 CHECK ((char_length(other_role) <= 100))
); );
...@@ -22472,6 +22473,8 @@ CREATE INDEX index_user_custom_attributes_on_key_and_value ON user_custom_attrib ...@@ -22472,6 +22473,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 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 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); 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 +23070,9 @@ ALTER TABLE ONLY project_features ...@@ -23067,6 +23070,9 @@ ALTER TABLE ONLY project_features
ALTER TABLE ONLY ci_pipelines ALTER TABLE ONLY ci_pipelines
ADD CONSTRAINT fk_190998ef09 FOREIGN KEY (external_pull_request_id) REFERENCES external_pull_requests(id) ON DELETE SET NULL; 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 ALTER TABLE ONLY vulnerabilities
ADD CONSTRAINT fk_1d37cddf91 FOREIGN KEY (epic_id) REFERENCES epics(id) ON DELETE SET NULL; ADD CONSTRAINT fk_1d37cddf91 FOREIGN KEY (epic_id) REFERENCES epics(id) ON DELETE SET NULL;
......
...@@ -14144,6 +14144,7 @@ type Mutation { ...@@ -14144,6 +14144,7 @@ type Mutation {
namespaceIncreaseStorageTemporarily(input: NamespaceIncreaseStorageTemporarilyInput!): NamespaceIncreaseStorageTemporarilyPayload namespaceIncreaseStorageTemporarily(input: NamespaceIncreaseStorageTemporarilyInput!): NamespaceIncreaseStorageTemporarilyPayload
oncallScheduleCreate(input: OncallScheduleCreateInput!): OncallScheduleCreatePayload oncallScheduleCreate(input: OncallScheduleCreateInput!): OncallScheduleCreatePayload
oncallScheduleDestroy(input: OncallScheduleDestroyInput!): OncallScheduleDestroyPayload oncallScheduleDestroy(input: OncallScheduleDestroyInput!): OncallScheduleDestroyPayload
oncallScheduleUpdate(input: OncallScheduleUpdateInput!): OncallScheduleUpdatePayload
pipelineCancel(input: PipelineCancelInput!): PipelineCancelPayload pipelineCancel(input: PipelineCancelInput!): PipelineCancelPayload
pipelineDestroy(input: PipelineDestroyInput!): PipelineDestroyPayload pipelineDestroy(input: PipelineDestroyInput!): PipelineDestroyPayload
pipelineRetry(input: PipelineRetryInput!): PipelineRetryPayload pipelineRetry(input: PipelineRetryInput!): PipelineRetryPayload
...@@ -14838,6 +14839,61 @@ type OncallScheduleDestroyPayload { ...@@ -14838,6 +14839,61 @@ type OncallScheduleDestroyPayload {
oncallSchedule: IncidentManagementOncallSchedule oncallSchedule: IncidentManagementOncallSchedule
} }
"""
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 Represents a package
""" """
......
...@@ -41158,6 +41158,33 @@ ...@@ -41158,6 +41158,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "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", "name": "pipelineCancel",
"description": null, "description": null,
...@@ -44104,6 +44131,152 @@ ...@@ -44104,6 +44131,152 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "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", "kind": "OBJECT",
"name": "Package", "name": "Package",
...@@ -2272,6 +2272,16 @@ Autogenerated return type of OncallScheduleDestroy. ...@@ -2272,6 +2272,16 @@ Autogenerated return type of OncallScheduleDestroy.
| `errors` | String! => Array | Errors encountered during execution of the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `oncallSchedule` | IncidentManagementOncallSchedule | The on-call schedule | | `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 ### Package
Represents a package. Represents a package.
......
...@@ -587,7 +587,7 @@ tenses, words, and phrases: ...@@ -587,7 +587,7 @@ tenses, words, and phrases:
<!-- vale gitlab.Simplicity = NO --> <!-- vale gitlab.Simplicity = NO -->
- Avoid words like _easily_, _simply_, _handy_, and _useful._ If the user - Avoid words like _easily_, _simply_, _handy_, and _useful._ If the user
doesn't find the process to be these things, we lose their trust. doesn't find the process to be these things, we lose their trust.
<!-- vale gitlab.Simplicity = NO --> <!-- vale gitlab.Simplicity = YES -->
### Word usage clarifications ### Word usage clarifications
......
...@@ -8,6 +8,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w ...@@ -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. > - [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. > - [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 The GitLab Dependency Proxy is a local proxy you can use for your frequently-accessed
upstream images. upstream images.
...@@ -17,9 +19,7 @@ upstream image from a registry, acting as a pull-through cache. ...@@ -17,9 +19,7 @@ upstream image from a registry, acting as a pull-through cache.
## Prerequisites ## Prerequisites
To use the Dependency Proxy: The Dependency Proxy must be [enabled by an administrator](../../../administration/packages/dependency_proxy.md).
- Your group must be public. Authentication for private groups is [not supported yet](https://gitlab.com/gitlab-org/gitlab/-/issues/11582).
### Supported images and packages ### Supported images and packages
...@@ -58,6 +58,56 @@ Prerequisites: ...@@ -58,6 +58,56 @@ Prerequisites:
- Docker Hub must be available. Follow [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/241639) - 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. 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: To store a Docker image in Dependency Proxy storage:
1. Go to your group's **Packages & Registries > Dependency Proxy**. 1. Go to your group's **Packages & Registries > Dependency Proxy**.
......
...@@ -9,7 +9,7 @@ module IncidentManagement ...@@ -9,7 +9,7 @@ module IncidentManagement
end end
def execute def execute
return IncidentManagement::OncallSchedule.none unless available? && allowed? return IncidentManagement::OncallSchedule.none unless allowed?
collection = project.incident_management_oncall_schedules collection = project.incident_management_oncall_schedules
collection = by_iid(collection) collection = by_iid(collection)
...@@ -21,11 +21,6 @@ module IncidentManagement ...@@ -21,11 +21,6 @@ module IncidentManagement
attr_reader :current_user, :project, :params attr_reader :current_user, :project, :params
def available?
Feature.enabled?(:oncall_schedules_mvc, project) &&
project.feature_available?(:oncall_schedules)
end
def allowed? def allowed?
Ability.allowed?(current_user, :read_incident_management_oncall_schedule, project) Ability.allowed?(current_user, :read_incident_management_oncall_schedule, project)
end end
......
...@@ -49,6 +49,7 @@ module EE ...@@ -49,6 +49,7 @@ module EE
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Update mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Update
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Delete mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Delete
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Create mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Create
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Update
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Destroy mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Destroy
prepend(Types::DeprecatedMutations) prepend(Types::DeprecatedMutations)
......
# frozen_string_literal: true
module Mutations
module IncidentManagement
module OncallSchedule
class Update < OncallScheduleBase
graphql_name 'OncallScheduleUpdate'
argument :project_path, GraphQL::ID_TYPE,
required: true,
description: 'The project to update the on-call schedule in'
argument :iid, GraphQL::STRING_TYPE,
required: true,
description: 'The on-call schedule internal ID to update'
argument :name, GraphQL::STRING_TYPE,
required: false,
description: 'The name of the on-call schedule'
argument :description, GraphQL::STRING_TYPE,
required: false,
description: 'The description of the on-call schedule'
argument :timezone, GraphQL::STRING_TYPE,
required: false,
description: 'The timezone of the on-call schedule'
def resolve(args)
oncall_schedule = authorized_find!(project_path: args[:project_path], iid: args[:iid])
response ::IncidentManagement::OncallSchedules::UpdateService.new(
oncall_schedule,
current_user,
args.slice(:name, :description, :timezone)
).execute
end
end
end
end
end
...@@ -40,6 +40,8 @@ module EE ...@@ -40,6 +40,8 @@ module EE
has_many :project_templates, through: :projects, foreign_key: 'custom_project_templates_group_id' has_many :project_templates, through: :projects, foreign_key: 'custom_project_templates_group_id'
has_many :managed_users, class_name: 'User', foreign_key: 'managing_group_id', inverse_of: :managing_group has_many :managed_users, class_name: 'User', foreign_key: 'managing_group_id', inverse_of: :managing_group
has_many :provisioned_user_details, class_name: 'UserDetail', foreign_key: 'provisioned_by_group_id', inverse_of: :provisioned_by_group
has_many :provisioned_users, through: :provisioned_user_details, source: :user
has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::GroupStage' has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::GroupStage'
has_many :value_streams, class_name: 'Analytics::CycleAnalytics::GroupValueStream' has_many :value_streams, class_name: 'Analytics::CycleAnalytics::GroupValueStream'
......
...@@ -33,6 +33,9 @@ module EE ...@@ -33,6 +33,9 @@ module EE
delegate :shared_runners_minutes_limit, :shared_runners_minutes_limit=, delegate :shared_runners_minutes_limit, :shared_runners_minutes_limit=,
:extra_shared_runners_minutes_limit, :extra_shared_runners_minutes_limit=, :extra_shared_runners_minutes_limit, :extra_shared_runners_minutes_limit=,
to: :namespace to: :namespace
delegate :provisioned_by_group, :provisioned_by_group=,
:provisioned_by_group_id, :provisioned_by_group_id=,
to: :user_detail, allow_nil: true
has_many :epics, foreign_key: :author_id has_many :epics, foreign_key: :author_id
has_many :requirements, foreign_key: :author_id, inverse_of: :author, class_name: 'RequirementsManagement::Requirement' has_many :requirements, foreign_key: :author_id, inverse_of: :author, class_name: 'RequirementsManagement::Requirement'
......
# frozen_string_literal: true
module EE
module UserDetail
extend ActiveSupport::Concern
prepended do
belongs_to :provisioned_by_group, class_name: 'Group', optional: true, inverse_of: :provisioned_user_details
end
end
end
...@@ -155,8 +155,7 @@ module EE ...@@ -155,8 +155,7 @@ module EE
with_scope :subject with_scope :subject
condition(:oncall_schedules_available) do condition(:oncall_schedules_available) do
::Feature.enabled?(:oncall_schedules_mvc, @subject) && ::Gitlab::IncidentManagement.oncall_schedules_available?(@subject)
@subject.feature_available?(:oncall_schedules)
end end
rule { visual_review_bot }.policy do rule { visual_review_bot }.policy do
......
...@@ -62,6 +62,8 @@ module EE ...@@ -62,6 +62,8 @@ module EE
build_scim_identity(user) build_scim_identity(user)
identity_params[:provider] = GROUP_SAML_PROVIDER identity_params[:provider] = GROUP_SAML_PROVIDER
user.provisioned_by_group_id = params[:group_id]
super super
end end
......
...@@ -31,8 +31,7 @@ module IncidentManagement ...@@ -31,8 +31,7 @@ module IncidentManagement
end end
def available? def available?
Feature.enabled?(:oncall_schedules_mvc, project) && ::Gitlab::IncidentManagement.oncall_schedules_available?(project)
project.feature_available?(:oncall_schedules)
end end
def error(message) def error(message)
......
...@@ -31,8 +31,7 @@ module IncidentManagement ...@@ -31,8 +31,7 @@ module IncidentManagement
end end
def available? def available?
Feature.enabled?(:oncall_schedules_mvc, project) && ::Gitlab::IncidentManagement.oncall_schedules_available?(project)
project.feature_available?(:oncall_schedules)
end end
def error(message) def error(message)
......
# frozen_string_literal: true
module IncidentManagement
module OncallSchedules
class UpdateService
# @param oncall_schedule [IncidentManagement::OncallSchedule]
# @param user [User]
# @param params [Hash]
def initialize(oncall_schedule, user, params)
@oncall_schedule = oncall_schedule
@user = user
@params = params
@project = oncall_schedule.project
end
def execute
return error_no_license unless available?
return error_no_permissions unless allowed?
if oncall_schedule.update(params)
success(oncall_schedule)
else
error(oncall_schedule.errors.full_messages.to_sentence)
end
end
private
attr_reader :oncall_schedule, :user, :params, :project
def allowed?
user&.can?(:admin_incident_management_oncall_schedule, project)
end
def available?
::Gitlab::IncidentManagement.oncall_schedules_available?(project)
end
def error(message)
ServiceResponse.error(message: message)
end
def success(oncall_schedule)
ServiceResponse.success(payload: { oncall_schedule: oncall_schedule })
end
def error_no_permissions
error(_('You have insufficient permissions to update an on-call schedule for this project'))
end
def error_no_license
error(_('Your license does not support on-call schedules'))
end
end
end
end
# frozen_string_literal: true
module Gitlab
module IncidentManagement
def self.oncall_schedules_available?(project)
::Feature.enabled?(:oncall_schedules_mvc, project) &&
project.feature_available?(:oncall_schedules)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::IncidentManagement::OncallSchedule::Update do
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:oncall_schedule) { create(:incident_management_oncall_schedule, project: project) }
let(:args) do
{
project_path: project.full_path,
iid: oncall_schedule.iid.to_s,
name: 'Updated name',
description: 'Updated description',
timezone: 'America/New_York'
}
end
specify { expect(described_class).to require_graphql_authorizations(:admin_incident_management_oncall_schedule) }
before do
stub_licensed_features(oncall_schedules: true)
end
describe '#resolve' do
subject(:resolve) { mutation_for(project, current_user).resolve(args) }
context 'user has access to project' do
before do
project.add_maintainer(current_user)
end
context 'when OncallSchedules::UpdateService responds with success' do
it 'returns the on-call schedule with no errors' do
expect(resolve).to eq(
oncall_schedule: oncall_schedule,
errors: []
)
end
end
context 'when OncallSchedules::UpdateService responds with an error' do
before do
allow_any_instance_of(::IncidentManagement::OncallSchedules::UpdateService)
.to receive(:execute)
.and_return(ServiceResponse.error(payload: { oncall_schedule: nil }, message: 'Name has already been taken'))
end
it 'returns errors' do
expect(resolve).to eq(
oncall_schedule: nil,
errors: ['Name has already been taken']
)
end
end
end
context 'when resource is not accessible to the user' do
it 'raises an error' do
expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
private
def mutation_for(project, user)
described_class.new(object: project, context: { current_user: user }, field: nil)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::IncidentManagement do
let_it_be_with_refind(:project) { create(:project) }
describe '.oncall_schedules_available?' do
subject { described_class.oncall_schedules_available?(project) }
before do
stub_licensed_features(oncall_schedules: true)
stub_feature_flags(oncall_schedules_mvc: project)
end
it { is_expected.to be_truthy }
context 'when feature flag is disabled' do
before do
stub_feature_flags(oncall_schedules_mvc: false)
end
it { is_expected.to be_falsey }
end
context 'when there is no license' do
before do
stub_licensed_features(oncall_schedules: false)
end
it { is_expected.to be_falsey }
end
end
end
...@@ -24,6 +24,8 @@ RSpec.describe Group do ...@@ -24,6 +24,8 @@ RSpec.describe Group do
it { is_expected.to have_many(:saml_group_links) } it { is_expected.to have_many(:saml_group_links) }
it { is_expected.to have_many(:epics) } it { is_expected.to have_many(:epics) }
it { is_expected.to have_many(:epic_boards).inverse_of(:group) } it { is_expected.to have_many(:epic_boards).inverse_of(:group) }
it { is_expected.to have_many(:provisioned_user_details).inverse_of(:provisioned_by_group) }
it { is_expected.to have_many(:provisioned_users) }
it_behaves_like 'model with wiki' do it_behaves_like 'model with wiki' do
let(:container) { create(:group, :nested, :wiki_repo) } let(:container) { create(:group, :nested, :wiki_repo) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe UserDetail do
it { is_expected.to belong_to(:provisioned_by_group) }
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Updating an on-call schedule' do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:oncall_schedule) { create(:incident_management_oncall_schedule, project: project) }
let(:variables) do
{
project_path: project.full_path,
iid: oncall_schedule.iid.to_s,
name: 'Updated name',
description: 'Updated description',
timezone: 'America/New_York'
}
end
let(:mutation) do
graphql_mutation(:oncall_schedule_update, variables) do
<<~QL
clientMutationId
errors
oncallSchedule {
iid
name
description
timezone
}
QL
end
end
let(:mutation_response) { graphql_mutation_response(:oncall_schedule_update) }
before do
stub_licensed_features(oncall_schedules: true)
project.add_maintainer(user)
end
it 'updates the on-call schedule' do
post_graphql_mutation(mutation, current_user: user)
oncall_schedule_response = mutation_response['oncallSchedule']
expect(response).to have_gitlab_http_status(:success)
expect(oncall_schedule_response.slice(*%w[iid name description timezone])).to eq(
'iid' => oncall_schedule.iid.to_s,
'name' => variables[:name],
'description' => variables[:description],
'timezone' => variables[:timezone]
)
end
end
...@@ -208,12 +208,16 @@ RSpec.describe API::Scim do ...@@ -208,12 +208,16 @@ RSpec.describe API::Scim do
it 'created the identity' do it 'created the identity' do
expect(group.scim_identities.with_extern_uid('test_uid').first).not_to be_nil expect(group.scim_identities.with_extern_uid('test_uid').first).not_to be_nil
end end
it 'marks the user as provisioned by the group' do
expect(new_user.provisioned_by_group).to eq(group)
end
end end
context 'existing user' do context 'existing user' do
before do let(:old_user) { create(:user, email: 'work@example.com') }
old_user = create(:user, email: 'work@example.com')
before do
create(:scim_identity, user: old_user, group: group, extern_uid: 'test_uid') create(:scim_identity, user: old_user, group: group, extern_uid: 'test_uid')
group.add_guest(old_user) group.add_guest(old_user)
...@@ -227,6 +231,10 @@ RSpec.describe API::Scim do ...@@ -227,6 +231,10 @@ RSpec.describe API::Scim do
it 'has the user external ID' do it 'has the user external ID' do
expect(json_response['id']).to eq('test_uid') expect(json_response['id']).to eq('test_uid')
end end
it 'does not mark the user as provisioned' do
expect(old_user.reload.provisioned_by_group).to be_nil
end
end end
it_behaves_like 'storing arguments in the application context' do it_behaves_like 'storing arguments in the application context' do
......
...@@ -30,19 +30,26 @@ RSpec.describe Users::BuildService do ...@@ -30,19 +30,26 @@ RSpec.describe Users::BuildService do
end end
context 'with scim identity' do context 'with scim identity' do
let_it_be(:group) { create(:group) }
let_it_be(:scim_identity_params) { { extern_uid: 'uid', provider: 'group_scim', group_id: group.id } }
before do before do
params.merge!(scim_identity_params) params.merge!(scim_identity_params)
end end
let_it_be(:scim_identity_params) { { extern_uid: 'uid', provider: 'group_scim', group_id: 1 } }
it 'passes allowed attributes to both scim and saml identity' do it 'passes allowed attributes to both scim and saml identity' do
scim_identity_params.delete(:provider) scim_params = scim_identity_params.dup
scim_params.delete(:provider)
expect(ScimIdentity).to receive(:new).with(hash_including(scim_identity_params)).and_call_original expect(ScimIdentity).to receive(:new).with(hash_including(scim_params)).and_call_original
expect(Identity).to receive(:new).with(hash_including(identity_params)).and_call_original expect(Identity).to receive(:new).with(hash_including(identity_params)).and_call_original
service.execute service.execute
end end
it 'marks the user as provisioned by group' do
expect(service.execute.provisioned_by_group_id).to eq(group.id)
end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::OncallSchedules::UpdateService do
let_it_be(:user_with_permissions) { create(:user) }
let_it_be(:user_without_permissions) { create(:user) }
let_it_be_with_refind(:project) { create(:project) }
let_it_be_with_reload(:oncall_schedule) { create(:incident_management_oncall_schedule, project: project) }
let(:current_user) { user_with_permissions }
let(:params) { { name: 'Updated name', description: 'Updated description', timezone: 'America/New_York' } }
let(:service) { described_class.new(oncall_schedule, current_user, params) }
before do
stub_licensed_features(oncall_schedules: true)
project.add_maintainer(user_with_permissions)
end
describe '#execute' do
shared_examples 'error response' do |message|
it 'has an informative message' do
expect(execute).to be_error
expect(execute.message).to eq(message)
end
end
subject(:execute) { service.execute }
context 'when the current_user is anonymous' do
let(:current_user) { nil }
it_behaves_like 'error response', 'You have insufficient permissions to update an on-call schedule for this project'
end
context 'when the current_user does not have permissions to update on-call schedules' do
let(:current_user) { user_without_permissions }
it_behaves_like 'error response', 'You have insufficient permissions to update an on-call schedule for this project'
end
context 'when feature is not available' do
before do
stub_licensed_features(oncall_schedules: false)
end
it_behaves_like 'error response', 'Your license does not support on-call schedules'
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(oncall_schedules_mvc: false)
end
it_behaves_like 'error response', 'Your license does not support on-call schedules'
end
context 'when an on-call schedule witht the same name already exists' do
before do
create(:incident_management_oncall_schedule, project: project, name: params[:name])
end
it_behaves_like 'error response', 'Name has already been taken'
end
context 'with valid params' do
it 'successfully creates an on-call schedule' do
response = execute
payload = response.payload
oncall_schedule.reload
expect(response).to be_success
expect(payload[:oncall_schedule]).to eq(oncall_schedule)
expect(oncall_schedule).to be_a(::IncidentManagement::OncallSchedule)
expect(oncall_schedule.name).to eq(params[:name])
expect(oncall_schedule.description).to eq(params[:description])
expect(oncall_schedule.timezone).to eq(params[:timezone])
end
end
end
end
...@@ -25646,9 +25646,6 @@ msgstr "" ...@@ -25646,9 +25646,6 @@ msgstr ""
msgid "Something went wrong, unable to search projects" msgid "Something went wrong, unable to search projects"
msgstr "" msgstr ""
msgid "Something went wrong."
msgstr ""
msgid "Something went wrong. Please try again." msgid "Something went wrong. Please try again."
msgstr "" msgstr ""
...@@ -31432,6 +31429,9 @@ msgstr "" ...@@ -31432,6 +31429,9 @@ msgstr ""
msgid "You have insufficient permissions to remove this HTTP integration" msgid "You have insufficient permissions to remove this HTTP integration"
msgstr "" 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" msgid "You have insufficient permissions to update this HTTP integration"
msgstr "" msgstr ""
......
# 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
...@@ -3,8 +3,77 @@ ...@@ -3,8 +3,77 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Groups::DependencyProxyForContainersController do RSpec.describe Groups::DependencyProxyForContainersController do
include HttpBasicAuthHelpers
include DependencyProxyHelpers
let_it_be(:user) { create(:user) }
let(:group) { create(:group) } let(:group) { create(:group) }
let(:token_response) { { status: :success, token: 'abcd1234' } } 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 shared_examples 'not found when disabled' do
context 'feature disabled' do context 'feature disabled' do
...@@ -27,6 +96,8 @@ RSpec.describe Groups::DependencyProxyForContainersController do ...@@ -27,6 +96,8 @@ RSpec.describe Groups::DependencyProxyForContainersController do
allow_next_instance_of(DependencyProxy::RequestTokenService) do |instance| allow_next_instance_of(DependencyProxy::RequestTokenService) do |instance|
allow(instance).to receive(:execute).and_return(token_response) allow(instance).to receive(:execute).and_return(token_response)
end end
request.headers['HTTP_AUTHORIZATION'] = token_header
end end
describe 'GET #manifest' do describe 'GET #manifest' do
...@@ -46,6 +117,10 @@ RSpec.describe Groups::DependencyProxyForContainersController do ...@@ -46,6 +117,10 @@ RSpec.describe Groups::DependencyProxyForContainersController do
enable_dependency_proxy enable_dependency_proxy
end 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 context 'remote token request fails' do
let(:token_response) do let(:token_response) do
{ {
...@@ -113,6 +188,10 @@ RSpec.describe Groups::DependencyProxyForContainersController do ...@@ -113,6 +188,10 @@ RSpec.describe Groups::DependencyProxyForContainersController do
enable_dependency_proxy enable_dependency_proxy
end 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 context 'remote blob request fails' do
let(:blob_response) do let(:blob_response) do
{ {
......
...@@ -79,13 +79,19 @@ RSpec.describe 'Group Dependency Proxy' do ...@@ -79,13 +79,19 @@ RSpec.describe 'Group Dependency Proxy' do
sign_in(developer) sign_in(developer)
end end
context 'group is private' do context 'feature flag is disabled' do
let(:group) { create(:group, :private) } before do
stub_feature_flags(dependency_proxy_for_private_groups: false)
end
it 'informs user that feature is only available for public groups' do context 'group is private' do
visit path 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
end end
......
...@@ -7,44 +7,6 @@ RSpec.describe 'Issuables Close/Reopen/Report toggle' do ...@@ -7,44 +7,6 @@ RSpec.describe 'Issuables Close/Reopen/Report toggle' do
let(:user) { create(:user) } 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 context 'on a merge request' do
let(:container) { find('.detail-page-header-actions') } let(:container) { find('.detail-page-header-actions') }
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
...@@ -60,7 +22,22 @@ RSpec.describe 'Issuables Close/Reopen/Report toggle' do ...@@ -60,7 +22,22 @@ RSpec.describe 'Issuables Close/Reopen/Report toggle' do
visit project_merge_request_path(project, issuable) visit project_merge_request_path(project, issuable)
end 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 context 'when the merge request is open' do
let(:issuable) { create(:merge_request, :opened, source_project: project) } let(:issuable) { create(:merge_request, :opened, source_project: project) }
......
...@@ -15,7 +15,7 @@ RSpec.describe 'User reopens a merge requests', :js do ...@@ -15,7 +15,7 @@ RSpec.describe 'User reopens a merge requests', :js do
end end
it 'reopens a merge request' do 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) click_link('Reopen merge request', match: :first)
......
...@@ -34,8 +34,17 @@ describe('Multiline comment utilities', () => { ...@@ -34,8 +34,17 @@ describe('Multiline comment utilities', () => {
expect(getSymbol(type)).toEqual(result); expect(getSymbol(type)).toEqual(result);
}); });
}); });
describe('getCommentedLines', () => { const inlineDiffLines = [{ line_code: '1' }, { line_code: '2' }, { line_code: '3' }];
const diffLines = [{ 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', () => { it('returns a default object when `selectedCommentPosition` is not provided', () => {
expect(getCommentedLines(undefined, diffLines)).toEqual({ startLine: 4, endLine: 4 }); expect(getCommentedLines(undefined, diffLines)).toEqual({ startLine: 4, endLine: 4 });
}); });
......
...@@ -6,26 +6,6 @@ RSpec.describe MergeRequestsHelper do ...@@ -6,26 +6,6 @@ RSpec.describe MergeRequestsHelper do
include ActionView::Helpers::UrlHelper include ActionView::Helpers::UrlHelper
include ProjectForksHelper 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 describe '#state_name_with_icon' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
......
...@@ -54,4 +54,11 @@ RSpec.describe DependencyProxy::Registry, type: :model do ...@@ -54,4 +54,11 @@ RSpec.describe DependencyProxy::Registry, type: :model do
end end
end 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 end
...@@ -5,7 +5,13 @@ require 'spec_helper' ...@@ -5,7 +5,13 @@ require 'spec_helper'
RSpec.describe NamespaceOnboardingAction do RSpec.describe NamespaceOnboardingAction do
let(:namespace) { build(:namespace) } 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 describe '.completed?' do
let(:action) { :subscription_created } let(:action) { :subscription_created }
......
This diff is collapsed.
...@@ -81,6 +81,10 @@ RSpec.describe "Groups", "routing" do ...@@ -81,6 +81,10 @@ RSpec.describe "Groups", "routing" do
end end
describe 'dependency proxy for containers' do 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 context 'image name without namespace' do
it 'routes to #manifest' do it 'routes to #manifest' do
expect(get('/v2/gitlabhq/dependency_proxy/containers/ruby/manifests/2.3.6')) expect(get('/v2/gitlabhq/dependency_proxy/containers/ruby/manifests/2.3.6'))
......
# 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
# 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
...@@ -45,6 +45,12 @@ RSpec.describe PostReceiveService do ...@@ -45,6 +45,12 @@ RSpec.describe PostReceiveService do
it 'does not return error' do it 'does not return error' do
expect(subject).to be_empty expect(subject).to be_empty
end end
it 'does not record a namespace onboarding progress action' do
expect(NamespaceOnboardingAction).not_to receive(:create_action)
subject
end
end end
context 'when repository is nil' do context 'when repository is nil' do
...@@ -80,6 +86,13 @@ RSpec.describe PostReceiveService do ...@@ -80,6 +86,13 @@ RSpec.describe PostReceiveService do
expect(response.reference_counter_decreased).to be(true) expect(response.reference_counter_decreased).to be(true)
end end
it 'records a namespace onboarding progress action' do
expect(NamespaceOnboardingAction).to receive(:create_action)
.with(project.namespace, :git_write)
subject
end
end end
context 'with Project' do context 'with Project' do
......
...@@ -25,6 +25,13 @@ module DependencyProxyHelpers ...@@ -25,6 +25,13 @@ module DependencyProxyHelpers
.to_return(status: status, body: body) .to_return(status: status, body: body)
end 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 private
def registry def registry
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment