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() {
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');
};
......
......@@ -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 };
}
......@@ -41,12 +41,6 @@
@include media-breakpoint-down(xs) {
width: 100%;
margin-top: 10px;
> .issue-btn-group {
> .btn {
width: 100%;
}
}
}
}
......
......@@ -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
......
# 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 @@
module Groups
class DependencyProxiesController < Groups::ApplicationController
include DependencyProxyAccess
include DependencyProxy::GroupAccess
before_action :authorize_admin_dependency_proxy!, only: :update
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
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
......
......@@ -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
......
......@@ -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')
......
......@@ -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
......
......@@ -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')
# 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)
......
# 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
}.freeze
enum action: ACTIONS
......
......@@ -31,3 +31,5 @@ class UserDetail < ApplicationRecord
self.bio = '' if bio_changed? && bio.nil?
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
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')
......@@ -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
......
- 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'}"
- 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.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?
.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
= 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
%span.sr-only= _('Toggle dropdown')
%span.gl-sr-only= _('Toggle dropdown')
= sprite_icon "angle-down", size: 12
%ul.js-issuable-close-menu.dropdown-menu.dropdown-menu-right
- if issuable.open?
%ul.dropdown-menu.dropdown-menu-right
- if @merge_request.open?
%li
= link_to close_issuable_path(issuable), method: :put do
= link_to close_issuable_path(@merge_request), method: :put do
.description
%strong.title
= _('Close')
= display_issuable_type
- unless issuable_author_is_current_user(issuable)
- unless issuable.closed?
- unless issuable_author_is_current_user(@merge_request)
- unless @merge_request.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)) }
%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
%p.text.gl-mb-0
= _('Report %{display_issuable_type} that are abusive, inappropriate or spam.') % { display_issuable_type: display_issuable_type.pluralize }
......@@ -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')
---
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
milestone: '13.6'
type: development
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
# 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
......
# 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 (
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))
);
......@@ -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 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 +23070,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;
......
......@@ -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
......@@ -14838,6 +14839,61 @@ type OncallScheduleDestroyPayload {
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
"""
......
......@@ -41158,6 +41158,33 @@
"isDeprecated": false,
"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,
......@@ -44104,6 +44131,152 @@
"enumValues": 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",
"name": "Package",
......@@ -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.
......
......@@ -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
......
......@@ -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**.
......
......@@ -9,7 +9,7 @@ module IncidentManagement
end
def execute
return IncidentManagement::OncallSchedule.none unless available? && allowed?
return IncidentManagement::OncallSchedule.none unless allowed?
collection = project.incident_management_oncall_schedules
collection = by_iid(collection)
......@@ -21,11 +21,6 @@ module IncidentManagement
attr_reader :current_user, :project, :params
def available?
Feature.enabled?(:oncall_schedules_mvc, project) &&
project.feature_available?(:oncall_schedules)
end
def allowed?
Ability.allowed?(current_user, :read_incident_management_oncall_schedule, project)
end
......
......@@ -49,6 +49,7 @@ module EE
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Update
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Delete
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Create
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Update
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Destroy
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
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 :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 :value_streams, class_name: 'Analytics::CycleAnalytics::GroupValueStream'
......
......@@ -33,6 +33,9 @@ module EE
delegate :shared_runners_minutes_limit, :shared_runners_minutes_limit=,
:extra_shared_runners_minutes_limit, :extra_shared_runners_minutes_limit=,
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 :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
with_scope :subject
condition(:oncall_schedules_available) do
::Feature.enabled?(:oncall_schedules_mvc, @subject) &&
@subject.feature_available?(:oncall_schedules)
::Gitlab::IncidentManagement.oncall_schedules_available?(@subject)
end
rule { visual_review_bot }.policy do
......
......@@ -62,6 +62,8 @@ module EE
build_scim_identity(user)
identity_params[:provider] = GROUP_SAML_PROVIDER
user.provisioned_by_group_id = params[:group_id]
super
end
......
......@@ -31,8 +31,7 @@ module IncidentManagement
end
def available?
Feature.enabled?(:oncall_schedules_mvc, project) &&
project.feature_available?(:oncall_schedules)
::Gitlab::IncidentManagement.oncall_schedules_available?(project)
end
def error(message)
......
......@@ -31,8 +31,7 @@ module IncidentManagement
end
def available?
Feature.enabled?(:oncall_schedules_mvc, project) &&
project.feature_available?(:oncall_schedules)
::Gitlab::IncidentManagement.oncall_schedules_available?(project)
end
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
it { is_expected.to have_many(:saml_group_links) }
it { is_expected.to have_many(:epics) }
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
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
it 'created the identity' do
expect(group.scim_identities.with_extern_uid('test_uid').first).not_to be_nil
end
it 'marks the user as provisioned by the group' do
expect(new_user.provisioned_by_group).to eq(group)
end
end
context 'existing user' do
before do
old_user = create(:user, email: 'work@example.com')
let(:old_user) { create(:user, email: 'work@example.com') }
before do
create(:scim_identity, user: old_user, group: group, extern_uid: 'test_uid')
group.add_guest(old_user)
......@@ -227,6 +231,10 @@ RSpec.describe API::Scim do
it 'has the user external ID' do
expect(json_response['id']).to eq('test_uid')
end
it 'does not mark the user as provisioned' do
expect(old_user.reload.provisioned_by_group).to be_nil
end
end
it_behaves_like 'storing arguments in the application context' do
......
......@@ -30,19 +30,26 @@ RSpec.describe Users::BuildService do
end
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
params.merge!(scim_identity_params)
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
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
service.execute
end
it 'marks the user as provisioned by group' do
expect(service.execute.provisioned_by_group_id).to eq(group.id)
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 ""
msgid "Something went wrong, unable to search projects"
msgstr ""
msgid "Something went wrong."
msgstr ""
msgid "Something went wrong. Please try again."
msgstr ""
......@@ -31432,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 ""
......
# 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 @@
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
{
......
......@@ -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
......
......@@ -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) }
......
......@@ -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)
......
......@@ -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 });
});
......
......@@ -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
......
......@@ -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
......@@ -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 }
......
This diff is collapsed.
......@@ -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'))
......
# 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
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
......
......@@ -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
......
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