Commit 6309e13b authored by Thomas Watts's avatar Thomas Watts

Allow admins to limit registration of project and group runners

In some instances, we would like to prevent project and group users from registering runners.
Admins can now toggle this preference through the Admin Area > Settings > CI/CD page.
The specific and group runner registration views and POST 'runners/' API have been updated accordingly.

Changelog: added
parent c044eeae
......@@ -207,6 +207,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
params[:application_setting][:import_sources]&.delete("")
params[:application_setting][:valid_runner_registrars]&.delete("")
params[:application_setting][:restricted_visibility_levels]&.delete("")
if params[:application_setting].key?(:required_instance_ci_template)
......@@ -245,7 +246,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
disabled_oauth_sign_in_sources: [],
import_sources: [],
restricted_visibility_levels: [],
repository_storages_weighted: {}
repository_storages_weighted: {},
valid_runner_registrars: []
]
end
......
......@@ -437,6 +437,10 @@ module ApplicationSettingsHelper
Feature.enabled?(:help_page_documentation_redirect)
end
def valid_runner_registrars
Gitlab::CurrentSettings.valid_runner_registrars
end
def signup_enabled?
!!Gitlab::CurrentSettings.signup_enabled
end
......
......@@ -372,6 +372,8 @@ class ApplicationSetting < ApplicationRecord
end
end
validate :check_valid_runner_registrars
validate :terms_exist, if: :enforce_terms?
validates :external_authorization_service_default_label,
......
......@@ -15,6 +15,7 @@ module ApplicationSettingImplementation
# forbidden.
FORBIDDEN_KEY_VALUE = KeyRestrictionValidator::FORBIDDEN
SUPPORTED_KEY_TYPES = %i[rsa dsa ecdsa ed25519].freeze
VALID_RUNNER_REGISTRAR_TYPES = %w(project group).freeze
DEFAULT_PROTECTED_PATHS = [
'/users/password',
......@@ -186,6 +187,7 @@ module ApplicationSettingImplementation
user_default_external: false,
user_default_internal_regex: nil,
user_show_add_ssh_key_message: true,
valid_runner_registrars: VALID_RUNNER_REGISTRAR_TYPES,
wiki_page_max_content_bytes: 50.megabytes,
container_registry_delete_tags_service_timeout: 250,
container_registry_expiration_policies_worker_capacity: 0,
......@@ -507,6 +509,17 @@ module ApplicationSettingImplementation
end
end
def check_valid_runner_registrars
valid = valid_runner_registrar_combinations.include?(valid_runner_registrars)
errors.add(:valid_runner_registrars, _("%{value} is not included in the list") % { value: valid_runner_registrars }) unless valid
end
def valid_runner_registrar_combinations
0.upto(VALID_RUNNER_REGISTRAR_TYPES.size).flat_map do |n|
VALID_RUNNER_REGISTRAR_TYPES.permutation(n).to_a
end
end
def terms_exist
return unless enforce_terms?
......
= form_for @application_setting, url: ci_cd_admin_application_settings_path(anchor: 'js-runner-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
= hidden_field_tag "application_setting[valid_runner_registrars][]", nil
- ApplicationSetting::VALID_RUNNER_REGISTRAR_TYPES.each do |type|
.form-check
= f.check_box(:valid_runner_registrars, { multiple: true, checked: valid_runner_registrars.include?(type), class: 'form-check-input' }, type, nil)
= f.label :valid_runner_registrars, class: 'form-check-label' do
= s_("Runners|Members of the %{type} can register runners") % { type: type }
%span.form-text.gl-text-gray-600
= _('If no options are selected, only administrators can register runners.')
= link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'runner-registration'), target: '_blank', rel: 'noopener noreferrer'
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
......@@ -38,3 +38,13 @@
= _('Various container registry settings.')
.settings-content
= render 'registry'
- if Feature.enabled?(:runner_registration_control)
%section.settings.as-runner.no-animate#js-runner-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
= s_('Runners|Runner registration')
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? 'Collapse' : 'Expand'
.settings-content
= render 'runner_registrars_form'
......@@ -9,19 +9,24 @@
-# Proper policies should be implemented per
-# https://gitlab.com/gitlab-org/gitlab-foss/issues/45894
- if can?(current_user, :admin_pipeline, @group)
= render partial: 'ci/runner/how_to_setup_runner_automatically',
locals: { type: 'group',
clusters_path: group_clusters_path(@group) }
- if params[:ci_runner_templates]
.bs-callout.help-callout
- if can?(current_user, :admin_pipeline, @group) && valid_runner_registrars.include?('group')
= render partial: 'ci/runner/how_to_setup_runner_automatically',
locals: { type: 'group',
clusters_path: group_clusters_path(@group) }
- if params[:ci_runner_templates]
%hr
= render partial: 'ci/runner/setup_runner_in_aws',
locals: { registration_token: @group.runners_token }
%hr
= render partial: 'ci/runner/setup_runner_in_aws',
locals: { registration_token: @group.runners_token }
%hr
= render partial: 'ci/runner/how_to_setup_runner',
locals: { registration_token: @group.runners_token,
type: 'group',
reset_token_url: reset_registration_token_group_settings_ci_cd_path,
project_path: '',
group_path: @group.full_path }
%br
= render partial: 'ci/runner/how_to_setup_runner',
locals: { registration_token: @group.runners_token,
type: 'group',
reset_token_url: reset_registration_token_group_settings_ci_cd_path,
project_path: '',
group_path: @group.full_path }
%br
- else
= _('Please contact an admin to register runners.')
= link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'runner-registration'), target: '_blank', rel: 'noopener noreferrer'
......@@ -2,22 +2,26 @@
= _('Specific runners')
.bs-callout.help-callout
= _('These runners are specific to this project.')
%hr
= render partial: 'ci/runner/how_to_setup_runner_automatically',
locals: { type: 'specific',
clusters_path: project_clusters_path(@project) }
- if params[:ci_runner_templates]
- if valid_runner_registrars.include?('project')
= _('These runners are specific to this project.')
%hr
= render partial: 'ci/runner/setup_runner_in_aws',
locals: { registration_token: @project.runners_token }
%hr
= render partial: 'ci/runner/how_to_setup_runner',
locals: { registration_token: @project.runners_token,
type: 'specific',
reset_token_url: reset_registration_token_namespace_project_settings_ci_cd_path,
project_path: @project.path_with_namespace,
group_path: '' }
= render partial: 'ci/runner/how_to_setup_runner_automatically',
locals: { type: 'specific',
clusters_path: project_clusters_path(@project) }
- if params[:ci_runner_templates]
%hr
= render partial: 'ci/runner/setup_runner_in_aws',
locals: { registration_token: @project.runners_token }
%hr
= render partial: 'ci/runner/how_to_setup_runner',
locals: { registration_token: @project.runners_token,
type: 'specific',
reset_token_url: reset_registration_token_namespace_project_settings_ci_cd_path,
project_path: @project.path_with_namespace,
group_path: '' }
- else
= _('Please contact an admin to register runners.')
= link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'runner-registration'), target: '_blank', rel: 'noopener noreferrer'
%hr
......
---
name: runner_registration_control
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61407
rollout_issue_url:
milestone: '14.1'
type: development
group: group::runner
default_enabled: false
# frozen_string_literal: true
class AddValidRunnerRegistrars < ActiveRecord::Migration[6.0]
def change
add_column :application_settings, :valid_runner_registrars, :string, array: true, default: %w(project group)
end
end
e915378e1ebb78b528abfecda55cdc52a690d982e4377876b818197b3134c09a
\ No newline at end of file
......@@ -9521,6 +9521,7 @@ CREATE TABLE application_settings (
encrypted_elasticsearch_password_iv bytea,
diff_max_lines integer DEFAULT 50000 NOT NULL,
diff_max_files integer DEFAULT 1000 NOT NULL,
valid_runner_registrars character varying[] DEFAULT '{project,group}'::character varying[],
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
CONSTRAINT app_settings_ext_pipeline_validation_service_url_text_limit CHECK ((char_length(external_pipeline_validation_service_url) <= 255)),
CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)),
......@@ -280,6 +280,45 @@ To set the maximum file size:
1. Enter the maximum file size, in bytes.
1. Click **Save size limits**.
## Runner registration
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/22225) in GitLab 14.1.
> - [Deployed behind a feature flag](../../feature_flags.md), disabled by default.
> - Disabled on GitLab.com.
> - Not recommended for production use.
> - To use in GitLab self-managed instances, ask a GitLab administrator to enable it. **(FREE SELF)**
GitLab administrators can adjust who is allowed to register runners, by showing and hiding areas of the UI.
By default, all members of a project and group are able to register runners.
To change this:
1. On the top bar, select **Menu >** **{admin}** **Admin**.
1. Go to **Settings > CI/CD**.
1. Expand the **Runner registration** section.
1. Select the desired options.
1. Click **Save changes**.
When the registration sections are hidden in the UI, members of the project or group that need to register runners must contact the administrators.
This feature is currently behind a feature flag.
To enable it:
**In Omnibus installations:**
1. Enter the Rails console:
```shell
sudo gitlab-rails console
```
1. Flip the switch and enable the feature flag:
```ruby
Feature.enable(:runner_registration_control)
```
## Troubleshooting
### 413 Request Entity Too Large
......
......@@ -34,10 +34,10 @@ module API
if runner_registration_token_valid?
# Create shared runner. Requires admin access
attributes.merge(runner_type: :instance_type)
elsif @project = Project.find_by_runners_token(params[:token])
elsif runner_registrar_valid?('project') && @project = Project.find_by_runners_token(params[:token])
# Create a specific runner for the project
attributes.merge(runner_type: :project_type, projects: [@project])
elsif @group = Group.find_by_runners_token(params[:token])
elsif runner_registrar_valid?('group') && @group = Group.find_by_runners_token(params[:token])
# Create a specific runner for the group
attributes.merge(runner_type: :group_type, groups: [@group])
else
......
......@@ -14,6 +14,10 @@ module API
ActiveSupport::SecurityUtils.secure_compare(params[:token], Gitlab::CurrentSettings.runners_registration_token)
end
def runner_registrar_valid?(type)
Feature.disabled?(:runner_registration_control) || Gitlab::CurrentSettings.valid_runner_registrars.include?(type)
end
def authenticate_runner!
forbidden! unless current_runner
......
......@@ -1002,6 +1002,9 @@ msgstr ""
msgid "%{user} created an issue: %{issue_link}"
msgstr ""
msgid "%{value} is not included in the list"
msgstr ""
msgid "%{value} s"
msgstr ""
......@@ -16562,6 +16565,9 @@ msgstr ""
msgid "If enabled, only protected branches will be mirrored."
msgstr ""
msgid "If no options are selected, only administrators can register runners."
msgstr ""
msgid "If the YouTube URL is https://www.youtube.com/watch?v=0t1DgySidms then the video ID is %{id}"
msgstr ""
......@@ -24539,6 +24545,9 @@ msgstr ""
msgid "Please complete your profile with email address"
msgstr ""
msgid "Please contact an admin to register runners."
msgstr ""
msgid "Please contact your GitLab administrator if you think this is an error."
msgstr ""
......@@ -28282,6 +28291,9 @@ msgstr ""
msgid "Runners|Maximum job timeout"
msgstr ""
msgid "Runners|Members of the %{type} can register runners"
msgstr ""
msgid "Runners|Name"
msgstr ""
......@@ -28330,6 +28342,9 @@ msgstr ""
msgid "Runners|Runner is paused, last contact was %{runner_contact} ago"
msgstr ""
msgid "Runners|Runner registration"
msgstr ""
msgid "Runners|Shared runners are available to every project in a GitLab instance. If you want a runner to build only specific projects, restrict the project in the table below. After you restrict a runner to a project, you cannot change it back to a shared runner."
msgstr ""
......
......@@ -171,6 +171,13 @@ RSpec.describe Admin::ApplicationSettingsController, :do_not_mock_admin_mode_set
expect(ApplicationSetting.current.admin_mode).to be(true)
end
it 'updates valid_runner_registrars setting' do
put :update, params: { application_setting: { valid_runner_registrars: ['project', ''] } }
expect(response).to redirect_to(general_admin_application_settings_path)
expect(ApplicationSetting.current.valid_runner_registrars).to eq(['project'])
end
context "personal access token prefix settings" do
let(:application_settings) { ApplicationSetting.current }
......
......@@ -370,6 +370,43 @@ RSpec.describe 'Admin updates settings' do
expect(page).to have_content "Application settings saved successfully"
end
context 'Runner Registration' do
context 'when feature is enabled' do
before do
stub_feature_flags(runner_registration_control: true)
end
it 'allows admins to control who has access to register runners' do
visit ci_cd_admin_application_settings_path
expect(current_settings.valid_runner_registrars).to eq(ApplicationSetting::VALID_RUNNER_REGISTRAR_TYPES)
page.within('.as-runner') do
find_all('.form-check-input').each(&:click)
click_button 'Save changes'
end
expect(current_settings.valid_runner_registrars).to eq([])
expect(page).to have_content "Application settings saved successfully"
end
end
context 'when feature is disabled' do
before do
stub_feature_flags(runner_registration_control: false)
end
it 'does not allow admins to control who has access to register runners' do
visit ci_cd_admin_application_settings_path
expect(current_settings.valid_runner_registrars).to eq(ApplicationSetting::VALID_RUNNER_REGISTRAR_TYPES)
expect(page).not_to have_css('.as-runner')
end
end
end
context 'Container Registry' do
let(:feature_flag_enabled) { true }
let(:client_support) { true }
......
......@@ -178,6 +178,26 @@ RSpec.describe ApplicationSettingsHelper do
end
end
describe '.valid_runner_registrars' do
subject { helper.valid_runner_registrars }
context 'when only admins are permitted to register runners' do
before do
stub_application_setting(valid_runner_registrars: [])
end
it { is_expected.to eq [] }
end
context 'when group and project users are permitted to register runners' do
before do
stub_application_setting(valid_runner_registrars: ApplicationSetting::VALID_RUNNER_REGISTRAR_TYPES)
end
it { is_expected.to eq ApplicationSetting::VALID_RUNNER_REGISTRAR_TYPES }
end
end
describe '.signup_enabled?' do
subject { helper.signup_enabled? }
......
......@@ -134,6 +134,14 @@ RSpec.describe ApplicationSetting do
it { is_expected.to allow_value('disabled').for(:whats_new_variant) }
it { is_expected.not_to allow_value(nil).for(:whats_new_variant) }
it { is_expected.not_to allow_value(['']).for(:valid_runner_registrars) }
it { is_expected.not_to allow_value(['OBVIOUSLY_WRONG']).for(:valid_runner_registrars) }
it { is_expected.not_to allow_value(%w(project project)).for(:valid_runner_registrars) }
it { is_expected.not_to allow_value([nil]).for(:valid_runner_registrars) }
it { is_expected.not_to allow_value(nil).for(:valid_runner_registrars) }
it { is_expected.to allow_value([]).for(:valid_runner_registrars) }
it { is_expected.to allow_value(%w(project group)).for(:valid_runner_registrars) }
context 'help_page_documentation_base_url validations' do
it { is_expected.to allow_value(nil).for(:help_page_documentation_base_url) }
it { is_expected.to allow_value('https://docs.gitlab.com').for(:help_page_documentation_base_url) }
......
......@@ -11,8 +11,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
before do
stub_feature_flags(ci_enable_live_trace: true)
stub_feature_flags(runner_registration_control: false)
stub_gitlab_calls
stub_application_setting(runners_registration_token: registration_token)
stub_application_setting(valid_runner_registrars: ApplicationSetting::VALID_RUNNER_REGISTRAR_TYPES)
allow_any_instance_of(::Ci::Runner).to receive(:cache_attributes)
end
......@@ -122,6 +124,33 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
expect(project.runners.recent.size).to eq(1)
end
end
context 'when valid runner registrars do not include project' do
before do
stub_application_setting(valid_runner_registrars: ['group'])
end
context 'when feature flag is enabled' do
before do
stub_feature_flags(runner_registration_control: true)
end
it 'returns 403 error' do
request
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when feature flag is disabled' do
it 'registers the runner' do
request
expect(response).to have_gitlab_http_status(:created)
expect(::Ci::Runner.first.active).to be true
end
end
end
end
context 'when group token is used' do
......@@ -180,6 +209,33 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
expect(group.runners.recent.size).to eq(1)
end
end
context 'when valid runner registrars do not include group' do
before do
stub_application_setting(valid_runner_registrars: ['project'])
end
context 'when feature flag is enabled' do
before do
stub_feature_flags(runner_registration_control: true)
end
it 'returns 403 error' do
request
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when feature flag is disabled' do
it 'registers the runner' do
request
expect(response).to have_gitlab_http_status(:created)
expect(::Ci::Runner.first.active).to be true
end
end
end
end
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'admin/application_settings/ci_cd.html.haml' do
let_it_be(:app_settings) { build(:application_setting) }
let_it_be(:user) { create(:admin) }
let_it_be(:default_plan_limits) { create(:plan_limits, :default_plan, :with_package_file_sizes) }
before do
assign(:application_setting, app_settings)
assign(:plans, [default_plan_limits.plan])
allow(view).to receive(:current_user).and_return(user)
end
describe 'CI CD Runner Registration' do
context 'when feature flag is enabled' do
before do
stub_feature_flags(runner_registration_control: true)
end
it 'has the setting section' do
render
expect(rendered).to have_css("#js-runner-settings")
end
it 'renders the correct setting section content' do
render
expect(rendered).to have_content("Runner registration")
expect(rendered).to have_content("If no options are selected, only administrators can register runners.")
end
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(runner_registration_control: false)
end
it 'does not have the setting section' do
render
expect(rendered).not_to have_css("#js-runner-settings")
end
it 'does not render the correct setting section content' do
render
expect(rendered).not_to have_content("Runner registration")
expect(rendered).not_to have_content("If no options are selected, only administrators can register runners.")
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'groups/runners/group_runners.html.haml' do
describe 'render' do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
before do
@group = group
allow(view).to receive(:current_user).and_return(user)
allow(view).to receive(:reset_registration_token_group_settings_ci_cd_path).and_return('banana_url')
allow(view).to receive(:can?).with(user, :admin_pipeline, group).and_return(true)
end
context 'when group runner registration is allowed' do
before do
stub_application_setting(valid_runner_registrars: ['group'])
end
it 'enables the Remove group button for a group' do
render 'groups/runners/group_runners', group: group
expect(rendered).to have_selector '#js-install-runner'
expect(rendered).not_to have_content 'Please contact an admin to register runners.'
end
end
context 'when group runner registration is not allowed' do
before do
stub_application_setting(valid_runner_registrars: ['project'])
end
it 'does not enable the the Remove group button for a group' do
render 'groups/runners/group_runners', group: group
expect(rendered).to have_content 'Please contact an admin to register runners.'
expect(rendered).not_to have_selector '#js-install-runner'
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'projects/runners/specific_runners.html.haml' do
describe 'render' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
before do
@project = project
@assignable_runners = []
@project_runners = []
allow(view).to receive(:reset_registration_token_namespace_project_settings_ci_cd_path).and_return('banana_url')
end
context 'when project runner registration is allowed' do
before do
stub_application_setting(valid_runner_registrars: ['project'])
end
it 'enables the Remove project button for a project' do
render 'projects/runners/specific_runners', project: project
expect(rendered).to have_selector '#js-install-runner'
expect(rendered).not_to have_content 'Please contact an admin to register runners.'
end
end
context 'when project runner registration is not allowed' do
before do
stub_application_setting(valid_runner_registrars: ['group'])
end
it 'does not enable the the Remove project button for a project' do
render 'projects/runners/specific_runners', project: project
expect(rendered).to have_content 'Please contact an admin to register runners.'
expect(rendered).not_to have_selector '#js-install-runner'
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment