From 6c46be4823292f25fa1fe28d1c899816c727893d Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 17 Dec 2020 03:10:36 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- ...ntainer_registry_authentication_service.rb | 12 +- config/gitlab.yml.example | 2 +- doc/.vale/gitlab/SubstitutionSuggestions.yml | 3 +- doc/ci/caching/index.md | 31 + .../compliance/license_compliance/index.md | 63 +- doc/user/project/web_ide/index.md | 2 +- lib/gitlab/setup_helper.rb | 45 +- scripts/gitaly-test-build | 2 + scripts/gitaly-test-spawn | 1 + scripts/gitaly_test.rb | 17 +- ...er_registry_authentication_service_spec.rb | 990 +--------------- spec/support/helpers/test_env.rb | 7 +- ...r_registry_auth_service_shared_examples.rb | 1006 +++++++++++++++++ 13 files changed, 1134 insertions(+), 1047 deletions(-) create mode 100644 spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb 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. +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 | 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)| +| 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