Commit cfd5870b authored by Douwe Maan's avatar Douwe Maan

Merge branch 'allow-disabling-of-git-access-protocol' into 'master'

Add setting that allows admins to choose which Git access protocols are enabled.

## What does this MR do?

It allows admins to disable one of the two protocols for Git access. They can choose to enable just SSH, HTTP or allow both. If one of them is disabled, the clone URL in the project will show only the allowed protocol, and no dropdown to change protocols will be presented.

## What are the relevant issue numbers?

Full implementation on GitLab's side for #18601 

GitLab Shell implementation: gitlab-org/gitlab-shell!62

GitLab Workhorse implementation: gitlab-org/gitlab-workhorse!51

## Screenshots (if relevant)

![Screen_Shot_2016-06-16_at_12.26.19_PM](/uploads/bad845142e9704a7385b2eaca51fd4eb/Screen_Shot_2016-06-16_at_12.26.19_PM.png)
![Screen_Shot_2016-06-20_at_4.24.54_PM](/uploads/6e452dd269e06f0be23841ce93866ed6/Screen_Shot_2016-06-20_at_4.24.54_PM.png)



/cc @jschatz1  this MR touches the UI. Please review.

See merge request !4696
parents 1141eaf5 0bdf6fe4
......@@ -23,6 +23,7 @@ v 8.10.0 (unreleased)
- Add notification settings dropdown for groups
- Allow importing from Github using Personal Access Tokens. (Eric K Idema)
- API: Todos !3188 (Robert Schilling)
- Add "Enabled Git access protocols" to Application Settings
- Fix user creation with stronger minimum password requirements !4054 (nathan-pmt)
- PipelinesFinder uses git cache data
- Check for conflicts with existing Project's wiki path when creating a new project.
......
......@@ -281,3 +281,21 @@
color: $gl-icon-color;
}
}
.clone-dropdown-btn a {
color: $dropdown-link-color;
&:hover {
text-decoration: none;
}
}
.btn-static {
background-color: $background-color !important;
border: 1px solid lightgrey;
cursor: default;
&:active {
-moz-box-shadow: inset 0 0 0 white;
-webkit-box-shadow: inset 0 0 0 white;
box-shadow: inset 0 0 0 white;
}
}
......@@ -110,6 +110,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:send_user_confirmation_email,
:container_registry_token_expire_delay,
:repository_storage,
:enabled_git_access_protocol,
restricted_visibility_levels: [],
import_sources: [],
disabled_oauth_sign_in_sources: []
......
......@@ -19,6 +19,8 @@ class Projects::GitHttpController < Projects::ApplicationController
render_ok
elsif receive_pack? && receive_pack_allowed?
render_ok
elsif http_blocked?
render_not_allowed
else
render_not_found
end
......@@ -154,6 +156,10 @@ class Projects::GitHttpController < Projects::ApplicationController
render plain: 'Not Found', status: :not_found
end
def render_not_allowed
render plain: download_access.message, status: :forbidden
end
def ci?
@ci.present?
end
......@@ -162,12 +168,28 @@ class Projects::GitHttpController < Projects::ApplicationController
return false unless Gitlab.config.gitlab_shell.upload_pack
if user
Gitlab::GitAccess.new(user, project).download_access_check.allowed?
download_access.allowed?
else
ci? || project.public?
end
end
def access
return @access if defined?(@access)
@access = Gitlab::GitAccess.new(user, project, 'http')
end
def download_access
return @download_access if defined?(@download_access)
@download_access = access.check('git-upload-pack')
end
def http_blocked?
!access.protocol_allowed?
end
def receive_pack_allowed?
return false unless Gitlab.config.gitlab_shell.receive_pack
......
......@@ -31,6 +31,28 @@ module ApplicationSettingsHelper
current_application_settings.akismet_enabled?
end
def allowed_protocols_present?
current_application_settings.enabled_git_access_protocol.present?
end
def enabled_protocol
case current_application_settings.enabled_git_access_protocol
when 'http'
gitlab_config.protocol
when 'ssh'
'ssh'
end
end
def enabled_project_button(project, protocol)
case protocol
when 'ssh'
ssh_clone_button(project, 'bottom', append_link: false)
else
http_clone_button(project, 'bottom', append_link: false)
end
end
# Return a group of checkboxes that use Bootstrap's button plugin for a
# toggle button effect.
def restricted_level_checkboxes(help_block_id)
......
......@@ -12,7 +12,7 @@ module BranchesHelper
def can_push_branch?(project, branch_name)
return false unless project.repository.branch_exists?(branch_name)
::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name)
::Gitlab::GitAccess.new(current_user, project, 'web').can_push_to_branch?(branch_name)
end
def project_branches
......
......@@ -40,33 +40,33 @@ module ButtonHelper
type: :button
end
def http_clone_button(project)
def http_clone_button(project, placement = 'right', append_link: true)
klass = 'http-selector'
klass << ' has-tooltip' if current_user.try(:require_password?)
protocol = gitlab_config.protocol.upcase
content_tag :a, protocol,
content_tag (append_link ? :a : :span), protocol,
class: klass,
href: project.http_url_to_repo,
href: (project.http_url_to_repo if append_link),
data: {
html: true,
placement: 'right',
placement: placement,
container: 'body',
title: "Set a password on your account<br>to pull or push via #{protocol}"
}
end
def ssh_clone_button(project)
def ssh_clone_button(project, placement = 'right', append_link: true)
klass = 'ssh-selector'
klass << ' has-tooltip' if current_user.try(:require_ssh_key?)
content_tag :a, 'SSH',
content_tag (append_link ? :a : :span), 'SSH',
class: klass,
href: project.ssh_url_to_repo,
href: (project.ssh_url_to_repo if append_link),
data: {
html: true,
placement: 'right',
placement: placement,
container: 'body',
title: 'Add an SSH key to your profile<br>to pull or push via SSH.'
}
......
......@@ -206,10 +206,14 @@ module ProjectsHelper
end
def default_clone_protocol
if allowed_protocols_present?
enabled_protocol
else
if !current_user || current_user.require_ssh_key?
gitlab_config.protocol
else
"ssh"
'ssh'
end
end
end
......
......@@ -59,6 +59,9 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
validates :enabled_git_access_protocol,
inclusion: { in: %w(ssh http), allow_blank: true, allow_nil: true }
validates_each :restricted_visibility_levels do |record, attr, value|
unless value.nil?
value.each do |level|
......
......@@ -481,7 +481,7 @@ class MergeRequest < ActiveRecord::Base
end
def can_be_merged_by?(user)
::Gitlab::GitAccess.new(user, project).can_push_to_branch?(target_branch)
::Gitlab::GitAccess.new(user, project, 'web').can_push_to_branch?(target_branch)
end
def mergeable_ci_state?
......
......@@ -23,7 +23,7 @@ module Commits
private
def check_push_permissions
allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(@target_branch)
allowed = ::Gitlab::GitAccess.new(current_user, project, 'web').can_push_to_branch?(@target_branch)
unless allowed
raise ValidationError.new('You are not allowed to push into this branch')
......
......@@ -43,7 +43,7 @@ module Files
end
def validate
allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(@target_branch)
allowed = ::Gitlab::GitAccess.new(current_user, project, 'web').can_push_to_branch?(@target_branch)
unless allowed
raise_error("You are not allowed to push into this branch")
......
......@@ -43,6 +43,12 @@
= link_to "(?)", help_page_path("integration", "bitbucket")
and GitLab.com
= link_to "(?)", help_page_path("integration", "gitlab")
.form-group
%label.control-label.col-sm-2 Enabled Git access protocols
.col-sm-10
= select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control')
%span.help-block#clone-protocol-help
Allow only the selected protocols to be used for Git access.
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
......
......@@ -2,7 +2,12 @@
.git-clone-holder.input-group
.input-group-btn
%a#clone-dropdown.clone-dropdown-btn.btn{href: '#', 'data-toggle' => 'dropdown'}
-if allowed_protocols_present?
.clone-dropdown-btn.btn.btn-static
%span
= enabled_project_button(project, enabled_protocol)
- else
%a#clone-dropdown.clone-dropdown-btn.btn{href: '#', data: { toggle: 'dropdown' }}
%span
= default_clone_protocol.upcase
= icon('caret-down')
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
# rubocop:disable all
class AddEnabledGitAccessProtocolsToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
def change
add_column :application_settings, :enabled_git_access_protocol, :string
end
end
......@@ -86,6 +86,7 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.integer "container_registry_token_expire_delay", default: 5
t.text "after_sign_up_text"
t.string "repository_storage", default: "default"
t.string "enabled_git_access_protocol"
end
create_table "audit_events", force: :cascade do |t|
......
......@@ -21,6 +21,7 @@
## Administrator documentation
- [Access restrictions](administration/access_restrictions.md) Define which Git access protocols can be used to talk to GitLab
- [Authentication/Authorization](administration/auth/README.md) Configure
external authentication with LDAP, SAML, CAS and additional Omniauth providers.
- [Custom Git hooks](administration/custom_hooks.md) Custom Git hooks (on the filesystem) for when webhooks aren't enough.
......
# Access Restrictions
> **Note:** This feature is only available on versions 8.10 and above.
With GitLab's Access restrictions you can choose which Git access protocols you
want your users to use to communicate with GitLab. This feature can be enabled
via the `Application Settings` in the Admin interface.
The setting is called `Enabled Git access protocols`, and it gives you the option
to choose between:
- Both SSH and HTTP(S)
- Only SSH
- Only HTTP(s)
![Settings Overview](img/access_restrictions.png)
## Enabled Protocol
When both SSH and HTTP(S) are enabled, GitLab will behave as usual, it will give
your users the option to choose which protocol they would like to use.
When you choose to allow only one of the protocols, a couple of things will happen:
- The project page will only show the allowed protocol's URL, with no option to
change it.
- A tooltip will be shown when you hover over the URL's protocol, if an action
on the user's part is required, e.g. adding an SSH key, or setting a password.
![Project URL with SSH only access](img/restricted_url.png)
On top of these UI restrictions, GitLab will deny all Git actions on the protocol
not selected.
> **Note:** Please keep in mind that disabling an access protocol does not actually
block access to the server itself. The ports used for the protocol, be it SSH or
HTTP, will still be accessible. What GitLab does is restrict access on the
application level.
\ No newline at end of file
......@@ -68,6 +68,7 @@ PUT /application/settings
| `after_sign_out_path` | string | no | Where to redirect users after logout |
| `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes |
| `repository_storage` | string | no | Storage path for new projects. The value should be the name of one of the repository storage paths defined in your gitlab.yml |
| `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols.
```bash
curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1
......
......@@ -13,6 +13,7 @@ module API
# action - git action (git-upload-pack or git-receive-pack)
# ref - branch name
# forced_push - forced_push
# protocol - Git access protocol being used, e.g. HTTP or SSH
#
helpers do
......@@ -46,11 +47,13 @@ module API
User.find_by(id: params[:user_id])
end
protocol = params[:protocol]
access =
if wiki?
Gitlab::GitAccessWiki.new(actor, project)
Gitlab::GitAccessWiki.new(actor, project, protocol)
else
Gitlab::GitAccess.new(actor, project)
Gitlab::GitAccess.new(actor, project, protocol)
end
access_status = access.check(params[:action], params[:changes])
......
module Gitlab
module Git
class Hook
GL_PROTOCOL = 'web'.freeze
attr_reader :name, :repo_path, :path
def initialize(name, repo_path)
......@@ -34,7 +35,8 @@ module Gitlab
vars = {
'GL_ID' => gl_id,
'PWD' => repo_path
'PWD' => repo_path,
'GL_PROTOCOL' => GL_PROTOCOL
}
options = {
......
......@@ -3,11 +3,12 @@ module Gitlab
DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }
PUSH_COMMANDS = %w{ git-receive-pack }
attr_reader :actor, :project
attr_reader :actor, :project, :protocol
def initialize(actor, project)
def initialize(actor, project, protocol)
@actor = actor
@project = project
@protocol = protocol
end
def user
......@@ -49,6 +50,8 @@ module Gitlab
end
def check(cmd, changes = nil)
return build_status_object(false, "Git access over #{protocol.upcase} is not allowed") unless protocol_allowed?
unless actor
return build_status_object(false, "No user or key was provided.")
end
......@@ -164,6 +167,10 @@ module Gitlab
Gitlab::ForcePushCheck.force_push?(project, oldrev, newrev)
end
def protocol_allowed?
Gitlab::ProtocolAccess.allowed?(protocol)
end
private
def protected_branch_action(oldrev, newrev, branch_name)
......
module Gitlab
module ProtocolAccess
def self.allowed?(protocol)
if protocol == 'web'
true
elsif current_application_settings.enabled_git_access_protocol.blank?
true
else
protocol == current_application_settings.enabled_git_access_protocol
end
end
end
end
require 'rails_helper'
feature 'Admin disables Git access protocol', feature: true do
let(:project) { create(:empty_project, :empty_repo) }
let(:admin) { create(:admin) }
background do
login_as(admin)
end
context 'with HTTP disabled' do
background do
disable_http_protocol
end
scenario 'shows only SSH url' do
visit_project
expect(page).to have_content("git clone #{project.ssh_url_to_repo}")
expect(page).not_to have_selector('#clone-dropdown')
end
end
context 'with SSH disabled' do
background do
disable_ssh_protocol
end
scenario 'shows only HTTP url' do
visit_project
expect(page).to have_content("git clone #{project.http_url_to_repo}")
expect(page).not_to have_selector('#clone-dropdown')
end
end
context 'with nothing disabled' do
background do
create(:personal_key, user: admin)
end
scenario 'shows default SSH url and protocol selection dropdown' do
visit_project
expect(page).to have_content("git clone #{project.ssh_url_to_repo}")
expect(page).to have_selector('#clone-dropdown')
end
end
def visit_project
visit namespace_project_path(project.namespace, project)
end
def disable_http_protocol
visit admin_application_settings_path
find('#application_setting_enabled_git_access_protocol').find(:xpath, 'option[2]').select_option
click_on 'Save'
end
def disable_ssh_protocol
visit admin_application_settings_path
find('#application_setting_enabled_git_access_protocol').find(:xpath, 'option[3]').select_option
click_on 'Save'
end
end
require 'spec_helper'
describe Gitlab::GitAccess, lib: true do
let(:access) { Gitlab::GitAccess.new(actor, project) }
let(:access) { Gitlab::GitAccess.new(actor, project, 'web') }
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:actor) { user }
......@@ -67,6 +67,43 @@ describe Gitlab::GitAccess, lib: true do
end
end
describe '#check with single protocols allowed' do
def disable_protocol(protocol)
settings = ::ApplicationSetting.create_from_defaults
settings.update_attribute(:enabled_git_access_protocol, protocol)
end
context 'ssh disabled' do
before do
disable_protocol('ssh')
@acc = Gitlab::GitAccess.new(actor, project, 'ssh')
end
it 'blocks ssh git push' do
expect(@acc.check('git-receive-pack').allowed?).to be_falsey
end
it 'blocks ssh git pull' do
expect(@acc.check('git-upload-pack').allowed?).to be_falsey
end
end
context 'http disabled' do
before do
disable_protocol('http')
@acc = Gitlab::GitAccess.new(actor, project, 'http')
end
it 'blocks http push' do
expect(@acc.check('git-receive-pack').allowed?).to be_falsey
end
it 'blocks http git pull' do
expect(@acc.check('git-upload-pack').allowed?).to be_falsey
end
end
end
describe 'download_access_check' do
describe 'master permissions' do
before { project.team << [user, :master] }
......
require 'spec_helper'
describe Gitlab::GitAccessWiki, lib: true do
let(:access) { Gitlab::GitAccessWiki.new(user, project) }
let(:access) { Gitlab::GitAccessWiki.new(user, project, 'web') }
let(:project) { create(:project) }
let(:user) { create(:user) }
......
......@@ -207,26 +207,86 @@ describe API::API, api: true do
expect(json_response["status"]).to be_falsey
end
end
context 'ssh access has been disabled' do
before do
settings = ::ApplicationSetting.create_from_defaults
settings.update_attribute(:enabled_git_access_protocol, 'http')
end
it 'rejects the SSH push' do
push(key, project)
expect(response.status).to eq(200)
expect(json_response['status']).to be_falsey
expect(json_response['message']).to eq 'Git access over SSH is not allowed'
end
it 'rejects the SSH pull' do
pull(key, project)
expect(response.status).to eq(200)
expect(json_response['status']).to be_falsey
expect(json_response['message']).to eq 'Git access over SSH is not allowed'
end
end
context 'http access has been disabled' do
before do
settings = ::ApplicationSetting.create_from_defaults
settings.update_attribute(:enabled_git_access_protocol, 'ssh')
end
it 'rejects the HTTP push' do
push(key, project, 'http')
expect(response.status).to eq(200)
expect(json_response['status']).to be_falsey
expect(json_response['message']).to eq 'Git access over HTTP is not allowed'
end
it 'rejects the HTTP pull' do
pull(key, project, 'http')
expect(response.status).to eq(200)
expect(json_response['status']).to be_falsey
expect(json_response['message']).to eq 'Git access over HTTP is not allowed'
end
end
context 'web actions are always allowed' do
it 'allows WEB push' do
settings = ::ApplicationSetting.create_from_defaults
settings.update_attribute(:enabled_git_access_protocol, 'ssh')
project.team << [user, :developer]
push(key, project, 'web')
expect(response.status).to eq(200)
expect(json_response['status']).to be_truthy
end
end
end
def pull(key, project)
def pull(key, project, protocol = 'ssh')
post(
api("/internal/allowed"),
key_id: key.id,
project: project.path_with_namespace,
action: 'git-upload-pack',
secret_token: secret_token
secret_token: secret_token,
protocol: protocol
)
end
def push(key, project)
def push(key, project, protocol = 'ssh')
post(
api("/internal/allowed"),
changes: 'd14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master',
key_id: key.id,
project: project.path_with_namespace,
action: 'git-receive-pack',
secret_token: secret_token
secret_token: secret_token,
protocol: protocol
)
end
......@@ -237,7 +297,8 @@ describe API::API, api: true do
key_id: key.id,
project: project.path_with_namespace,
action: 'git-upload-archive',
secret_token: secret_token
secret_token: secret_token,
protocol: 'ssh'
)
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