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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-12-17 06:10:36 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-12-17 06:10:36 +0300
commit6c46be4823292f25fa1fe28d1c899816c727893d (patch)
tree4298b8d46949a87db2b130eff3963166c5b1be3b
parent2e3423047a0df21153b06fee43743a1fa90e658e (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/services/auth/container_registry_authentication_service.rb12
-rw-r--r--config/gitlab.yml.example2
-rw-r--r--doc/.vale/gitlab/SubstitutionSuggestions.yml3
-rw-r--r--doc/ci/caching/index.md31
-rw-r--r--doc/user/compliance/license_compliance/index.md65
-rw-r--r--doc/user/project/web_ide/index.md2
-rw-r--r--lib/gitlab/setup_helper.rb45
-rwxr-xr-xscripts/gitaly-test-build2
-rwxr-xr-xscripts/gitaly-test-spawn1
-rw-r--r--scripts/gitaly_test.rb17
-rw-r--r--spec/services/auth/container_registry_authentication_service_spec.rb990
-rw-r--r--spec/support/helpers/test_env.rb7
-rw-r--r--spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb1006
13 files changed, 1135 insertions, 1048 deletions
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index 831a25a637e..d74f20511bd 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -130,6 +130,7 @@ module Auth
ContainerRepository.create_from_path!(path)
end
+ # Overridden in EE
def can_access?(requested_project, requested_action)
return false unless requested_project.container_registry_enabled?
return false if requested_project.repository_access_level == ::ProjectFeature::DISABLED
@@ -226,11 +227,16 @@ module Auth
end
end
+ # Overridden in EE
+ def extra_info
+ {}
+ end
+
def log_if_actions_denied(type, requested_project, requested_actions, authorized_actions)
return if requested_actions == authorized_actions
log_info = {
- message: "Denied container registry permissions",
+ message: 'Denied container registry permissions',
scope_type: type,
requested_project_path: requested_project.full_path,
requested_actions: requested_actions,
@@ -238,9 +244,11 @@ module Auth
username: current_user&.username,
user_id: current_user&.id,
project_path: project&.full_path
- }.compact
+ }.merge!(extra_info).compact
Gitlab::AuthLogger.warn(log_info)
end
end
end
+
+Auth::ContainerRegistryAuthenticationService.prepend_if_ee('EE::Auth::ContainerRegistryAuthenticationService')
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 291431aa23f..57788e55f8f 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -1386,7 +1386,7 @@ test:
storages:
default:
path: tmp/tests/repositories/
- gitaly_address: unix:tmp/tests/gitaly/gitaly.socket
+ gitaly_address: unix:tmp/tests/gitaly/praefect.socket
gitaly:
client_path: tmp/tests/gitaly
diff --git a/doc/.vale/gitlab/SubstitutionSuggestions.yml b/doc/.vale/gitlab/SubstitutionSuggestions.yml
index be5726a8d8f..82e3e789864 100644
--- a/doc/.vale/gitlab/SubstitutionSuggestions.yml
+++ b/doc/.vale/gitlab/SubstitutionSuggestions.yml
@@ -13,8 +13,9 @@ ignorecase: true
swap:
active user: '"billable user"'
active users: '"billable users"'
- since: '"because" or "after"'
+ docs: documentation
once that: '"after that"'
once the: '"after the"'
once you: '"after you"'
+ since: '"because" or "after"'
within: '"in"'
diff --git a/doc/ci/caching/index.md b/doc/ci/caching/index.md
index e8b22a24017..08a45714de3 100644
--- a/doc/ci/caching/index.md
+++ b/doc/ci/caching/index.md
@@ -316,6 +316,37 @@ rspec:
- rspec spec
```
+If you have jobs that each need a different selection of gems, use the `prefix`
+keyword in the global `cache` definition. This configuration generates a different
+cache for each job.
+
+For example, a testing job might not need the same gems as a job that deploys to
+production:
+
+```yaml
+cache:
+ key:
+ files:
+ - Gemfile.lock
+ prefix: ${CI_JOB_NAME}
+ paths:
+ - vendor/ruby
+
+test_job:
+ stage: test
+ before_script:
+ - bundle install --without production --path vendor/ruby
+ script:
+ - bundle exec rspec
+
+deploy_job:
+ stage: production
+ before_script:
+ - bundle install --without test --path vendor/ruby
+ script:
+ - bundle exec deploy
+```
+
### Caching Go dependencies
Assuming your project is using [Go Modules](https://github.com/golang/go/wiki/Modules) to install
diff --git a/doc/user/compliance/license_compliance/index.md b/doc/user/compliance/license_compliance/index.md
index c4f1bd8b424..f78b6115623 100644
--- a/doc/user/compliance/license_compliance/index.md
+++ b/doc/user/compliance/license_compliance/index.md
@@ -19,14 +19,14 @@ in your existing `.gitlab-ci.yml` file or by implicitly using
[Auto License Compliance](../../../topics/autodevops/stages.md#auto-license-compliance)
that is provided by [Auto DevOps](../../../topics/autodevops/index.md).
-GitLab checks the License Compliance report, compares the licenses between the
-source and target branches, and shows the information right on the merge request.
-Denied licenses are notated with an `x` red icon next to them
-as well as new licenses which need a decision from you. In addition, you can
-[manually allow or deny](#policies)
-licenses in your project's license compliance policy section. If GitLab detects a denied license
-in a new commit, GitLab blocks any merge requests containing that commit and instructs the developer
-to remove the license.
+The [License Finder](https://github.com/pivotal/LicenseFinder) scan tool runs as part of the CI/CD
+pipeline, and detects the licenses in use. GitLab checks the License Compliance report, compares the
+licenses between the source and target branches, and shows the information right on the merge
+request. Denied licenses are indicated by a `x` red icon next to them as well as new licenses that
+need a decision from you. In addition, you can [manually allow or deny](#policies) licenses in your
+project's license compliance policy section. If a denied license is detected in a new commit,
+GitLab blocks any merge requests containing that commit and instructs the developer to remove the
+license.
NOTE:
If the license compliance report doesn't have anything to compare to, no information
@@ -51,36 +51,33 @@ You can view and modify existing policies from the [policies](#policies) tab.
The following languages and package managers are supported.
-| Language | Package managers | Notes | Scan Tool |
-|------------|------------------|-------|-----------|
-| JavaScript | [Bower](https://bower.io/), [npm](https://www.npmjs.com/) | | [License Finder](https://github.com/pivotal/LicenseFinder) |
-| Go | [Godep](https://github.com/tools/godep), [go mod](https://github.com/golang/go/wiki/Modules) | | [License Finder](https://github.com/pivotal/LicenseFinder) |
-| Java | [Gradle](https://gradle.org/), [Maven](https://maven.apache.org/) | | [License Finder](https://github.com/pivotal/LicenseFinder) |
-| .NET | [Nuget](https://www.nuget.org/) | The .NET Framework is supported via the [mono project](https://www.mono-project.com/). There are, however, some limitations. The scanner doesn't support Windows-specific dependencies and doesn't report dependencies of your project's listed dependencies. Also, the scanner always marks detected licenses for all dependencies as `unknown`. | [License Finder](https://github.com/pivotal/LicenseFinder) |
-| Python | [pip](https://pip.pypa.io/en/stable/) | Python is supported through [requirements.txt](https://pip.pypa.io/en/stable/user_guide/#requirements-files) and [Pipfile.lock](https://github.com/pypa/pipfile#pipfilelock). | [License Finder](https://github.com/pivotal/LicenseFinder) |
-| Ruby | [gem](https://rubygems.org/) | | [License Finder](https://github.com/pivotal/LicenseFinder)|
+Java 8 and Gradle 1.x projects are not supported. The minimum supported version of Maven is 3.2.5.
-NOTE:
-Java 8 and Gradle 1.x projects are not supported.
-The minimum supported version of Maven is 3.2.5.
+| Language | Package managers | Notes |
+|------------|----------------------------------------------------------------------------------------------|-------|
+| JavaScript | [Bower](https://bower.io/), [npm](https://www.npmjs.com/) | |
+| Go | [Godep](https://github.com/tools/godep), [go mod](https://github.com/golang/go/wiki/Modules) | |
+| Java | [Gradle](https://gradle.org/), [Maven](https://maven.apache.org/) | |
+| .NET | [Nuget](https://www.nuget.org/) | The .NET Framework is supported via the [mono project](https://www.mono-project.com/). There are, however, some limitations. The scanner doesn't support Windows-specific dependencies and doesn't report dependencies of your project's listed dependencies. Also, the scanner always marks detected licenses for all dependencies as `unknown`. |
+| Python | [pip](https://pip.pypa.io/en/stable/) | Python is supported through [requirements.txt](https://pip.pypa.io/en/stable/user_guide/#requirements-files) and [Pipfile.lock](https://github.com/pypa/pipfile#pipfilelock). |
+| Ruby | [gem](https://rubygems.org/) | |
### Experimental support
-The following languages and package managers are [supported experimentally](https://github.com/pivotal/LicenseFinder#experimental-project-types),
-which means that the reported licenses might be incomplete or inaccurate.
-
-| Language | Package managers | Scan Tool |
-|------------|-------------------------------------------------------------------|----------------------------------------------------------|
-| JavaScript | [Yarn](https://yarnpkg.com/)|[License Finder](https://github.com/pivotal/LicenseFinder)|
-| Go | go get, gvt, glide, dep, trash, govendor |[License Finder](https://github.com/pivotal/LicenseFinder)|
-| Erlang | [Rebar](https://www.rebar3.org/) |[License Finder](https://github.com/pivotal/LicenseFinder)|
-| Objective-C, Swift | [Carthage](https://github.com/Carthage/Carthage) | [License Finder](https://github.com/pivotal/LicenseFinder) |
-| Objective-C, Swift | [CocoaPods](https://cocoapods.org/) v0.39 and below |[License Finder](https://github.com/pivotal/LicenseFinder)|
-| Elixir | [Mix](https://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html) |[License Finder](https://github.com/pivotal/LicenseFinder)|
-| C++/C | [Conan](https://conan.io/) |[License Finder](https://github.com/pivotal/LicenseFinder)|
-| Scala | [sbt](https://www.scala-sbt.org/) |[License Finder](https://github.com/pivotal/LicenseFinder)|
-| Rust | [Cargo](https://crates.io) |[License Finder](https://github.com/pivotal/LicenseFinder)|
-| PHP | [Composer](https://getcomposer.org/) |[License Finder](https://github.com/pivotal/LicenseFinder)|
+The following languages and package managers are [supported experimentally](https://github.com/pivotal/LicenseFinder#experimental-project-types).
+The reported licenses might be incomplete or inaccurate.
+
+| Language | Package managers |
+|------------|---------------------------------------------------------------------------------------------------------------|
+| JavaScript | [Yarn](https://yarnpkg.com/) |
+| Go | go get, gvt, glide, dep, trash, govendor |
+| Erlang | [Rebar](https://www.rebar3.org/) |
+| Objective-C, Swift | [Carthage](https://github.com/Carthage/Carthage), [CocoaPods](https://cocoapods.org/) v0.39 and below |
+| Elixir | [Mix](https://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html) |
+| C++/C | [Conan](https://conan.io/) |
+| Scala | [sbt](https://www.scala-sbt.org/) |
+| Rust | [Cargo](https://crates.io) |
+| PHP | [Composer](https://getcomposer.org/) |
## Requirements
diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md
index 16ca90003e6..29b24028e48 100644
--- a/doc/user/project/web_ide/index.md
+++ b/doc/user/project/web_ide/index.md
@@ -288,7 +288,7 @@ below.
WARNING:
Interactive Web Terminals for the Web IDE is currently in **Beta**.
-Shared runners [do not yet support Interactive Web Terminals](https://gitlab.com/gitlab-org/gitlab/-/issues/24674),
+GitLab.com shared runners [do not yet support Interactive Web Terminals](https://gitlab.com/gitlab-org/gitlab/-/issues/24674),
so you would need to use your own private runner to make use of this feature.
[Interactive Web Terminals](../../../ci/interactive_web_terminal/index.md)
diff --git a/lib/gitlab/setup_helper.rb b/lib/gitlab/setup_helper.rb
index f6e4c3bd584..48f204e0b86 100644
--- a/lib/gitlab/setup_helper.rb
+++ b/lib/gitlab/setup_helper.rb
@@ -4,10 +4,10 @@ require 'toml-rb'
module Gitlab
module SetupHelper
- def create_configuration(dir, storage_paths, force: false)
+ def create_configuration(dir, storage_paths, force: false, options: {})
generate_configuration(
- configuration_toml(dir, storage_paths),
- get_config_path(dir),
+ configuration_toml(dir, storage_paths, options),
+ get_config_path(dir, options),
force: force
)
end
@@ -31,7 +31,7 @@ module Gitlab
module Workhorse
extend Gitlab::SetupHelper
class << self
- def configuration_toml(dir, _)
+ def configuration_toml(dir, _, _)
config = { redis: { URL: redis_url } }
TomlRB.dump(config)
@@ -41,8 +41,8 @@ module Gitlab
Gitlab::Redis::SharedState.url
end
- def get_config_path(dir)
- File.join(dir, 'config.toml')
+ def get_config_path(dir, _)
+ File.join(dir, 'config_path')
end
def compile_into(dir)
@@ -76,7 +76,7 @@ module Gitlab
# because it uses a Unix socket.
# For development and testing purposes, an extra storage is added to gitaly,
# which is not known to Rails, but must be explicitly stubbed.
- def configuration_toml(gitaly_dir, storage_paths, gitaly_ruby: true)
+ def configuration_toml(gitaly_dir, storage_paths, options, gitaly_ruby: true)
storages = []
address = nil
@@ -97,14 +97,20 @@ module Gitlab
config = { socket_path: address.sub(/\Aunix:/, '') }
if Rails.env.test?
+ socket_filename = options[:gitaly_socket] || "gitaly.socket"
+
+ config = {
+ # Override the set gitaly_address since Praefect is in the loop
+ socket_path: File.join(gitaly_dir, socket_filename),
+ auth: { token: 'secret' },
+ # Compared to production, tests run in constrained environments. This
+ # number is meant to grow with the number of concurrent rails requests /
+ # sidekiq jobs, and concurrency will be low anyway in test.
+ git: { catfile_cache_size: 5 }
+ }
+
storage_path = Rails.root.join('tmp', 'tests', 'second_storage').to_s
storages << { name: 'test_second_storage', path: storage_path }
-
- config[:auth] = { token: 'secret' }
- # Compared to production, tests run in constrained environments. This
- # number is meant to grow with the number of concurrent rails requests /
- # sidekiq jobs, and concurrency will be low anyway in test.
- config[:git] = { catfile_cache_size: 5 }
end
config[:storage] = storages
@@ -124,8 +130,9 @@ module Gitlab
private
- def get_config_path(dir)
- File.join(dir, 'config.toml')
+ def get_config_path(dir, options)
+ config_filename = options[:config_filename] || 'config.toml'
+ File.join(dir, config_filename)
end
end
end
@@ -133,9 +140,11 @@ module Gitlab
module Praefect
extend Gitlab::SetupHelper
class << self
- def configuration_toml(gitaly_dir, storage_paths)
+ def configuration_toml(gitaly_dir, _, _)
nodes = [{ storage: 'default', address: "unix:#{gitaly_dir}/gitaly.socket", primary: true, token: 'secret' }]
- storages = [{ name: 'default', node: nodes }]
+ second_storage_nodes = [{ storage: 'test_second_storage', address: "unix:#{gitaly_dir}/gitaly2.socket", primary: true, token: 'secret' }]
+
+ storages = [{ name: 'default', node: nodes }, { name: 'test_second_storage', node: second_storage_nodes }]
failover = { enabled: false }
config = { socket_path: "#{gitaly_dir}/praefect.socket", memory_queue_enabled: true, virtual_storage: storages, failover: failover }
config[:token] = 'secret' if Rails.env.test?
@@ -145,7 +154,7 @@ module Gitlab
private
- def get_config_path(dir)
+ def get_config_path(dir, _)
File.join(dir, 'praefect.config.toml')
end
end
diff --git a/scripts/gitaly-test-build b/scripts/gitaly-test-build
index 5254d957afd..00927646046 100755
--- a/scripts/gitaly-test-build
+++ b/scripts/gitaly-test-build
@@ -19,8 +19,10 @@ class GitalyTestBuild
# Starting gitaly further validates its configuration
gitaly_pid = start_gitaly
+ gitaly2_pid = start_gitaly2
praefect_pid = start_praefect
Process.kill('TERM', gitaly_pid)
+ Process.kill('TERM', gitaly2_pid)
Process.kill('TERM', praefect_pid)
# Make the 'gitaly' executable look newer than 'GITALY_SERVER_VERSION'.
diff --git a/scripts/gitaly-test-spawn b/scripts/gitaly-test-spawn
index 8e16b2bb656..c2ff9cd08aa 100755
--- a/scripts/gitaly-test-spawn
+++ b/scripts/gitaly-test-spawn
@@ -15,6 +15,7 @@ class GitalyTestSpawn
# In local development this pid file is used by rspec.
IO.write(File.expand_path('../tmp/tests/gitaly.pid', __dir__), start_gitaly)
+ IO.write(File.expand_path('../tmp/tests/gitaly2.pid', __dir__), start_gitaly2)
IO.write(File.expand_path('../tmp/tests/praefect.pid', __dir__), start_praefect)
end
end
diff --git a/scripts/gitaly_test.rb b/scripts/gitaly_test.rb
index 54bf07b3773..559ad8f4345 100644
--- a/scripts/gitaly_test.rb
+++ b/scripts/gitaly_test.rb
@@ -62,21 +62,36 @@ module GitalyTest
case service
when :gitaly
File.join(tmp_tests_gitaly_dir, 'config.toml')
+ when :gitaly2
+ File.join(tmp_tests_gitaly_dir, 'gitaly2.config.toml')
when :praefect
File.join(tmp_tests_gitaly_dir, 'praefect.config.toml')
end
end
+ def service_binary(service)
+ case service
+ when :gitaly, :gitaly2
+ 'gitaly'
+ when :praefect
+ 'praefect'
+ end
+ end
+
def start_gitaly
start(:gitaly)
end
+ def start_gitaly2
+ start(:gitaly2)
+ end
+
def start_praefect
start(:praefect)
end
def start(service)
- args = ["#{tmp_tests_gitaly_dir}/#{service}"]
+ args = ["#{tmp_tests_gitaly_dir}/#{service_binary(service)}"]
args.push("-config") if service == :praefect
args.push(config_path(service))
pid = spawn(env, *args, [:out, :err] => "log/#{service}-test.log")
diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb
index 90ef32f1c5c..ba7acd3d3df 100644
--- a/spec/services/auth/container_registry_authentication_service_spec.rb
+++ b/spec/services/auth/container_registry_authentication_service_spec.rb
@@ -5,993 +5,5 @@ require 'spec_helper'
RSpec.describe Auth::ContainerRegistryAuthenticationService do
include AdminModeHelper
- let(:current_project) { nil }
- let(:current_user) { nil }
- let(:current_params) { {} }
- let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) }
- let(:payload) { JWT.decode(subject[:token], rsa_key, true, { algorithm: 'RS256' }).first }
-
- let(:authentication_abilities) do
- [:read_container_image, :create_container_image, :admin_container_image]
- end
-
- subject do
- described_class.new(current_project, current_user, current_params)
- .execute(authentication_abilities: authentication_abilities)
- end
-
- before do
- allow(Gitlab.config.registry).to receive_messages(enabled: true, issuer: 'rspec', key: nil)
- allow_next_instance_of(JSONWebToken::RSAToken) do |instance|
- allow(instance).to receive(:key).and_return(rsa_key)
- end
- end
-
- shared_examples 'an authenticated' do
- it { is_expected.to include(:token) }
- it { expect(payload).to include('access') }
- end
-
- shared_examples 'a valid token' do
- it { is_expected.to include(:token) }
- it { expect(payload).to include('access') }
-
- context 'a expirable' do
- let(:expires_at) { Time.zone.at(payload['exp']) }
- let(:expire_delay) { 10 }
-
- context 'for default configuration' do
- it { expect(expires_at).not_to be_within(2.seconds).of(Time.current + expire_delay.minutes) }
- end
-
- context 'for changed configuration' do
- before do
- stub_application_setting(container_registry_token_expire_delay: expire_delay)
- end
-
- it { expect(expires_at).to be_within(2.seconds).of(Time.current + expire_delay.minutes) }
- end
- end
- end
-
- shared_examples 'a browsable' do
- let(:access) do
- [{ 'type' => 'registry',
- 'name' => 'catalog',
- 'actions' => ['*'] }]
- end
-
- it_behaves_like 'a valid token'
- it_behaves_like 'not a container repository factory'
-
- it 'has the correct scope' do
- expect(payload).to include('access' => access)
- end
- end
-
- shared_examples 'an accessible' do
- let(:access) do
- [{ 'type' => 'repository',
- 'name' => project.full_path,
- 'actions' => actions }]
- end
-
- it_behaves_like 'a valid token'
-
- it 'has the correct scope' do
- expect(payload).to include('access' => access)
- end
- end
-
- shared_examples 'an inaccessible' do
- it_behaves_like 'a valid token'
- it { expect(payload).to include('access' => []) }
- end
-
- shared_examples 'a deletable' do
- it_behaves_like 'an accessible' do
- let(:actions) { ['*'] }
- end
- end
-
- shared_examples 'a deletable since registry 2.7' do
- it_behaves_like 'an accessible' do
- let(:actions) { ['delete'] }
- end
- end
-
- shared_examples 'a pullable' do
- it_behaves_like 'an accessible' do
- let(:actions) { ['pull'] }
- end
- end
-
- shared_examples 'a pushable' do
- it_behaves_like 'an accessible' do
- let(:actions) { ['push'] }
- end
- end
-
- shared_examples 'a pullable and pushable' do
- it_behaves_like 'an accessible' do
- let(:actions) { %w(pull push) }
- end
- end
-
- shared_examples 'a forbidden' do
- it { is_expected.to include(http_status: 403) }
- it { is_expected.not_to include(:token) }
- end
-
- shared_examples 'container repository factory' do
- it 'creates a new container repository resource' do
- expect { subject }
- .to change { project.container_repositories.count }.by(1)
- end
- end
-
- shared_examples 'not a container repository factory' do
- it 'does not create a new container repository resource' do
- expect { subject }.not_to change { ContainerRepository.count }
- end
- end
-
- describe '#full_access_token' do
- let_it_be(:project) { create(:project) }
- let(:token) { described_class.full_access_token(project.full_path) }
-
- subject { { token: token } }
-
- it_behaves_like 'an accessible' do
- let(:actions) { ['*'] }
- end
-
- it_behaves_like 'not a container repository factory'
- end
-
- describe '#pull_access_token' do
- let_it_be(:project) { create(:project) }
- let(:token) { described_class.pull_access_token(project.full_path) }
-
- subject { { token: token } }
-
- it_behaves_like 'an accessible' do
- let(:actions) { ['pull'] }
- end
-
- it_behaves_like 'not a container repository factory'
- end
-
- context 'user authorization' do
- let_it_be(:current_user) { create(:user) }
-
- context 'for registry catalog' do
- let(:current_params) do
- { scopes: ["registry:catalog:*"] }
- end
-
- context 'disallow browsing for users without GitLab admin rights' do
- it_behaves_like 'an inaccessible'
- it_behaves_like 'not a container repository factory'
- end
- end
-
- context 'for private project' do
- let_it_be(:project) { create(:project) }
-
- context 'allow to use scope-less authentication' do
- it_behaves_like 'a valid token'
- end
-
- context 'allow developer to push images' do
- before_all do
- project.add_developer(current_user)
- end
-
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:push"] }
- end
-
- it_behaves_like 'a pushable'
- it_behaves_like 'container repository factory'
- end
-
- context 'disallow developer to delete images' do
- before_all do
- project.add_developer(current_user)
- end
-
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:*"] }
- end
-
- it_behaves_like 'an inaccessible'
- it_behaves_like 'not a container repository factory'
-
- it 'logs an auth warning' do
- expect(Gitlab::AuthLogger).to receive(:warn).with(
- message: 'Denied container registry permissions',
- scope_type: 'repository',
- requested_project_path: project.full_path,
- requested_actions: ['*'],
- authorized_actions: [],
- user_id: current_user.id,
- username: current_user.username
- )
-
- subject
- end
- end
-
- context 'disallow developer to delete images since registry 2.7' do
- before_all do
- project.add_developer(current_user)
- end
-
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:delete"] }
- end
-
- it_behaves_like 'an inaccessible'
- it_behaves_like 'not a container repository factory'
- end
-
- context 'allow reporter to pull images' do
- before_all do
- project.add_reporter(current_user)
- end
-
- context 'when pulling from root level repository' do
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:pull"] }
- end
-
- it_behaves_like 'a pullable'
- it_behaves_like 'not a container repository factory'
- end
- end
-
- context 'disallow reporter to delete images' do
- before_all do
- project.add_reporter(current_user)
- end
-
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:*"] }
- end
-
- it_behaves_like 'an inaccessible'
- it_behaves_like 'not a container repository factory'
- end
-
- context 'disallow reporter to delete images since registry 2.7' do
- before_all do
- project.add_reporter(current_user)
- end
-
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:delete"] }
- end
-
- it_behaves_like 'an inaccessible'
- it_behaves_like 'not a container repository factory'
- end
-
- context 'return a least of privileges' do
- before_all do
- project.add_reporter(current_user)
- end
-
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:push,pull"] }
- end
-
- it_behaves_like 'a pullable'
- it_behaves_like 'not a container repository factory'
- end
-
- context 'disallow guest to pull or push images' do
- before_all do
- project.add_guest(current_user)
- end
-
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:pull,push"] }
- end
-
- it_behaves_like 'an inaccessible'
- it_behaves_like 'not a container repository factory'
- end
-
- context 'disallow guest to delete images' do
- before_all do
- project.add_guest(current_user)
- end
-
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:*"] }
- end
-
- it_behaves_like 'an inaccessible'
- it_behaves_like 'not a container repository factory'
- end
-
- context 'disallow guest to delete images since registry 2.7' do
- before_all do
- project.add_guest(current_user)
- end
-
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:delete"] }
- end
-
- it_behaves_like 'an inaccessible'
- it_behaves_like 'not a container repository factory'
- end
- end
-
- context 'for public project' do
- let_it_be(:project) { create(:project, :public) }
-
- context 'allow anyone to pull images' do
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:pull"] }
- end
-
- it_behaves_like 'a pullable'
- it_behaves_like 'not a container repository factory'
- end
-
- context 'disallow anyone to push images' do
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:push"] }
- end
-
- it_behaves_like 'an inaccessible'
- it_behaves_like 'not a container repository factory'
- end
-
- context 'disallow anyone to delete images' do
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:*"] }
- end
-
- it_behaves_like 'an inaccessible'
- it_behaves_like 'not a container repository factory'
- end
-
- context 'disallow anyone to delete images since registry 2.7' do
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:delete"] }
- end
-
- it_behaves_like 'an inaccessible'
- it_behaves_like 'not a container repository factory'
- end
-
- context 'when repository name is invalid' do
- let(:current_params) do
- { scopes: ['repository:invalid:push'] }
- end
-
- it_behaves_like 'an inaccessible'
- it_behaves_like 'not a container repository factory'
- end
- end
-
- context 'for internal project' do
- let_it_be(:project) { create(:project, :internal) }
-
- context 'for internal user' do
- context 'allow anyone to pull images' do
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:pull"] }
- end
-
- it_behaves_like 'a pullable'
- it_behaves_like 'not a container repository factory'
- end
-
- context 'disallow anyone to push images' do
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:push"] }
- end
-
- it_behaves_like 'an inaccessible'
- it_behaves_like 'not a container repository factory'
- end
-
- context 'disallow anyone to delete images' do
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:*"] }
- end
-
- it_behaves_like 'an inaccessible'
- it_behaves_like 'not a container repository factory'
- end
-
- context 'disallow anyone to delete images since registry 2.7' do
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:delete"] }
- end
-
- it_behaves_like 'an inaccessible'
- it_behaves_like 'not a container repository factory'
- end
- end
-
- context 'for external user' do
- context 'disallow anyone to pull or push images' do
- let_it_be(:current_user) { create(:user, external: true) }
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:pull,push"] }
- end
-
- it_behaves_like 'an inaccessible'
- it_behaves_like 'not a container repository factory'
- end
-
- context 'disallow anyone to delete images' do
- let_it_be(:current_user) { create(:user, external: true) }
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:*"] }
- end
-
- it_behaves_like 'an inaccessible'
- it_behaves_like 'not a container repository factory'
- end
-
- context 'disallow anyone to delete images since registry 2.7' do
- let_it_be(:current_user) { create(:user, external: true) }
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:delete"] }
- end
-
- it_behaves_like 'an inaccessible'
- it_behaves_like 'not a container repository factory'
- end
- end
- end
- end
-
- context 'delete authorized as maintainer' do
- let_it_be(:current_project) { create(:project) }
- let_it_be(:current_user) { create(:user) }
-
- let(:authentication_abilities) do
- [:admin_container_image]
- end
-
- before_all do
- current_project.add_maintainer(current_user)
- end
-
- it_behaves_like 'a valid token'
-
- context 'allow to delete images' do
- let(:current_params) do
- { scopes: ["repository:#{current_project.full_path}:*"] }
- end
-
- it_behaves_like 'a deletable' do
- let(:project) { current_project }
- end
- end
-
- context 'allow to delete images since registry 2.7' do
- let(:current_params) do
- { scopes: ["repository:#{current_project.full_path}:delete"] }
- end
-
- it_behaves_like 'a deletable since registry 2.7' do
- let(:project) { current_project }
- end
- end
- end
-
- context 'build authorized as user' do
- let_it_be(:current_project) { create(:project) }
- let_it_be(:current_user) { create(:user) }
-
- let(:authentication_abilities) do
- [:build_read_container_image, :build_create_container_image, :build_destroy_container_image]
- end
-
- before_all do
- current_project.add_developer(current_user)
- end
-
- context 'allow to use offline_token' do
- let(:current_params) do
- { offline_token: true }
- end
-
- it_behaves_like 'an authenticated'
- end
-
- it_behaves_like 'a valid token'
-
- context 'allow to pull and push images' do
- let(:current_params) do
- { scopes: ["repository:#{current_project.full_path}:pull,push"] }
- end
-
- it_behaves_like 'a pullable and pushable' do
- let(:project) { current_project }
- end
-
- it_behaves_like 'container repository factory' do
- let(:project) { current_project }
- end
- end
-
- context 'allow to delete images since registry 2.7' do
- let(:current_params) do
- { scopes: ["repository:#{current_project.full_path}:delete"] }
- end
-
- it_behaves_like 'a deletable since registry 2.7' do
- let(:project) { current_project }
- end
- end
-
- context 'disallow to delete images' do
- let(:current_params) do
- { scopes: ["repository:#{current_project.full_path}:*"] }
- end
-
- it_behaves_like 'an inaccessible' do
- let(:project) { current_project }
- end
- end
-
- context 'for other projects' do
- context 'when pulling' do
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:pull"] }
- end
-
- context 'allow for public' do
- let_it_be(:project) { create(:project, :public) }
-
- it_behaves_like 'a pullable'
- it_behaves_like 'not a container repository factory'
- end
-
- shared_examples 'pullable for being team member' do
- context 'when you are not member' do
- it_behaves_like 'an inaccessible'
- it_behaves_like 'not a container repository factory'
- end
-
- context 'when you are member' do
- before_all do
- project.add_developer(current_user)
- end
-
- it_behaves_like 'a pullable'
- it_behaves_like 'not a container repository factory'
- end
-
- context 'when you are owner' do
- let_it_be(:project) { create(:project, namespace: current_user.namespace) }
-
- it_behaves_like 'a pullable'
- it_behaves_like 'not a container repository factory'
- end
- end
-
- context 'for private' do
- let_it_be(:project) { create(:project, :private) }
-
- it_behaves_like 'pullable for being team member'
-
- context 'when you are admin' do
- let_it_be(:current_user) { create(:admin) }
-
- context 'when you are not member' do
- it_behaves_like 'an inaccessible'
- it_behaves_like 'not a container repository factory'
- end
-
- context 'when you are member' do
- before_all do
- project.add_developer(current_user)
- end
-
- it_behaves_like 'a pullable'
- it_behaves_like 'not a container repository factory'
- end
-
- context 'when you are owner' do
- let_it_be(:project) { create(:project, namespace: current_user.namespace) }
-
- it_behaves_like 'a pullable'
- it_behaves_like 'not a container repository factory'
- end
- end
- end
- end
-
- context 'when pushing' do
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:push"] }
- end
-
- context 'disallow for all' do
- context 'when you are member' do
- let_it_be(:project) { create(:project, :public) }
-
- before_all do
- project.add_developer(current_user)
- end
-
- it_behaves_like 'an inaccessible'
- it_behaves_like 'not a container repository factory'
- end
-
- context 'when you are owner' do
- let_it_be(:project) { create(:project, :public, namespace: current_user.namespace) }
-
- it_behaves_like 'an inaccessible'
- it_behaves_like 'not a container repository factory'
- end
- end
- end
- end
-
- context 'for project without container registry' do
- let_it_be(:project) { create(:project, :public, container_registry_enabled: false) }
-
- before do
- project.update!(container_registry_enabled: false)
- end
-
- context 'disallow when pulling' do
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:pull"] }
- end
-
- it_behaves_like 'an inaccessible'
- it_behaves_like 'not a container repository factory'
- end
- end
-
- context 'for project that disables repository' do
- let_it_be(:project) { create(:project, :public, :repository_disabled) }
-
- context 'disallow when pulling' do
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:pull"] }
- end
-
- it_behaves_like 'an inaccessible'
- it_behaves_like 'not a container repository factory'
- end
- end
- end
-
- context 'registry catalog browsing authorized as admin' do
- let_it_be(:current_user) { create(:user, :admin) }
- let_it_be(:project) { create(:project, :public) }
-
- let(:current_params) do
- { scopes: ["registry:catalog:*"] }
- end
-
- it_behaves_like 'a browsable'
- end
-
- context 'support for multiple scopes' do
- let_it_be(:internal_project) { create(:project, :internal) }
- let_it_be(:private_project) { create(:project, :private) }
-
- let(:current_params) do
- {
- scopes: [
- "repository:#{internal_project.full_path}:pull",
- "repository:#{private_project.full_path}:pull"
- ]
- }
- end
-
- context 'user has access to all projects' do
- let_it_be(:current_user) { create(:user, :admin) }
-
- before do
- enable_admin_mode!(current_user)
- end
-
- it_behaves_like 'a browsable' do
- let(:access) do
- [
- { 'type' => 'repository',
- 'name' => internal_project.full_path,
- 'actions' => ['pull'] },
- { 'type' => 'repository',
- 'name' => private_project.full_path,
- 'actions' => ['pull'] }
- ]
- end
- end
- end
-
- context 'user only has access to internal project' do
- let_it_be(:current_user) { create(:user) }
-
- it_behaves_like 'a browsable' do
- let(:access) do
- [
- { 'type' => 'repository',
- 'name' => internal_project.full_path,
- 'actions' => ['pull'] }
- ]
- end
- end
- end
-
- context 'anonymous access is rejected' do
- let(:current_user) { nil }
-
- it_behaves_like 'a forbidden'
- end
- end
-
- context 'unauthorized' do
- context 'disallow to use scope-less authentication' do
- it_behaves_like 'a forbidden'
- it_behaves_like 'not a container repository factory'
- end
-
- context 'for invalid scope' do
- let(:current_params) do
- { scopes: ['invalid:aa:bb'] }
- end
-
- it_behaves_like 'a forbidden'
- it_behaves_like 'not a container repository factory'
- end
-
- context 'for private project' do
- let_it_be(:project) { create(:project, :private) }
-
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:pull"] }
- end
-
- it_behaves_like 'a forbidden'
- end
-
- context 'for public project' do
- let_it_be(:project) { create(:project, :public) }
-
- context 'when pulling and pushing' do
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:pull,push"] }
- end
-
- it_behaves_like 'a pullable'
- it_behaves_like 'not a container repository factory'
- end
-
- context 'when pushing' do
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:push"] }
- end
-
- it_behaves_like 'a forbidden'
- it_behaves_like 'not a container repository factory'
- end
- end
-
- context 'for registry catalog' do
- let(:current_params) do
- { scopes: ["registry:catalog:*"] }
- end
-
- it_behaves_like 'a forbidden'
- it_behaves_like 'not a container repository factory'
- end
- end
-
- context 'for deploy tokens' do
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:pull"] }
- end
-
- context 'when deploy token has read and write registry as scopes' do
- let(:current_user) { create(:deploy_token, write_registry: true, projects: [project]) }
-
- shared_examples 'able to login' do
- context 'registry provides read_container_image authentication_abilities' do
- let(:current_params) { {} }
- let(:authentication_abilities) { [:read_container_image] }
-
- it_behaves_like 'an authenticated'
- end
- end
-
- context 'for public project' do
- let_it_be(:project) { create(:project, :public) }
-
- context 'when pulling' do
- it_behaves_like 'a pullable'
- end
-
- context 'when pushing' do
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:push"] }
- end
-
- it_behaves_like 'a pushable'
- end
-
- it_behaves_like 'able to login'
- end
-
- context 'for internal project' do
- let_it_be(:project) { create(:project, :internal) }
-
- context 'when pulling' do
- it_behaves_like 'a pullable'
- end
-
- context 'when pushing' do
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:push"] }
- end
-
- it_behaves_like 'a pushable'
- end
-
- it_behaves_like 'able to login'
- end
-
- context 'for private project' do
- let_it_be(:project) { create(:project, :private) }
-
- context 'when pulling' do
- it_behaves_like 'a pullable'
- end
-
- context 'when pushing' do
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:push"] }
- end
-
- it_behaves_like 'a pushable'
- end
-
- it_behaves_like 'able to login'
- end
- end
-
- context 'when deploy token does not have read_registry scope' do
- let(:current_user) { create(:deploy_token, projects: [project], read_registry: false) }
-
- shared_examples 'unable to login' do
- context 'registry provides no container authentication_abilities' do
- let(:current_params) { {} }
- let(:authentication_abilities) { [] }
-
- it_behaves_like 'a forbidden'
- end
-
- context 'registry provides inapplicable container authentication_abilities' do
- let(:current_params) { {} }
- let(:authentication_abilities) { [:download_code] }
-
- it_behaves_like 'a forbidden'
- end
- end
-
- context 'for public project' do
- let_it_be(:project) { create(:project, :public) }
-
- context 'when pulling' do
- it_behaves_like 'a pullable'
- end
-
- it_behaves_like 'unable to login'
- end
-
- context 'for internal project' do
- let_it_be(:project) { create(:project, :internal) }
-
- context 'when pulling' do
- it_behaves_like 'an inaccessible'
- end
-
- it_behaves_like 'unable to login'
- end
-
- context 'for private project' do
- let_it_be(:project) { create(:project, :internal) }
-
- context 'when pulling' do
- it_behaves_like 'an inaccessible'
- end
-
- context 'when logging in' do
- let(:current_params) { {} }
- let(:authentication_abilities) { [] }
-
- it_behaves_like 'a forbidden'
- end
-
- it_behaves_like 'unable to login'
- end
- end
-
- context 'when deploy token is not related to the project' do
- let_it_be(:current_user) { create(:deploy_token, read_registry: false) }
-
- context 'for public project' do
- let_it_be(:project) { create(:project, :public) }
-
- context 'when pulling' do
- it_behaves_like 'a pullable'
- end
- end
-
- context 'for internal project' do
- let_it_be(:project) { create(:project, :internal) }
-
- context 'when pulling' do
- it_behaves_like 'an inaccessible'
- end
- end
-
- context 'for private project' do
- let_it_be(:project) { create(:project, :internal) }
-
- context 'when pulling' do
- it_behaves_like 'an inaccessible'
- end
- end
- end
-
- context 'when deploy token has been revoked' do
- let(:current_user) { create(:deploy_token, :revoked, projects: [project]) }
-
- context 'for public project' do
- let_it_be(:project) { create(:project, :public) }
-
- it_behaves_like 'a pullable'
- end
-
- context 'for internal project' do
- let_it_be(:project) { create(:project, :internal) }
-
- it_behaves_like 'an inaccessible'
- end
-
- context 'for private project' do
- let_it_be(:project) { create(:project, :internal) }
-
- it_behaves_like 'an inaccessible'
- end
- end
- end
-
- context 'user authorization' do
- let_it_be(:current_user) { create(:user) }
-
- context 'with multiple scopes' do
- let_it_be(:project) { create(:project) }
-
- context 'allow developer to push images' do
- before_all do
- project.add_developer(current_user)
- end
-
- let(:current_params) do
- { scopes: ["repository:#{project.full_path}:push"] }
- end
-
- it_behaves_like 'a pushable'
- it_behaves_like 'container repository factory'
- end
- end
- end
+ it_behaves_like 'a container registry auth service'
end
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index f54153a72f8..01571277a1d 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -168,6 +168,11 @@ module TestEnv
version: Gitlab::GitalyClient.expected_server_version,
task: "gitlab:gitaly:install[#{install_gitaly_args}]") do
Gitlab::SetupHelper::Gitaly.create_configuration(gitaly_dir, { 'default' => repos_path }, force: true)
+ Gitlab::SetupHelper::Gitaly.create_configuration(
+ gitaly_dir,
+ { 'default' => repos_path }, force: true,
+ options: { gitaly_socket: "gitaly2.socket", config_filename: "gitaly2.config.toml" }
+ )
Gitlab::SetupHelper::Praefect.create_configuration(gitaly_dir, { 'praefect' => repos_path }, force: true)
end
@@ -283,7 +288,7 @@ module TestEnv
host = "[#{host}]" if host.include?(':')
listen_addr = [host, port].join(':')
- config_path = Gitlab::SetupHelper::Workhorse.get_config_path(workhorse_dir)
+ config_path = Gitlab::SetupHelper::Workhorse.get_config_path(workhorse_dir, {})
# This should be set up in setup_workhorse, but since
# component_needs_update? only checks that versions are consistent,
diff --git a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb
new file mode 100644
index 00000000000..ba176b616c3
--- /dev/null
+++ b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb
@@ -0,0 +1,1006 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'container registry auth service context' do
+ let(:current_project) { nil }
+ let(:current_user) { nil }
+ let(:current_params) { {} }
+ let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) }
+ let(:payload) { JWT.decode(subject[:token], rsa_key, true, { algorithm: 'RS256' }).first }
+
+ let(:authentication_abilities) do
+ [:read_container_image, :create_container_image, :admin_container_image]
+ end
+
+ let(:log_data) { { message: 'Denied container registry permissions' } }
+
+ subject do
+ described_class.new(current_project, current_user, current_params)
+ .execute(authentication_abilities: authentication_abilities)
+ end
+
+ before do
+ allow(Gitlab.config.registry).to receive_messages(enabled: true, issuer: 'rspec', key: nil)
+ allow_next_instance_of(JSONWebToken::RSAToken) do |instance|
+ allow(instance).to receive(:key).and_return(rsa_key)
+ end
+ end
+end
+
+RSpec.shared_examples 'an authenticated' do
+ it { is_expected.to include(:token) }
+ it { expect(payload).to include('access') }
+end
+
+RSpec.shared_examples 'a valid token' do
+ it { is_expected.to include(:token) }
+ it { expect(payload).to include('access') }
+
+ context 'a expirable' do
+ let(:expires_at) { Time.zone.at(payload['exp']) }
+ let(:expire_delay) { 10 }
+
+ context 'for default configuration' do
+ it { expect(expires_at).not_to be_within(2.seconds).of(Time.current + expire_delay.minutes) }
+ end
+
+ context 'for changed configuration' do
+ before do
+ stub_application_setting(container_registry_token_expire_delay: expire_delay)
+ end
+
+ it { expect(expires_at).to be_within(2.seconds).of(Time.current + expire_delay.minutes) }
+ end
+ end
+end
+
+RSpec.shared_examples 'a browsable' do
+ let(:access) do
+ [{ 'type' => 'registry',
+ 'name' => 'catalog',
+ 'actions' => ['*'] }]
+ end
+
+ it_behaves_like 'a valid token'
+ it_behaves_like 'not a container repository factory'
+
+ it 'has the correct scope' do
+ expect(payload).to include('access' => access)
+ end
+end
+
+RSpec.shared_examples 'an accessible' do
+ let(:access) do
+ [{ 'type' => 'repository',
+ 'name' => project.full_path,
+ 'actions' => actions }]
+ end
+
+ it_behaves_like 'a valid token'
+
+ it 'has the correct scope' do
+ expect(payload).to include('access' => access)
+ end
+end
+
+RSpec.shared_examples 'an inaccessible' do
+ it_behaves_like 'a valid token'
+ it { expect(payload).to include('access' => []) }
+end
+
+RSpec.shared_examples 'a deletable' do
+ it_behaves_like 'an accessible' do
+ let(:actions) { ['*'] }
+ end
+end
+
+RSpec.shared_examples 'a deletable since registry 2.7' do
+ it_behaves_like 'an accessible' do
+ let(:actions) { ['delete'] }
+ end
+end
+
+RSpec.shared_examples 'a pullable' do
+ it_behaves_like 'an accessible' do
+ let(:actions) { ['pull'] }
+ end
+end
+
+RSpec.shared_examples 'a pushable' do
+ it_behaves_like 'an accessible' do
+ let(:actions) { ['push'] }
+ end
+end
+
+RSpec.shared_examples 'a pullable and pushable' do
+ it_behaves_like 'an accessible' do
+ let(:actions) { %w(pull push) }
+ end
+end
+
+RSpec.shared_examples 'a forbidden' do
+ it { is_expected.to include(http_status: 403) }
+ it { is_expected.not_to include(:token) }
+end
+
+RSpec.shared_examples 'container repository factory' do
+ it 'creates a new container repository resource' do
+ expect { subject }
+ .to change { project.container_repositories.count }.by(1)
+ end
+end
+
+RSpec.shared_examples 'not a container repository factory' do
+ it 'does not create a new container repository resource' do
+ expect { subject }.not_to change { ContainerRepository.count }
+ end
+end
+
+RSpec.shared_examples 'logs an auth warning' do |requested_actions|
+ let(:expected) do
+ {
+ scope_type: 'repository',
+ requested_project_path: project.full_path,
+ requested_actions: requested_actions,
+ authorized_actions: [],
+ user_id: current_user.id,
+ username: current_user.username
+ }
+ end
+
+ it do
+ expect(Gitlab::AuthLogger).to receive(:warn).with(expected.merge!(log_data))
+
+ subject
+ end
+end
+
+RSpec.shared_examples 'a container registry auth service' do
+ include_context 'container registry auth service context'
+
+ describe '#full_access_token' do
+ let_it_be(:project) { create(:project) }
+ let(:token) { described_class.full_access_token(project.full_path) }
+
+ subject { { token: token } }
+
+ it_behaves_like 'an accessible' do
+ let(:actions) { ['*'] }
+ end
+
+ it_behaves_like 'not a container repository factory'
+ end
+
+ describe '#pull_access_token' do
+ let_it_be(:project) { create(:project) }
+ let(:token) { described_class.pull_access_token(project.full_path) }
+
+ subject { { token: token } }
+
+ it_behaves_like 'an accessible' do
+ let(:actions) { ['pull'] }
+ end
+
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'user authorization' do
+ let_it_be(:current_user) { create(:user) }
+
+ context 'for registry catalog' do
+ let(:current_params) do
+ { scopes: ["registry:catalog:*"] }
+ end
+
+ context 'disallow browsing for users without GitLab admin rights' do
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+
+ context 'for private project' do
+ let_it_be(:project) { create(:project) }
+
+ context 'allow to use scope-less authentication' do
+ it_behaves_like 'a valid token'
+ end
+
+ context 'allow developer to push images' do
+ before_all do
+ project.add_developer(current_user)
+ end
+
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:push"] }
+ end
+
+ it_behaves_like 'a pushable'
+ it_behaves_like 'container repository factory'
+ end
+
+ context 'disallow developer to delete images' do
+ before_all do
+ project.add_developer(current_user)
+ end
+
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:*"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+
+ it_behaves_like 'logs an auth warning', ['*']
+ end
+
+ context 'disallow developer to delete images since registry 2.7' do
+ before_all do
+ project.add_developer(current_user)
+ end
+
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:delete"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'allow reporter to pull images' do
+ before_all do
+ project.add_reporter(current_user)
+ end
+
+ context 'when pulling from root level repository' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:pull"] }
+ end
+
+ it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+
+ context 'disallow reporter to delete images' do
+ before_all do
+ project.add_reporter(current_user)
+ end
+
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:*"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'disallow reporter to delete images since registry 2.7' do
+ before_all do
+ project.add_reporter(current_user)
+ end
+
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:delete"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'return a least of privileges' do
+ before_all do
+ project.add_reporter(current_user)
+ end
+
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:push,pull"] }
+ end
+
+ it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'disallow guest to pull or push images' do
+ before_all do
+ project.add_guest(current_user)
+ end
+
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:pull,push"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'disallow guest to delete images' do
+ before_all do
+ project.add_guest(current_user)
+ end
+
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:*"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'disallow guest to delete images since registry 2.7' do
+ before_all do
+ project.add_guest(current_user)
+ end
+
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:delete"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+
+ context 'for public project' do
+ let_it_be(:project) { create(:project, :public) }
+
+ context 'allow anyone to pull images' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:pull"] }
+ end
+
+ it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'disallow anyone to push images' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:push"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'disallow anyone to delete images' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:*"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'disallow anyone to delete images since registry 2.7' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:delete"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'when repository name is invalid' do
+ let(:current_params) do
+ { scopes: ['repository:invalid:push'] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+
+ context 'for internal project' do
+ let_it_be(:project) { create(:project, :internal) }
+
+ context 'for internal user' do
+ context 'allow anyone to pull images' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:pull"] }
+ end
+
+ it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'disallow anyone to push images' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:push"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'disallow anyone to delete images' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:*"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'disallow anyone to delete images since registry 2.7' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:delete"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+
+ context 'for external user' do
+ context 'disallow anyone to pull or push images' do
+ let_it_be(:current_user) { create(:user, external: true) }
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:pull,push"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'disallow anyone to delete images' do
+ let_it_be(:current_user) { create(:user, external: true) }
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:*"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'disallow anyone to delete images since registry 2.7' do
+ let_it_be(:current_user) { create(:user, external: true) }
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:delete"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+ end
+ end
+
+ context 'delete authorized as maintainer' do
+ let_it_be(:current_project) { create(:project) }
+ let_it_be(:current_user) { create(:user) }
+
+ let(:authentication_abilities) do
+ [:admin_container_image]
+ end
+
+ before_all do
+ current_project.add_maintainer(current_user)
+ end
+
+ it_behaves_like 'a valid token'
+
+ context 'allow to delete images' do
+ let(:current_params) do
+ { scopes: ["repository:#{current_project.full_path}:*"] }
+ end
+
+ it_behaves_like 'a deletable' do
+ let(:project) { current_project }
+ end
+ end
+
+ context 'allow to delete images since registry 2.7' do
+ let(:current_params) do
+ { scopes: ["repository:#{current_project.full_path}:delete"] }
+ end
+
+ it_behaves_like 'a deletable since registry 2.7' do
+ let(:project) { current_project }
+ end
+ end
+ end
+
+ context 'build authorized as user' do
+ let_it_be(:current_project) { create(:project) }
+ let_it_be(:current_user) { create(:user) }
+
+ let(:authentication_abilities) do
+ [:build_read_container_image, :build_create_container_image, :build_destroy_container_image]
+ end
+
+ before_all do
+ current_project.add_developer(current_user)
+ end
+
+ context 'allow to use offline_token' do
+ let(:current_params) do
+ { offline_token: true }
+ end
+
+ it_behaves_like 'an authenticated'
+ end
+
+ it_behaves_like 'a valid token'
+
+ context 'allow to pull and push images' do
+ let(:current_params) do
+ { scopes: ["repository:#{current_project.full_path}:pull,push"] }
+ end
+
+ it_behaves_like 'a pullable and pushable' do
+ let(:project) { current_project }
+ end
+
+ it_behaves_like 'container repository factory' do
+ let(:project) { current_project }
+ end
+ end
+
+ context 'allow to delete images since registry 2.7' do
+ let(:current_params) do
+ { scopes: ["repository:#{current_project.full_path}:delete"] }
+ end
+
+ it_behaves_like 'a deletable since registry 2.7' do
+ let(:project) { current_project }
+ end
+ end
+
+ context 'disallow to delete images' do
+ let(:current_params) do
+ { scopes: ["repository:#{current_project.full_path}:*"] }
+ end
+
+ it_behaves_like 'an inaccessible' do
+ let(:project) { current_project }
+ end
+ end
+
+ context 'for other projects' do
+ context 'when pulling' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:pull"] }
+ end
+
+ context 'allow for public' do
+ let_it_be(:project) { create(:project, :public) }
+
+ it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ shared_examples 'pullable for being team member' do
+ context 'when you are not member' do
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'when you are member' do
+ before_all do
+ project.add_developer(current_user)
+ end
+
+ it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'when you are owner' do
+ let_it_be(:project) { create(:project, namespace: current_user.namespace) }
+
+ it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+
+ context 'for private' do
+ let_it_be(:project) { create(:project, :private) }
+
+ it_behaves_like 'pullable for being team member'
+
+ context 'when you are admin' do
+ let_it_be(:current_user) { create(:admin) }
+
+ context 'when you are not member' do
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'when you are member' do
+ before_all do
+ project.add_developer(current_user)
+ end
+
+ it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'when you are owner' do
+ let_it_be(:project) { create(:project, namespace: current_user.namespace) }
+
+ it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+ end
+ end
+
+ context 'when pushing' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:push"] }
+ end
+
+ context 'disallow for all' do
+ context 'when you are member' do
+ let_it_be(:project) { create(:project, :public) }
+
+ before_all do
+ project.add_developer(current_user)
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'when you are owner' do
+ let_it_be(:project) { create(:project, :public, namespace: current_user.namespace) }
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+ end
+ end
+
+ context 'for project without container registry' do
+ let_it_be(:project) { create(:project, :public, container_registry_enabled: false) }
+
+ before do
+ project.update!(container_registry_enabled: false)
+ end
+
+ context 'disallow when pulling' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:pull"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+
+ context 'for project that disables repository' do
+ let_it_be(:project) { create(:project, :public, :repository_disabled) }
+
+ context 'disallow when pulling' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:pull"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+ end
+
+ context 'registry catalog browsing authorized as admin' do
+ let_it_be(:current_user) { create(:user, :admin) }
+ let_it_be(:project) { create(:project, :public) }
+
+ let(:current_params) do
+ { scopes: ["registry:catalog:*"] }
+ end
+
+ it_behaves_like 'a browsable'
+ end
+
+ context 'support for multiple scopes' do
+ let_it_be(:internal_project) { create(:project, :internal) }
+ let_it_be(:private_project) { create(:project, :private) }
+
+ let(:current_params) do
+ {
+ scopes: [
+ "repository:#{internal_project.full_path}:pull",
+ "repository:#{private_project.full_path}:pull"
+ ]
+ }
+ end
+
+ context 'user has access to all projects' do
+ let_it_be(:current_user) { create(:user, :admin) }
+
+ before do
+ enable_admin_mode!(current_user)
+ end
+
+ it_behaves_like 'a browsable' do
+ let(:access) do
+ [
+ { 'type' => 'repository',
+ 'name' => internal_project.full_path,
+ 'actions' => ['pull'] },
+ { 'type' => 'repository',
+ 'name' => private_project.full_path,
+ 'actions' => ['pull'] }
+ ]
+ end
+ end
+ end
+
+ context 'user only has access to internal project' do
+ let_it_be(:current_user) { create(:user) }
+
+ it_behaves_like 'a browsable' do
+ let(:access) do
+ [
+ { 'type' => 'repository',
+ 'name' => internal_project.full_path,
+ 'actions' => ['pull'] }
+ ]
+ end
+ end
+ end
+
+ context 'anonymous access is rejected' do
+ let(:current_user) { nil }
+
+ it_behaves_like 'a forbidden'
+ end
+ end
+
+ context 'unauthorized' do
+ context 'disallow to use scope-less authentication' do
+ it_behaves_like 'a forbidden'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'for invalid scope' do
+ let(:current_params) do
+ { scopes: ['invalid:aa:bb'] }
+ end
+
+ it_behaves_like 'a forbidden'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'for private project' do
+ let_it_be(:project) { create(:project, :private) }
+
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:pull"] }
+ end
+
+ it_behaves_like 'a forbidden'
+ end
+
+ context 'for public project' do
+ let_it_be(:project) { create(:project, :public) }
+
+ context 'when pulling and pushing' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:pull,push"] }
+ end
+
+ it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'when pushing' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:push"] }
+ end
+
+ it_behaves_like 'a forbidden'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+
+ context 'for registry catalog' do
+ let(:current_params) do
+ { scopes: ["registry:catalog:*"] }
+ end
+
+ it_behaves_like 'a forbidden'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+
+ context 'for deploy tokens' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:pull"] }
+ end
+
+ context 'when deploy token has read and write registry as scopes' do
+ let(:current_user) { create(:deploy_token, write_registry: true, projects: [project]) }
+
+ shared_examples 'able to login' do
+ context 'registry provides read_container_image authentication_abilities' do
+ let(:current_params) { {} }
+ let(:authentication_abilities) { [:read_container_image] }
+
+ it_behaves_like 'an authenticated'
+ end
+ end
+
+ context 'for public project' do
+ let_it_be(:project) { create(:project, :public) }
+
+ context 'when pulling' do
+ it_behaves_like 'a pullable'
+ end
+
+ context 'when pushing' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:push"] }
+ end
+
+ it_behaves_like 'a pushable'
+ end
+
+ it_behaves_like 'able to login'
+ end
+
+ context 'for internal project' do
+ let_it_be(:project) { create(:project, :internal) }
+
+ context 'when pulling' do
+ it_behaves_like 'a pullable'
+ end
+
+ context 'when pushing' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:push"] }
+ end
+
+ it_behaves_like 'a pushable'
+ end
+
+ it_behaves_like 'able to login'
+ end
+
+ context 'for private project' do
+ let_it_be(:project) { create(:project, :private) }
+
+ context 'when pulling' do
+ it_behaves_like 'a pullable'
+ end
+
+ context 'when pushing' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:push"] }
+ end
+
+ it_behaves_like 'a pushable'
+ end
+
+ it_behaves_like 'able to login'
+ end
+ end
+
+ context 'when deploy token does not have read_registry scope' do
+ let(:current_user) { create(:deploy_token, projects: [project], read_registry: false) }
+
+ shared_examples 'unable to login' do
+ context 'registry provides no container authentication_abilities' do
+ let(:current_params) { {} }
+ let(:authentication_abilities) { [] }
+
+ it_behaves_like 'a forbidden'
+ end
+
+ context 'registry provides inapplicable container authentication_abilities' do
+ let(:current_params) { {} }
+ let(:authentication_abilities) { [:download_code] }
+
+ it_behaves_like 'a forbidden'
+ end
+ end
+
+ context 'for public project' do
+ let_it_be(:project) { create(:project, :public) }
+
+ context 'when pulling' do
+ it_behaves_like 'a pullable'
+ end
+
+ it_behaves_like 'unable to login'
+ end
+
+ context 'for internal project' do
+ let_it_be(:project) { create(:project, :internal) }
+
+ context 'when pulling' do
+ it_behaves_like 'an inaccessible'
+ end
+
+ it_behaves_like 'unable to login'
+ end
+
+ context 'for private project' do
+ let_it_be(:project) { create(:project, :internal) }
+
+ context 'when pulling' do
+ it_behaves_like 'an inaccessible'
+ end
+
+ context 'when logging in' do
+ let(:current_params) { {} }
+ let(:authentication_abilities) { [] }
+
+ it_behaves_like 'a forbidden'
+ end
+
+ it_behaves_like 'unable to login'
+ end
+ end
+
+ context 'when deploy token is not related to the project' do
+ let_it_be(:current_user) { create(:deploy_token, read_registry: false) }
+
+ context 'for public project' do
+ let_it_be(:project) { create(:project, :public) }
+
+ context 'when pulling' do
+ it_behaves_like 'a pullable'
+ end
+ end
+
+ context 'for internal project' do
+ let_it_be(:project) { create(:project, :internal) }
+
+ context 'when pulling' do
+ it_behaves_like 'an inaccessible'
+ end
+ end
+
+ context 'for private project' do
+ let_it_be(:project) { create(:project, :internal) }
+
+ context 'when pulling' do
+ it_behaves_like 'an inaccessible'
+ end
+ end
+ end
+
+ context 'when deploy token has been revoked' do
+ let(:current_user) { create(:deploy_token, :revoked, projects: [project]) }
+
+ context 'for public project' do
+ let_it_be(:project) { create(:project, :public) }
+
+ it_behaves_like 'a pullable'
+ end
+
+ context 'for internal project' do
+ let_it_be(:project) { create(:project, :internal) }
+
+ it_behaves_like 'an inaccessible'
+ end
+
+ context 'for private project' do
+ let_it_be(:project) { create(:project, :internal) }
+
+ it_behaves_like 'an inaccessible'
+ end
+ end
+ end
+
+ context 'user authorization' do
+ let_it_be(:current_user) { create(:user) }
+
+ context 'with multiple scopes' do
+ let_it_be(:project) { create(:project) }
+
+ context 'allow developer to push images' do
+ before_all do
+ project.add_developer(current_user)
+ end
+
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:push"] }
+ end
+
+ it_behaves_like 'a pushable'
+ it_behaves_like 'container repository factory'
+ end
+ end
+ end
+end