Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-08-04 09:08:21 +00:00
parent b41e09c9ce
commit eada495948
56 changed files with 842 additions and 258 deletions

View File

@ -2441,31 +2441,6 @@ Gitlab/FeatureAvailableUsage:
- 'lib/api/helpers/related_resources_helpers.rb'
- 'spec/models/concerns/featurable_spec.rb'
# WIP See https://gitlab.com/gitlab-org/gitlab/-/issues/327490
Style/RegexpLiteralMixedPreserve:
Exclude:
- 'qa/qa/page/project/settings/advanced.rb'
- 'qa/spec/service/docker_run/gitlab_runner_spec.rb'
- 'rubocop/cop/gitlab/duplicate_spec_location.rb'
- 'spec/features/clusters/cluster_health_dashboard_spec.rb'
- 'spec/features/markdown/metrics_spec.rb'
- 'spec/features/search/user_searches_for_code_spec.rb'
- 'spec/features/snippets/embedded_snippet_spec.rb'
- 'spec/helpers/diff_helper_spec.rb'
- 'spec/helpers/releases_helper_spec.rb'
- 'spec/lib/gitlab/ci/reports/test_case_spec.rb'
- 'spec/lib/gitlab/consul/internal_spec.rb'
- 'spec/lib/gitlab/import_export/shared_spec.rb'
- 'spec/lib/gitlab/utils/usage_data_spec.rb'
- 'spec/presenters/ci/build_runner_presenter_spec.rb'
- 'spec/requests/api/projects_spec.rb'
- 'spec/services/jira/requests/projects/list_service_spec.rb'
- 'spec/support/capybara.rb'
- 'spec/support/helpers/grafana_api_helpers.rb'
- 'spec/support/helpers/query_recorder.rb'
- 'spec/support/helpers/require_migration.rb'
- 'spec/views/layouts/_head.html.haml_spec.rb'
# WIP see: https://gitlab.com/gitlab-org/gitlab/-/issues/335808
Database/MultipleDatabases:
Exclude:

View File

@ -223,7 +223,7 @@ gem 're2', '~> 1.2.0'
gem 'version_sorter', '~> 2.2.4'
# Export Ruby Regex to Javascript
gem 'js_regex', '~> 3.4'
gem 'js_regex', '~> 3.7'
# User agent parsing
gem 'device_detector'
@ -523,7 +523,7 @@ gem 'valid_email', '~> 0.1'
# JSON
gem 'json', '~> 2.3.0'
gem 'json_schemer', '~> 0.2.12'
gem 'json_schemer', '~> 0.2.18'
gem 'oj', '~> 3.10.6'
gem 'multi_json', '~> 1.14.1'
gem 'yajl-ruby', '~> 1.4.1', require: 'yajl'

View File

@ -179,7 +179,8 @@ GEM
mime-types (>= 1.16)
ssrf_filter (~> 1.0)
cbor (0.5.9.6)
character_set (1.4.0)
character_set (1.4.1)
sorted_set (~> 1.0)
charlock_holmes (0.7.7)
chef-config (16.10.17)
addressable
@ -307,8 +308,8 @@ GEM
dry-inflector (~> 0.1, >= 0.1.2)
dry-logic (~> 1.0, >= 1.0.2)
e2mmap (0.1.0)
ecma-re-validator (0.2.1)
regexp_parser (~> 1.2)
ecma-re-validator (0.3.0)
regexp_parser (~> 2.0)
ed25519 (1.2.4)
elasticsearch (6.8.2)
elasticsearch-api (= 6.8.2)
@ -604,7 +605,7 @@ GEM
temple (>= 0.8.2)
thor
tilt
hana (1.3.6)
hana (1.3.7)
hangouts-chat (0.0.5)
hashdiff (1.0.1)
hashie (4.1.0)
@ -652,19 +653,19 @@ GEM
multipart-post
oauth (~> 0.5, >= 0.5.0)
jmespath (1.4.0)
js_regex (3.4.0)
js_regex (3.7.0)
character_set (~> 1.4)
regexp_parser (~> 1.5)
regexp_property_values (~> 0.3)
regexp_parser (~> 2.1)
regexp_property_values (~> 1.0)
json (2.3.0)
json-jwt (1.13.0)
activesupport (>= 4.2)
aes_key_wrap
bindata
json_schemer (0.2.12)
ecma-re-validator (~> 0.2)
json_schemer (0.2.18)
ecma-re-validator (~> 0.3)
hana (~> 1.3)
regexp_parser (~> 1.5)
regexp_parser (~> 2.0)
uri_template (~> 0.7)
jsonpath (1.1.0)
multi_json
@ -1010,6 +1011,7 @@ GEM
ffi (>= 1.0.6)
msgpack (>= 0.4.3)
optimist (>= 3.0.0)
rbtree (0.4.4)
rchardet (1.8.0)
rdoc (6.3.2)
re2 (1.2.0)
@ -1035,8 +1037,8 @@ GEM
redis-store (>= 1.2, < 2)
redis-store (1.8.1)
redis (>= 4, < 5)
regexp_parser (1.8.2)
regexp_property_values (0.3.5)
regexp_parser (2.1.1)
regexp_property_values (1.0.0)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
@ -1174,6 +1176,7 @@ GEM
rubyzip (>= 1.2.2)
sentry-raven (3.1.2)
faraday (>= 1.0)
set (1.0.1)
settingslogic (2.0.9)
sexp_processor (4.15.1)
shellany (0.0.1)
@ -1218,6 +1221,9 @@ GEM
thor (~> 1.0)
tilt (~> 2.0)
yard (~> 0.9, >= 0.9.24)
sorted_set (1.0.3)
rbtree
set (~> 1.0)
spamcheck (0.1.0)
grpc (~> 1.0)
spring (2.1.1)
@ -1509,9 +1515,9 @@ DEPENDENCIES
invisible_captcha (~> 1.1.0)
ipaddress (~> 0.8.3)
jira-ruby (~> 2.1.4)
js_regex (~> 3.4)
js_regex (~> 3.7)
json (~> 2.3.0)
json_schemer (~> 0.2.12)
json_schemer (~> 0.2.18)
jwt (~> 2.1.0)
kaminari (~> 1.0)
kas-grpc (~> 0.0.2)

View File

@ -1,3 +1,3 @@
import initFilePickers from '~/file_pickers';
document.addEventListener('DOMContentLoaded', initFilePickers);
initFilePickers();

View File

@ -1,19 +1,17 @@
import initSettingsPanels from '~/settings_panels';
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
initSettingsPanels();
// Initialize expandable settings panels
initSettingsPanels();
const domainCard = document.querySelector('.js-domain-cert-show');
const domainForm = document.querySelector('.js-domain-cert-inputs');
const domainReplaceButton = document.querySelector('.js-domain-cert-replace-btn');
const domainSubmitButton = document.querySelector('.js-serverless-domain-submit');
const domainCard = document.querySelector('.js-domain-cert-show');
const domainForm = document.querySelector('.js-domain-cert-inputs');
const domainReplaceButton = document.querySelector('.js-domain-cert-replace-btn');
const domainSubmitButton = document.querySelector('.js-serverless-domain-submit');
if (domainReplaceButton && domainCard && domainForm) {
domainReplaceButton.addEventListener('click', () => {
domainCard.classList.add('hidden');
domainForm.classList.remove('hidden');
domainSubmitButton.removeAttribute('disabled');
});
}
});
if (domainReplaceButton && domainCard && domainForm) {
domainReplaceButton.addEventListener('click', () => {
domainCard.classList.add('hidden');
domainForm.classList.remove('hidden');
domainSubmitButton.removeAttribute('disabled');
});
}

View File

@ -5,6 +5,4 @@ import Translate from '~/vue_shared/translate';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => {
mountBadgeSettings(GROUP_BADGE);
});
mountBadgeSettings(GROUP_BADGE);

View File

@ -1,7 +1,5 @@
import mountImportProjectsTable from '~/import_entities/import_projects';
document.addEventListener('DOMContentLoaded', () => {
const mountElement = document.getElementById('import-projects-mount-element');
const mountElement = document.getElementById('import-projects-mount-element');
mountImportProjectsTable(mountElement);
});
mountImportProjectsTable(mountElement);

View File

@ -5,9 +5,7 @@ import initCompareSelector from '~/projects/compare';
initCompareSelector();
document.addEventListener('DOMContentLoaded', () => {
new Diff(); // eslint-disable-line no-new
const paddingTop = 16;
initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop);
GpgBadges.fetch();
});
new Diff(); // eslint-disable-line no-new
const paddingTop = 16;
initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop);
GpgBadges.fetch();

View File

@ -1,3 +1,3 @@
import initCycleAnalytics from '~/cycle_analytics';
document.addEventListener('DOMContentLoaded', initCycleAnalytics);
initCycleAnalytics();

View File

@ -1,3 +1,3 @@
import initContributorsGraphs from '~/contributors';
document.addEventListener('DOMContentLoaded', initContributorsGraphs);
initContributorsGraphs();

View File

@ -1,3 +1,3 @@
import mountJiraImportApp from '~/jira_import';
document.addEventListener('DOMContentLoaded', mountJiraImportApp);
mountJiraImportApp();

View File

@ -1,3 +1,3 @@
import initForm from 'ee_else_ce/pages/projects/issues/form';
document.addEventListener('DOMContentLoaded', initForm);
initForm();

View File

@ -1,3 +1,3 @@
import initTerminal from '~/terminal/';
document.addEventListener('DOMContentLoaded', initTerminal);
initTerminal();

View File

@ -1,3 +1,3 @@
import initForm from '~/pages/projects/pages_domains/form';
document.addEventListener('DOMContentLoaded', initForm);
initForm();

View File

@ -1,3 +1,3 @@
import initForm from '~/pages/projects/pages_domains/form';
document.addEventListener('DOMContentLoaded', initForm);
initForm();

View File

@ -1,7 +1,7 @@
import Vue from 'vue';
import PipelineSchedulesCallout from '../shared/components/pipeline_schedules_callout.vue';
document.addEventListener('DOMContentLoaded', () => {
function initPipelineSchedules() {
const el = document.getElementById('pipeline-schedules-callout');
if (!el) {
@ -21,4 +21,6 @@ document.addEventListener('DOMContentLoaded', () => {
return createElement(PipelineSchedulesCallout);
},
});
});
}
initPipelineSchedules();

View File

@ -1,43 +0,0 @@
# 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: true)
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

View File

@ -12,15 +12,15 @@ module DependencyProxy
private
def verify_dependency_proxy_enabled!
render_404 unless group.dependency_proxy_feature_available?
render_404 unless group&.dependency_proxy_feature_available?
end
def authorize_read_dependency_proxy!
access_denied! unless can?(current_user, :read_dependency_proxy, group)
access_denied! unless can?(auth_user, :read_dependency_proxy, group)
end
def authorize_admin_dependency_proxy!
access_denied! unless can?(current_user, :admin_dependency_proxy, group)
access_denied! unless can?(auth_user, :admin_dependency_proxy, group)
end
end
end

View File

@ -2,7 +2,7 @@
module Groups
class DependencyProxiesController < Groups::ApplicationController
include DependencyProxy::GroupAccess
include ::DependencyProxy::GroupAccess
before_action :authorize_admin_dependency_proxy!, only: :update
before_action :dependency_proxy

View File

@ -0,0 +1,76 @@
# frozen_string_literal: true
module Groups
module DependencyProxy
class ApplicationController < ::ApplicationController
EMPTY_AUTH_RESULT = Gitlab::Auth::Result.new(nil, nil, nil, nil).freeze
delegate :actor, to: :@authentication_result, allow_nil: true
# This allows auth_user to be set in the base ApplicationController
alias_method :authenticated_user, :actor
# We disable `authenticate_user!` since the `DependencyProxy::ApplicationController` performs auth using JWT token
skip_before_action :authenticate_user!, raise: false
prepend_before_action :authenticate_user_from_jwt_token!
def authenticate_user_from_jwt_token!
return unless dependency_proxy_for_private_groups?
if Feature.enabled?(:dependency_proxy_deploy_tokens)
authenticate_with_http_token do |token, _|
@authentication_result = EMPTY_AUTH_RESULT
found_user = user_from_token(token)
sign_in(found_user) if found_user.is_a?(User)
end
request_bearer_token! unless authenticated_user
else
authenticate_with_http_token do |token, _|
user = user_from_token(token)
sign_in(user) if user
end
request_bearer_token! unless current_user
end
end
private
def dependency_proxy_for_private_groups?
Feature.enabled?(:dependency_proxy_for_private_groups, default_enabled: true)
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)
return User.find(token_payload['user_id']) unless Feature.enabled?(:dependency_proxy_deploy_tokens)
if token_payload['user_id']
token_user = User.find(token_payload['user_id'])
return unless token_user
@authentication_result = Gitlab::Auth::Result.new(token_user, nil, :user, [])
return token_user
elsif token_payload['deploy_token']
deploy_token = DeployToken.active.find_by_token(token_payload['deploy_token'])
return unless deploy_token
@authentication_result = Gitlab::Auth::Result.new(deploy_token, nil, :deploy_token, [])
return deploy_token
end
nil
rescue JWT::DecodeError, JWT::ExpiredSignature, JWT::ImmatureSignature
nil
end
end
end
end

View File

@ -1,8 +1,6 @@
# frozen_string_literal: true
class Groups::DependencyProxyAuthController < ApplicationController
include DependencyProxy::Auth
class Groups::DependencyProxyAuthController < ::Groups::DependencyProxy::ApplicationController
feature_category :dependency_proxy
def authenticate

View File

@ -1,11 +1,12 @@
# frozen_string_literal: true
class Groups::DependencyProxyForContainersController < Groups::ApplicationController
include DependencyProxy::Auth
class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy::ApplicationController
include Gitlab::Utils::StrongMemoize
include DependencyProxy::GroupAccess
include SendFileUpload
include ::PackagesHelper # for event tracking
before_action :ensure_group
before_action :ensure_token_granted!
before_action :ensure_feature_enabled!
@ -24,7 +25,7 @@ class Groups::DependencyProxyForContainersController < Groups::ApplicationContro
content_type = result[:manifest].content_type
event_name = tracking_event_name(object_type: :manifest, from_cache: result[:from_cache])
track_package_event(event_name, :dependency_proxy, namespace: group, user: current_user)
track_package_event(event_name, :dependency_proxy, namespace: group, user: auth_user)
send_upload(
result[:manifest].file,
proxy: true,
@ -42,7 +43,7 @@ class Groups::DependencyProxyForContainersController < Groups::ApplicationContro
if result[:status] == :success
event_name = tracking_event_name(object_type: :blob, from_cache: result[:from_cache])
track_package_event(event_name, :dependency_proxy, namespace: group, user: current_user)
track_package_event(event_name, :dependency_proxy, namespace: group, user: auth_user)
send_upload(result[:blob].file)
else
head result[:http_status]
@ -51,6 +52,12 @@ class Groups::DependencyProxyForContainersController < Groups::ApplicationContro
private
def group
strong_memoize(:group) do
Group.find_by_full_path(params[:group_id], follow_redirects: request.get?)
end
end
def image
params[:image]
end
@ -71,6 +78,10 @@ class Groups::DependencyProxyForContainersController < Groups::ApplicationContro
group.dependency_proxy_setting || group.create_dependency_proxy_setting
end
def ensure_group
render_404 unless group
end
def ensure_feature_enabled!
render_404 unless dependency_proxy.enabled
end

View File

@ -128,7 +128,6 @@ module Ci
end
scope :eager_load_job_artifacts, -> { includes(:job_artifacts) }
scope :eager_load_job_artifacts_archive, -> { includes(:job_artifacts_archive) }
scope :eager_load_tags, -> { includes(:tags) }
scope :eager_load_everything, -> do

View File

@ -10,6 +10,7 @@ class DeployToken < ApplicationRecord
AVAILABLE_SCOPES = %i(read_repository read_registry write_registry
read_package_registry write_package_registry).freeze
GITLAB_DEPLOY_TOKEN_NAME = 'gitlab-deploy-token'
REQUIRED_DEPENDENCY_PROXY_SCOPES = %i[read_registry write_registry].freeze
default_value_for(:expires_at) { Forever.date }
@ -46,6 +47,12 @@ class DeployToken < ApplicationRecord
active.find_by(name: GITLAB_DEPLOY_TOKEN_NAME)
end
def valid_for_dependency_proxy?
group_type? &&
active? &&
REQUIRED_DEPENDENCY_PROXY_SCOPES.all? { |scope| scope.in?(scopes) }
end
def revoke!
update!(revoked: true)
end
@ -73,6 +80,14 @@ class DeployToken < ApplicationRecord
holder.has_access_to?(requested_project)
end
def has_access_to_group?(requested_group)
return false unless active?
return false unless group_type?
return false unless holder
holder.has_access_to_group?(requested_group)
end
# This is temporal. Currently we limit DeployToken
# to a single project or group, later we're going to
# extend that to be for multiple projects and namespaces.

View File

@ -11,9 +11,14 @@ class GroupDeployToken < ApplicationRecord
def has_access_to?(requested_project)
requested_project_group = requested_project&.group
return false unless requested_project_group
return true if requested_project_group.id == group_id
requested_project_group
has_access_to_group?(requested_project_group)
end
def has_access_to_group?(requested_group)
return true if requested_group.id == group_id
requested_group
.ancestors
.where(id: group_id)
.exists?

View File

@ -9,10 +9,6 @@ class Label < ApplicationRecord
include Sortable
include FromUnion
include Presentable
include IgnorableColumns
# TODO: Project#create_labels can remove column exception when this column is dropped from all envs
ignore_column :remove_on_close, remove_with: '14.1', remove_after: '2021-06-22'
cache_markdown_field :description, pipeline: :single_line

View File

@ -1427,8 +1427,7 @@ class Project < ApplicationRecord
# rubocop: disable CodeReuse/ServiceClass
def create_labels
Label.templates.each do |label|
# TODO: remove_on_close exception can be removed after the column is dropped from all envs
params = label.attributes.except('id', 'template', 'created_at', 'updated_at', 'type', 'remove_on_close')
params = label.attributes.except('id', 'template', 'created_at', 'updated_at', 'type')
Labels::FindOrCreateService.new(nil, self, params).execute(skip_authorization: true)
end
end

View File

@ -20,7 +20,6 @@ module Terraform
foreign_key: :terraform_state_id,
inverse_of: :terraform_state
scope :versioning_not_enabled, -> { where(versioning_enabled: false) }
scope :ordered_by_name, -> { order(:name) }
scope :with_name, -> (name) { where(name: name) }

View File

@ -50,6 +50,14 @@ class GroupPolicy < BasePolicy
@subject.dependency_proxy_feature_available?
end
condition(:dependency_proxy_access_allowed) do
if Feature.enabled?(:dependency_proxy_for_private_groups, default_enabled: true)
access_level >= GroupMember::REPORTER || valid_dependency_proxy_deploy_token
else
can?(:read_group)
end
end
desc "Deploy token with read_package_registry scope"
condition(:read_package_registry_deploy_token) do
@user.is_a?(DeployToken) && @user.groups.include?(@subject) && @user.read_package_registry
@ -212,7 +220,7 @@ class GroupPolicy < BasePolicy
enable :read_group
end
rule { can?(:read_group) & dependency_proxy_available }
rule { dependency_proxy_access_allowed & dependency_proxy_available }
.enable :read_dependency_proxy
rule { developer & dependency_proxy_available }
@ -260,6 +268,10 @@ class GroupPolicy < BasePolicy
def resource_access_token_creation_allowed?
resource_access_token_feature_available? && group.root_ancestor.namespace_settings.resource_access_token_creation_allowed?
end
def valid_dependency_proxy_deploy_token
@user.is_a?(DeployToken) && @user&.valid_for_dependency_proxy? && @user&.has_access_to_group?(@subject)
end
end
GroupPolicy.prepend_mod_with('GroupPolicy')

View File

@ -8,10 +8,7 @@ module Auth
def execute(authentication_abilities:)
return error('dependency proxy not enabled', 404) unless ::Gitlab.config.dependency_proxy.enabled
# Because app/controllers/concerns/dependency_proxy/auth.rb consumes this
# JWT only as `User.find`, we currently only allow User (not DeployToken, etc)
return error('access forbidden', 403) unless current_user
return error('access forbidden', 403) unless valid_user_actor?
{ token: authorized_token.encoded }
end
@ -36,11 +33,24 @@ module Auth
private
def valid_user_actor?
current_user || valid_deploy_token?
end
def valid_deploy_token?
deploy_token && deploy_token.valid_for_dependency_proxy?
end
def authorized_token
JSONWebToken::HMACToken.new(self.class.secret).tap do |token|
token['user_id'] = current_user.id
token['user_id'] = current_user.id if current_user
token['deploy_token'] = deploy_token.token if deploy_token
token.expire_time = self.class.token_expire_at
end
end
def deploy_token
params[:deploy_token]
end
end
end

View File

@ -9,6 +9,7 @@
= f.label s_('ProjectCreationLevel|Default project creation protection'), class: 'label-bold'
= f.select :default_project_creation, options_for_select(Gitlab::Access.project_creation_options, @application_setting.default_project_creation), {}, class: 'form-control'
= render_if_exists 'admin/application_settings/default_project_deletion_protection_setting', form: f
= render_if_exists 'admin/application_settings/default_delayed_project_deletion_setting', form: f
= render_if_exists 'admin/application_settings/default_project_deletion_adjourned_period_setting', form: f
.form-group.visibility-level-setting
= f.label :default_project_visibility, class: 'label-bold'

View File

@ -1,16 +1,3 @@
- if group_sidebar_link?(:kubernetes)
= nav_link(controller: [:clusters]) do
= link_to group_clusters_path(@group) do
.nav-icon-container
= sprite_icon('cloud-gear')
%span.nav-item-name
= _('Kubernetes')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: [:clusters], html_options: { class: "fly-out-top-item" } ) do
= link_to group_clusters_path(@group), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do
%strong.fly-out-top-item-name
= _('Kubernetes')
= render 'groups/sidebar/packages'
= render 'layouts/nav/sidebar/analytics_links', links: group_analytics_navbar_links(@group, current_user)

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/331082
milestone: '14.0'
type: development
group: group::dynamic analysis
default_enabled: false
default_enabled: true

View File

@ -0,0 +1,8 @@
---
name: dependency_proxy_deploy_tokens
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64363
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334565
milestone: '14.2'
type: development
group: group::package
default_enabled: false

View File

@ -96,7 +96,7 @@ Example response:
```
Users on GitLab [Premium or Ultimate](https://about.gitlab.com/pricing/) may also see
the `file_template_project_id`, `deletion_adjourned_period`, or the `geo_node_allowed_ips` parameters:
the `file_template_project_id`, `delayed_project_deletion`, `deletion_adjourned_period`, or the `geo_node_allowed_ips` parameters:
```json
{
@ -104,6 +104,7 @@ the `file_template_project_id`, `deletion_adjourned_period`, or the `geo_node_al
"signup_enabled" : true,
"file_template_project_id": 1,
"geo_node_allowed_ips": "0.0.0.0/0, ::/0",
"delayed_project_deletion": false,
"deletion_adjourned_period": 7,
...
}
@ -200,6 +201,7 @@ these parameters:
- `file_template_project_id`
- `geo_node_allowed_ips`
- `geo_status_timeout`
- `delayed_project_delection`
- `deletion_adjourned_period`
Example responses: **(PREMIUM SELF)**
@ -250,6 +252,7 @@ listed in the descriptions of the relevant settings.
| `default_project_visibility` | string | no | What visibility level new projects receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. |
| `default_projects_limit` | integer | no | Project limit per user. Default is `100000`. |
| `default_snippet_visibility` | string | no | What visibility level new snippets receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. |
| `delayed_project_deletion` | boolean | no | **(PREMIUM SELF)** Enable delayed project deletion by default in new groups. Default is `false`. |
| `deletion_adjourned_period` | integer | no | **(PREMIUM SELF)** The number of days to wait before deleting a project or group that is marked for deletion. Value must be between 0 and 90.
| `diff_max_patch_bytes` | integer | no | Maximum [diff patch size](../user/admin_area/diff_limits.md), in bytes. |
| `diff_max_files` | integer | no | Maximum [files in a diff](../user/admin_area/diff_limits.md). |

View File

@ -71,6 +71,18 @@ To ensure only Administrator users can delete projects:
1. Check the **Default project deletion protection** checkbox.
1. Click **Save changes**.
## Default delayed project deletion **(PREMIUM SELF)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/255449) in GitLab 14.2.
Projects in a group (but not a personal namespace) can be deleted after a delayed period, by
[configuring in Group Settings](../../group/index.md#enable-delayed-project-removal).
To enable delayed project deletion by default in new groups:
1. Check the **Default delayed project deletion** checkbox.
1. Click **Save changes**.
## Default deletion delay **(PREMIUM SELF)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32935) in GitLab 12.6.

View File

@ -1063,6 +1063,9 @@ follows:
Both methods are equivalent in functionality. Use whichever is feasible.
In [GitLab 14.2 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/324990), site profile
validation happens in a CI job using the [GitLab Runner](../../../ci/runners/index.md).
#### Create a site profile
To create a site profile:

View File

@ -89,6 +89,7 @@ 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`.
- A [group deploy token](../../../user/project/deploy_tokens/index.md#group-deploy-token) with the scope set to `read_registry` and `write_registry`.
#### Authenticate within CI/CD
@ -123,7 +124,7 @@ Proxy manually without including the port:
docker pull gitlab.example.com:443/my-group/dependency_proxy/containers/alpine:latest
```
You can also use [custom CI/CD variables](../../../ci/variables/index.md#custom-cicd-variables) to store and access your personal access token or other valid credentials.
You can also use [custom CI/CD variables](../../../ci/variables/index.md#custom-cicd-variables) to store and access your personal access token or deploy token.
### Store a Docker image in Dependency Proxy cache

View File

@ -94,3 +94,48 @@ the following table.
You may enable or disable project access token creation for all projects in a group in **Group > Settings > General > Permissions, LFS, 2FA > Allow project access token creation**.
Even when creation is disabled, you can still use and revoke existing project access tokens.
This setting is available only on top-level groups.
## Group access token workaround **(FREE SELF)**
NOTE:
This section describes a workaround and is subject to change.
Group access tokens let you use a single token to:
- Perform actions at the group level.
- Manage the projects within the group.
We don't support group access tokens in the GitLab UI, though GitLab self-managed
administrators can create them using the [Rails console](../../../administration/operations/rails_console.md).
<div class="video-fallback">
For a demo of the group access token workaround, see <a href="https://www.youtube.com/watch?v=W2fg1P1xmU0">Demo: Group Level Access Tokens</a>.
</div>
<figure class="video-container">
<iframe src="https://www.youtube.com/embed/W2fg1P1xmU0" frameborder="0" allowfullscreen="true"> </iframe>
</figure>
### Create a group access token
To create a group access token, run the following in a Rails console:
```ruby
admin = User.find(1) # group admin
group = Group.find(109) # the group you want to create a token for
bot = Users::CreateService.new(admin, { name: 'group_token', username: "group_#{group.id}_bot", email: "group_#{group.id}_bot@example.com", user_type: :project_bot }).execute # create the group bot user
# for further group access tokens, the username should be group_#{group.id}_bot#{bot_count}, e.g. group_109_bot2, and their email should be group_109_bot2@example.com
bot.confirm # confirm the bot
group.add_user(bot, :maintainer) # add the bot to the group at the desired access level
token = bot.personal_access_tokens.create(scopes:[:api, :write_repository], name: 'group_token') # give it a PAT
gtoken = token.token # get the token value
```
### Revoke a group access token
To revoke a group access token, run the following in a Rails console:
```ruby
bot = User.find_by(username: 'group_109_bot') # the owner of the token you want to revoke
token = bot.personal_access_tokens.last # the token you want to revoke
token.revoke!
```

View File

@ -214,6 +214,8 @@ module API
update_project_feature_usage_for(user_project)
next [] unless user_project.repo_exists?
branches = ::Kaminari.paginate_array(user_project.repository.branches.sort_by(&:name))
present paginate(branches), with: ::API::Github::Entities::Branch, project: user_project

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
module Sidebars
module Groups
module Menus
class KubernetesMenu < ::Sidebars::Menu
override :link
def link
group_clusters_path(context.group)
end
override :title
def title
_('Kubernetes')
end
override :sprite_icon
def sprite_icon
'cloud-gear'
end
override :render?
def render?
can?(context.current_user, :read_cluster, context.group)
end
override :extra_container_html_options
def extra_container_html_options
{
class: 'shortcuts-kubernetes'
}
end
override :active_routes
def active_routes
{ controller: :clusters }
end
end
end
end
end

View File

@ -11,6 +11,7 @@ module Sidebars
add_menu(Sidebars::Groups::Menus::IssuesMenu.new(context))
add_menu(Sidebars::Groups::Menus::MergeRequestsMenu.new(context))
add_menu(Sidebars::Groups::Menus::CiCdMenu.new(context))
add_menu(Sidebars::Groups::Menus::KubernetesMenu.new(context))
end
override :render_raw_menus_partial

View File

@ -10555,6 +10555,9 @@ msgstr ""
msgid "Default classification label"
msgstr ""
msgid "Default delayed project deletion"
msgstr ""
msgid "Default deletion delay"
msgstr ""
@ -11735,6 +11738,9 @@ msgstr ""
msgid "Documents reindexed: %{processed_documents} (%{percentage}%%)"
msgstr ""
msgid "Does not apply to projects in personal namespaces, which are deleted immediately on request."
msgstr ""
msgid "Domain"
msgstr ""
@ -12260,6 +12266,9 @@ msgstr ""
msgid "Enable container expiration and retention policies for projects created earlier than GitLab 12.7."
msgstr ""
msgid "Enable delayed project deletion by default for newly-created groups."
msgstr ""
msgid "Enable error tracking"
msgstr ""
@ -15944,7 +15953,7 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
msgid "GroupSettings|Enable delayed project removal"
msgid "GroupSettings|Enable delayed project deletion"
msgstr ""
msgid "GroupSettings|Export group"
@ -22577,9 +22586,6 @@ msgstr ""
msgid "Not all data has been processed yet, the accuracy of the chart for the selected timeframe is limited."
msgstr ""
msgid "Not applicable to personal namespaced projects, which are deleted immediately on request."
msgstr ""
msgid "Not available"
msgstr ""

View File

@ -30,16 +30,31 @@ RSpec.describe Groups::DependencyProxyAuthController do
end
context 'with valid JWT' do
let_it_be(:user) { create(:user) }
context 'user' do
let_it_be(:user) { create(:user) }
let(:jwt) { build_jwt(user) }
let(:token_header) { "Bearer #{jwt.encoded}" }
let(:jwt) { build_jwt(user) }
let(:token_header) { "Bearer #{jwt.encoded}" }
before do
request.headers['HTTP_AUTHORIZATION'] = token_header
before do
request.headers['HTTP_AUTHORIZATION'] = token_header
end
it { is_expected.to have_gitlab_http_status(:success) }
end
it { is_expected.to have_gitlab_http_status(:success) }
context 'deploy token' do
let_it_be(:user) { create(:deploy_token) }
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
end
context 'with invalid JWT' do
@ -51,7 +66,7 @@ RSpec.describe Groups::DependencyProxyAuthController do
request.headers['HTTP_AUTHORIZATION'] = token_header
end
it { is_expected.to have_gitlab_http_status(:not_found) }
it { is_expected.to have_gitlab_http_status(:unauthorized) }
end
context 'token with no user id' do
@ -61,7 +76,7 @@ RSpec.describe Groups::DependencyProxyAuthController do
request.headers['HTTP_AUTHORIZATION'] = token_header
end
it { is_expected.to have_gitlab_http_status(:not_found) }
it { is_expected.to have_gitlab_http_status(:unauthorized) }
end
context 'expired token' do
@ -76,6 +91,32 @@ RSpec.describe Groups::DependencyProxyAuthController do
it { is_expected.to have_gitlab_http_status(:unauthorized) }
end
context 'expired deploy token' do
let_it_be(:user) { create(:deploy_token, :expired) }
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(:unauthorized) }
end
context 'revoked deploy token' do
let_it_be(:user) { create(:deploy_token, :revoked) }
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(:unauthorized) }
end
end
end
end

View File

@ -7,8 +7,8 @@ RSpec.describe Groups::DependencyProxyForContainersController do
include DependencyProxyHelpers
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:group) { create(:group, :private) }
let(:group) { create(:group) }
let(:token_response) { { status: :success, token: 'abcd1234' } }
let(:jwt) { build_jwt(user) }
let(:token_header) { "Bearer #{jwt.encoded}" }
@ -20,6 +20,8 @@ RSpec.describe Groups::DependencyProxyForContainersController do
end
context 'feature flag disabled' do
let_it_be(:group) { create(:group) }
before do
stub_feature_flags(dependency_proxy_for_private_groups: false)
end
@ -35,13 +37,12 @@ RSpec.describe Groups::DependencyProxyForContainersController do
stub_feature_flags(dependency_proxy_for_private_groups: false)
end
it 'redirects', :aggregate_failures do
it 'returns not found' 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)
expect(response).to have_gitlab_http_status(:not_found)
end
end
@ -53,21 +54,95 @@ RSpec.describe Groups::DependencyProxyForContainersController do
request.headers['HTTP_AUTHORIZATION'] = token_header
end
it { is_expected.to have_gitlab_http_status(:not_found) }
it { is_expected.to have_gitlab_http_status(:unauthorized) }
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 'deploy tokens with dependency_proxy_deploy_tokens disabled' do
before do
stub_feature_flags(dependency_proxy_deploy_tokens: false)
end
context 'with deploy token from a different group,' do
let_it_be(:user) { create(:deploy_token, :group, :dependency_proxy_scopes) }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'with revoked deploy token' do
let_it_be(:user) { create(:deploy_token, :revoked, :group, :dependency_proxy_scopes) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'with expired deploy token' do
let_it_be(:user) { create(:deploy_token, :expired, :group, :dependency_proxy_scopes) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'with deploy token with insufficient scopes' do
let_it_be(:user) { create(:deploy_token, :group) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'when a group is not found' do
before do
expect(Group).to receive(:find_by_full_path).and_return(nil)
end
it { is_expected.to have_gitlab_http_status(:not_found) }
end
end
context 'deploy tokens with dependency_proxy_deploy_tokens enabled' do
context 'with deploy token from a different group,' do
let_it_be(:user) { create(:deploy_token, :group, :dependency_proxy_scopes) }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'with revoked deploy token' do
let_it_be(:user) { create(:deploy_token, :revoked, :group, :dependency_proxy_scopes) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) }
it { is_expected.to have_gitlab_http_status(:unauthorized) }
end
context 'with expired deploy token' do
let_it_be(:user) { create(:deploy_token, :expired, :group, :dependency_proxy_scopes) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) }
it { is_expected.to have_gitlab_http_status(:unauthorized) }
end
context 'with deploy token with insufficient scopes' do
let_it_be(:user) { create(:deploy_token, :group) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'when a group is not found' do
before do
expect(Group).to receive(:find_by_full_path).and_return(nil)
end
it { is_expected.to have_gitlab_http_status(:not_found) }
end
end
context 'when user is not found' do
before do
allow(User).to receive(:find).and_return(nil)
@ -115,6 +190,25 @@ RSpec.describe Groups::DependencyProxyForContainersController do
subject { get_manifest }
shared_examples 'a successful manifest pull' do
it 'sends a file' do
expect(controller).to receive(:send_file).with(manifest.file.path, type: manifest.content_type)
subject
end
it 'returns Content-Disposition: attachment', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['Docker-Content-Digest']).to eq(manifest.digest)
expect(response.headers['Content-Length']).to eq(manifest.size)
expect(response.headers['Docker-Distribution-Api-Version']).to eq(DependencyProxy::DISTRIBUTION_API_VERSION)
expect(response.headers['Etag']).to eq("\"#{manifest.digest}\"")
expect(response.headers['Content-Disposition']).to match(/^attachment/)
end
end
context 'feature enabled' do
before do
enable_dependency_proxy
@ -123,14 +217,6 @@ RSpec.describe Groups::DependencyProxyForContainersController do
it_behaves_like 'without a token'
it_behaves_like 'without permission'
it_behaves_like 'feature flag disabled with private group'
it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest'
context 'with a cache entry' do
let(:pull_response) { { status: :success, manifest: manifest, from_cache: true } }
it_behaves_like 'returning response status', :success
it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest_from_cache'
end
context 'remote token request fails' do
let(:token_response) do
@ -141,6 +227,10 @@ RSpec.describe Groups::DependencyProxyForContainersController do
}
end
before do
group.add_reporter(user)
end
it 'proxies status from the remote token request', :aggregate_failures do
subject
@ -158,6 +248,10 @@ RSpec.describe Groups::DependencyProxyForContainersController do
}
end
before do
group.add_reporter(user)
end
it 'proxies status from the remote manifest request', :aggregate_failures do
subject
@ -166,21 +260,58 @@ RSpec.describe Groups::DependencyProxyForContainersController do
end
end
it 'sends a file' do
expect(controller).to receive(:send_file).with(manifest.file.path, type: manifest.content_type)
context 'a valid user' do
before do
group.add_reporter(user)
end
subject
it_behaves_like 'a successful manifest pull'
it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest'
context 'with a cache entry' do
let(:pull_response) { { status: :success, manifest: manifest, from_cache: true } }
it_behaves_like 'returning response status', :success
it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest_from_cache'
end
context 'with dependency_proxy_deploy_tokens feature flag disabled' do
before do
stub_feature_flags(dependency_proxy_deploy_tokens: false)
end
it_behaves_like 'a successful manifest pull'
end
end
it 'returns Content-Disposition: attachment' do
subject
context 'a valid deploy token with dependency_proxy_deploy_tokens feature flag disabled' do
let_it_be(:user) { create(:deploy_token, :dependency_proxy_scopes, :group) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) }
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['Docker-Content-Digest']).to eq(manifest.digest)
expect(response.headers['Content-Length']).to eq(manifest.size)
expect(response.headers['Docker-Distribution-Api-Version']).to eq(DependencyProxy::DISTRIBUTION_API_VERSION)
expect(response.headers['Etag']).to eq("\"#{manifest.digest}\"")
expect(response.headers['Content-Disposition']).to match(/^attachment/)
before do
stub_feature_flags(dependency_proxy_deploy_tokens: false)
end
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'a valid deploy token' do
let_it_be(:user) { create(:deploy_token, :dependency_proxy_scopes, :group) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) }
it_behaves_like 'a successful manifest pull'
context 'pulling from a subgroup' do
let_it_be_with_reload(:parent_group) { create(:group) }
let_it_be_with_reload(:group) { create(:group, parent: parent_group) }
before do
parent_group.create_dependency_proxy_setting!(enabled: true)
group_deploy_token.update_column(:group_id, parent_group.id)
end
it_behaves_like 'a successful manifest pull'
end
end
end
@ -203,42 +334,7 @@ RSpec.describe Groups::DependencyProxyForContainersController do
end
end
subject { get_blob }
context 'feature enabled' do
before 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'
it_behaves_like 'a package tracking event', described_class.name, 'pull_blob'
context 'with a cache entry' do
let(:blob_response) { { status: :success, blob: blob, from_cache: true } }
it_behaves_like 'returning response status', :success
it_behaves_like 'a package tracking event', described_class.name, 'pull_blob_from_cache'
end
context 'remote blob request fails' do
let(:blob_response) do
{
status: :error,
http_status: 400,
message: ''
}
end
it 'proxies status from the remote blob request', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:bad_request)
expect(response.body).to be_empty
end
end
shared_examples 'a successful blob pull' do
it 'sends a file' do
expect(controller).to receive(:send_file).with(blob.file.path, {})
@ -253,6 +349,93 @@ RSpec.describe Groups::DependencyProxyForContainersController do
end
end
subject { get_blob }
context 'feature enabled' do
before 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
{
status: :error,
http_status: 400,
message: ''
}
end
before do
group.add_reporter(user)
end
it 'proxies status from the remote blob request', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:bad_request)
expect(response.body).to be_empty
end
end
context 'a valid user' do
before do
group.add_reporter(user)
end
it_behaves_like 'a successful blob pull'
it_behaves_like 'a package tracking event', described_class.name, 'pull_blob'
context 'with a cache entry' do
let(:blob_response) { { status: :success, blob: blob, from_cache: true } }
it_behaves_like 'returning response status', :success
it_behaves_like 'a package tracking event', described_class.name, 'pull_blob_from_cache'
end
context 'with dependency_proxy_deploy_tokens feature flag disabled' do
before do
stub_feature_flags(dependency_proxy_deploy_tokens: false)
end
it_behaves_like 'a successful blob pull'
end
end
context 'a valid deploy token with dependency_proxy_deploy_tokens feature flag disabled' do
let_it_be(:user) { create(:deploy_token, :group, :dependency_proxy_scopes) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) }
before do
stub_feature_flags(dependency_proxy_deploy_tokens: false)
end
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'a valid deploy token' do
let_it_be(:user) { create(:deploy_token, :group, :dependency_proxy_scopes) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) }
it_behaves_like 'a successful blob pull'
context 'pulling from a subgroup' do
let_it_be_with_reload(:parent_group) { create(:group) }
let_it_be_with_reload(:group) { create(:group, parent: parent_group) }
before do
parent_group.create_dependency_proxy_setting!(enabled: true)
group_deploy_token.update_column(:group_id, parent_group.id)
end
it_behaves_like 'a successful blob pull'
end
end
end
it_behaves_like 'not found when disabled'
def get_blob

View File

@ -35,9 +35,13 @@ FactoryBot.define do
end
trait :all_scopes do
write_registry { true}
write_registry { true }
read_package_registry { true }
write_package_registry { true }
end
trait :dependency_proxy_scopes do
write_registry { true }
end
end
end

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::Groups::Menus::KubernetesMenu do
let_it_be(:owner) { create(:user) }
let_it_be(:group) do
build(:group, :private).tap do |g|
g.add_owner(owner)
end
end
let(:user) { owner }
let(:context) { Sidebars::Groups::Context.new(current_user: user, container: group) }
let(:menu) { described_class.new(context) }
describe '#render?' do
context 'when user can read clusters' do
it 'returns true' do
expect(menu.render?).to eq true
end
end
context 'when user cannot read clusters rules' do
let(:user) { nil }
it 'returns false' do
expect(menu.render?).to eq false
end
end
end
end

View File

@ -22,6 +22,32 @@ RSpec.describe DeployToken do
it { is_expected.to validate_presence_of(:deploy_token_type) }
end
shared_examples 'invalid group deploy token' do
context 'revoked' do
before do
deploy_token.update_column(:revoked, true)
end
it { is_expected.to eq(false) }
end
context 'expired' do
before do
deploy_token.update!(expires_at: Date.today - 1.month)
end
it { is_expected.to eq(false) }
end
context 'project type' do
before do
deploy_token.update_column(:deploy_token_type, 2)
end
it { is_expected.to eq(false) }
end
end
describe 'deploy_token_type validations' do
context 'when a deploy token is associated to a group' do
it 'does not allow setting a project to it' do
@ -70,6 +96,50 @@ RSpec.describe DeployToken do
end
end
describe '#valid_for_dependency_proxy?' do
let_it_be_with_reload(:deploy_token) { create(:deploy_token, :group, :dependency_proxy_scopes) }
subject { deploy_token.valid_for_dependency_proxy? }
it { is_expected.to eq(true) }
it_behaves_like 'invalid group deploy token'
context 'insufficient scopes' do
before do
deploy_token.update_column(:write_registry, false)
end
it { is_expected.to eq(false) }
end
end
describe '#has_access_to_group?' do
let_it_be(:group) { create(:group) }
let_it_be_with_reload(:deploy_token) { create(:deploy_token, :group) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, group: group, deploy_token: deploy_token) }
let(:test_group) { group }
subject { deploy_token.has_access_to_group?(test_group) }
it { is_expected.to eq(true) }
it_behaves_like 'invalid group deploy token'
context 'for a sub group' do
let(:test_group) { create(:group, parent: group) }
it { is_expected.to eq(true) }
end
context 'for a different group' do
let(:test_group) { create(:group) }
it { is_expected.to eq(false) }
end
end
describe '#scopes' do
context 'with all the scopes' do
let_it_be(:deploy_token) { create(:deploy_token, :all_scopes) }

View File

@ -3,15 +3,40 @@
require 'spec_helper'
RSpec.describe GroupDeployToken, type: :model do
let(:group) { create(:group) }
let(:deploy_token) { create(:deploy_token) }
let_it_be(:group) { create(:group) }
let_it_be(:deploy_token) { create(:deploy_token) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, group: group, deploy_token: deploy_token) }
subject(:group_deploy_token) { create(:group_deploy_token, group: group, deploy_token: deploy_token) }
describe 'relationships' do
it { is_expected.to belong_to :group }
it { is_expected.to belong_to :deploy_token }
end
it { is_expected.to belong_to :group }
it { is_expected.to belong_to :deploy_token }
describe 'validation' do
it { is_expected.to validate_presence_of :deploy_token }
it { is_expected.to validate_presence_of :group }
it { is_expected.to validate_uniqueness_of(:deploy_token_id).scoped_to(:group_id) }
end
it { is_expected.to validate_presence_of :deploy_token }
it { is_expected.to validate_presence_of :group }
it { is_expected.to validate_uniqueness_of(:deploy_token_id).scoped_to(:group_id) }
describe '#has_access_to_group?' do
subject { group_deploy_token.has_access_to_group?(test_group) }
context 'for itself' do
let(:test_group) { group }
it { is_expected.to eq(true) }
end
context 'for a subgroup' do
let(:test_group) { create(:group, parent: group) }
it { is_expected.to eq(true) }
end
context 'for other group' do
let(:test_group) { create(:group) }
it { is_expected.to eq(false) }
end
end
end

View File

@ -472,6 +472,17 @@ RSpec.describe API::V3::Github do
expect(response).to have_gitlab_http_status(:ok)
end
context 'when the project has no repository', :aggregate_failures do
let_it_be(:project) { create(:project, creator: user) }
it 'returns an empty collection response' do
jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_empty
end
end
end
context 'unauthenticated' do

View File

@ -224,8 +224,10 @@ RSpec.describe JwtController do
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :private, group: group) }
let_it_be(:group_deploy_token) { create(:deploy_token, :group, groups: [group]) }
let_it_be(:project_deploy_token) { create(:deploy_token, :project, projects: [project]) }
let_it_be(:group_deploy_token) { create(:deploy_token, :group, :dependency_proxy_scopes) }
let_it_be(:gdeploy_token) { create(:group_deploy_token, deploy_token: group_deploy_token, group: group) }
let_it_be(:project_deploy_token) { create(:deploy_token, :project, :dependency_proxy_scopes) }
let_it_be(:pdeploy_token) { create(:project_deploy_token, deploy_token: project_deploy_token, project: project) }
let_it_be(:service_name) { 'dependency_proxy' }
let(:headers) { { authorization: credentials(credential_user, credential_password) } }
@ -264,7 +266,7 @@ RSpec.describe JwtController do
let(:credential_user) { group_deploy_token.username }
let(:credential_password) { group_deploy_token.token }
it_behaves_like 'returning response status', :forbidden
it_behaves_like 'with valid credentials'
end
context 'with project deploy token' do
@ -274,6 +276,28 @@ RSpec.describe JwtController do
it_behaves_like 'returning response status', :forbidden
end
context 'with revoked group deploy token' do
let(:credential_user) { group_deploy_token.username }
let(:credential_password) { project_deploy_token.token }
before do
group_deploy_token.update_column(:revoked, true)
end
it_behaves_like 'returning response status', :unauthorized
end
context 'with group deploy token with insufficient scopes' do
let(:credential_user) { group_deploy_token.username }
let(:credential_password) { project_deploy_token.token }
before do
group_deploy_token.update_column(:write_registry, false)
end
it_behaves_like 'returning response status', :unauthorized
end
context 'with invalid credentials' do
let(:credential_user) { 'foo' }
let(:credential_password) { 'bar' }

View File

@ -21,6 +21,12 @@ RSpec.describe Auth::DependencyProxyAuthenticationService do
end
end
shared_examples 'returning a token' do
it 'returns a token' do
expect(subject[:token]).not_to be_nil
end
end
context 'dependency proxy is not enabled' do
before do
stub_config(dependency_proxy: { enabled: false })
@ -35,10 +41,14 @@ RSpec.describe Auth::DependencyProxyAuthenticationService do
it_behaves_like 'returning', status: 403, message: 'access forbidden'
end
context 'with a deploy token as user' do
let_it_be(:user) { create(:deploy_token, :group, :dependency_proxy_scopes) }
it_behaves_like 'returning a token'
end
context 'with a user' do
it 'returns a token' do
expect(subject[:token]).not_to be_nil
end
it_behaves_like 'returning a token'
end
end
end

View File

@ -14,6 +14,19 @@ RSpec.describe DependencyProxy::AuthTokenService do
result = subject
expect(result['user_id']).to eq(user.id)
expect(result['deploy_token']).to be_nil
end
context 'with a deploy token' do
let_it_be(:deploy_token) { create(:deploy_token) }
let_it_be(:token) { build_jwt(deploy_token) }
it 'returns the deploy token' do
result = subject
expect(result['deploy_token']).to eq(deploy_token.token)
expect(result['user_id']).to be_nil
end
end
it 'raises an error if the token is expired' do

View File

@ -34,7 +34,8 @@ module DependencyProxyHelpers
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['user_id'] = user.id if user.is_a?(User)
jwt['deploy_token'] = user.token if user.is_a?(DeployToken)
jwt.expire_time = expire_time || jwt.issued_at + 1.minute
end
end

View File

@ -100,4 +100,12 @@ RSpec.describe 'layouts/nav/sidebar/_group' do
expect(rendered).to have_link('Runners', href: group_runners_path(group))
end
end
describe 'Kubernetes menu' do
it 'has a link to the group cluster list path' do
render
expect(rendered).to have_link('Kubernetes', href: group_clusters_path(group))
end
end
end

View File

@ -16,4 +16,4 @@ OptionParser.new do |opts|
end
end.parse!
Tooling::ParallelRSpecRunner.run(options)
Tooling::ParallelRSpecRunner.run(**options)