Commit ed519c16 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'ci-lfs-fetch' into 'master'

Allow to fetch LFS from CI

## What does this MR do?

This adds support for fetching LFS object from CI jobs (mostly it's made for supporting GitLab CI).

## What is left?

- [x] Write tests covering a new authorization mechanism

cc @grzesiek @marin

See merge request !4465
parents 1ddbbf35 6bc22d95
...@@ -79,6 +79,7 @@ v 8.9.0 (unreleased) ...@@ -79,6 +79,7 @@ v 8.9.0 (unreleased)
- Pipelines can be canceled only when there are running builds - Pipelines can be canceled only when there are running builds
- Allow authentication using personal access tokens - Allow authentication using personal access tokens
- Use downcased path to container repository as this is expected path by Docker - Use downcased path to container repository as this is expected path by Docker
- Allow to use CI token to fetch LFS objects
- Custom notification settings - Custom notification settings
- Projects pending deletion will render a 404 page - Projects pending deletion will render a 404 page
- Measure queue duration between gitlab-workhorse and Rails - Measure queue duration between gitlab-workhorse and Rails
......
...@@ -31,7 +31,7 @@ module Grack ...@@ -31,7 +31,7 @@ module Grack
auth! auth!
lfs_response = Gitlab::Lfs::Router.new(project, @user, @request).try_call lfs_response = Gitlab::Lfs::Router.new(project, @user, @ci, @request).try_call
return lfs_response unless lfs_response.nil? return lfs_response unless lfs_response.nil?
if @user.nil? && !@ci if @user.nil? && !@ci
......
...@@ -2,10 +2,11 @@ module Gitlab ...@@ -2,10 +2,11 @@ module Gitlab
module Lfs module Lfs
class Response class Response
def initialize(project, user, request) def initialize(project, user, ci, request)
@origin_project = project @origin_project = project
@project = storage_project(project) @project = storage_project(project)
@user = user @user = user
@ci = ci
@env = request.env @env = request.env
@request = request @request = request
end end
...@@ -189,7 +190,7 @@ module Gitlab ...@@ -189,7 +190,7 @@ module Gitlab
return render_not_enabled unless Gitlab.config.lfs.enabled return render_not_enabled unless Gitlab.config.lfs.enabled
unless @project.public? unless @project.public?
return render_unauthorized unless @user return render_unauthorized unless @user || @ci
return render_forbidden unless user_can_fetch? return render_forbidden unless user_can_fetch?
end end
...@@ -210,7 +211,7 @@ module Gitlab ...@@ -210,7 +211,7 @@ module Gitlab
def user_can_fetch? def user_can_fetch?
# Check user access against the project they used to initiate the pull # Check user access against the project they used to initiate the pull
@user.can?(:download_code, @origin_project) @ci || @user.can?(:download_code, @origin_project)
end end
def user_can_push? def user_can_push?
......
module Gitlab module Gitlab
module Lfs module Lfs
class Router class Router
def initialize(project, user, request) attr_reader :project, :user, :ci, :request
def initialize(project, user, ci, request)
@project = project @project = project
@user = user @user = user
@ci = ci
@env = request.env @env = request.env
@request = request @request = request
end end
...@@ -80,7 +83,7 @@ module Gitlab ...@@ -80,7 +83,7 @@ module Gitlab
def lfs def lfs
return unless @project return unless @project
Gitlab::Lfs::Response.new(@project, @user, @request) Gitlab::Lfs::Response.new(@project, @user, @ci, @request)
end end
def sanitize_tmp_filename(name) def sanitize_tmp_filename(name)
......
...@@ -17,12 +17,15 @@ describe Gitlab::Lfs::Router, lib: true do ...@@ -17,12 +17,15 @@ describe Gitlab::Lfs::Router, lib: true do
} }
end end
let(:lfs_router_auth) { new_lfs_router(project, user) } let(:lfs_router_auth) { new_lfs_router(project, user: user) }
let(:lfs_router_noauth) { new_lfs_router(project, nil) } let(:lfs_router_ci_auth) { new_lfs_router(project, ci: true) }
let(:lfs_router_public_auth) { new_lfs_router(public_project, user) } let(:lfs_router_noauth) { new_lfs_router(project) }
let(:lfs_router_public_noauth) { new_lfs_router(public_project, nil) } let(:lfs_router_public_auth) { new_lfs_router(public_project, user: user) }
let(:lfs_router_forked_noauth) { new_lfs_router(forked_project, nil) } let(:lfs_router_public_ci_auth) { new_lfs_router(public_project, ci: true) }
let(:lfs_router_forked_auth) { new_lfs_router(forked_project, user_two) } let(:lfs_router_public_noauth) { new_lfs_router(public_project) }
let(:lfs_router_forked_noauth) { new_lfs_router(forked_project) }
let(:lfs_router_forked_auth) { new_lfs_router(forked_project, user: user_two) }
let(:lfs_router_forked_ci_auth) { new_lfs_router(forked_project, ci: true) }
let(:sample_oid) { "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80" } let(:sample_oid) { "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80" }
let(:sample_size) { 499013 } let(:sample_size) { 499013 }
...@@ -80,6 +83,7 @@ describe Gitlab::Lfs::Router, lib: true do ...@@ -80,6 +83,7 @@ describe Gitlab::Lfs::Router, lib: true do
context 'with required headers' do context 'with required headers' do
before do before do
project.lfs_objects << lfs_object
env['HTTP_X_SENDFILE_TYPE'] = "X-Sendfile" env['HTTP_X_SENDFILE_TYPE'] = "X-Sendfile"
end end
...@@ -91,7 +95,6 @@ describe Gitlab::Lfs::Router, lib: true do ...@@ -91,7 +95,6 @@ describe Gitlab::Lfs::Router, lib: true do
context 'when user has project access' do context 'when user has project access' do
before do before do
project.lfs_objects << lfs_object
project.team << [user, :master] project.team << [user, :master]
end end
...@@ -104,6 +107,17 @@ describe Gitlab::Lfs::Router, lib: true do ...@@ -104,6 +107,17 @@ describe Gitlab::Lfs::Router, lib: true do
expect(lfs_router_auth.try_call[1]['X-Sendfile']).to eq(lfs_object.file.path) expect(lfs_router_auth.try_call[1]['X-Sendfile']).to eq(lfs_object.file.path)
end end
end end
context 'when CI is authorized' do
it "responds with status 200" do
expect(lfs_router_ci_auth.try_call.first).to eq(200)
end
it "responds with the file location" do
expect(lfs_router_ci_auth.try_call[1]['Content-Type']).to eq("application/octet-stream")
expect(lfs_router_ci_auth.try_call[1]['X-Sendfile']).to eq(lfs_object.file.path)
end
end
end end
context 'without required headers' do context 'without required headers' do
...@@ -134,7 +148,6 @@ describe Gitlab::Lfs::Router, lib: true do ...@@ -134,7 +148,6 @@ describe Gitlab::Lfs::Router, lib: true do
end end
describe 'download' do describe 'download' do
describe 'when user is authenticated' do
before do before do
body = { 'operation' => 'download', body = { 'operation' => 'download',
'objects' => [ 'objects' => [
...@@ -145,20 +158,14 @@ describe Gitlab::Lfs::Router, lib: true do ...@@ -145,20 +158,14 @@ describe Gitlab::Lfs::Router, lib: true do
env['rack.input'] = StringIO.new(body) env['rack.input'] = StringIO.new(body)
end end
describe 'when user has download access' do shared_examples 'an authorized requests' do
before do
@auth = authorize(user)
env["HTTP_AUTHORIZATION"] = @auth
project.team << [user, :reporter]
end
context 'when downloading an lfs object that is assigned to our project' do context 'when downloading an lfs object that is assigned to our project' do
before do before do
project.lfs_objects << lfs_object project.lfs_objects << lfs_object
end end
it 'responds with status 200 and href to download' do it 'responds with status 200 and href to download' do
response = lfs_router_auth.try_call response = router.try_call
expect(response.first).to eq(200) expect(response.first).to eq(200)
response_body = ActiveSupport::JSON.decode(response.last.first) response_body = ActiveSupport::JSON.decode(response.last.first)
...@@ -168,7 +175,7 @@ describe Gitlab::Lfs::Router, lib: true do ...@@ -168,7 +175,7 @@ describe Gitlab::Lfs::Router, lib: true do
'actions' => { 'actions' => {
'download' => { 'download' => {
'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
'header' => { 'Authorization' => @auth } 'header' => { 'Authorization' => auth }
} }
} }
}]) }])
...@@ -181,7 +188,7 @@ describe Gitlab::Lfs::Router, lib: true do ...@@ -181,7 +188,7 @@ describe Gitlab::Lfs::Router, lib: true do
end end
it 'responds with status 200 and error message' do it 'responds with status 200 and error message' do
response = lfs_router_auth.try_call response = router.try_call
expect(response.first).to eq(200) expect(response.first).to eq(200)
response_body = ActiveSupport::JSON.decode(response.last.first) response_body = ActiveSupport::JSON.decode(response.last.first)
...@@ -208,7 +215,7 @@ describe Gitlab::Lfs::Router, lib: true do ...@@ -208,7 +215,7 @@ describe Gitlab::Lfs::Router, lib: true do
end end
it "responds with status 200 and error message" do it "responds with status 200 and error message" do
response = lfs_router_auth.try_call response = router.try_call
expect(response.first).to eq(200) expect(response.first).to eq(200)
response_body = ActiveSupport::JSON.decode(response.last.first) response_body = ActiveSupport::JSON.decode(response.last.first)
...@@ -240,7 +247,7 @@ describe Gitlab::Lfs::Router, lib: true do ...@@ -240,7 +247,7 @@ describe Gitlab::Lfs::Router, lib: true do
end end
it "responds with status 200 with upload hypermedia link for the new object" do it "responds with status 200 with upload hypermedia link for the new object" do
response = lfs_router_auth.try_call response = router.try_call
expect(response.first).to eq(200) expect(response.first).to eq(200)
response_body = ActiveSupport::JSON.decode(response.last.first) response_body = ActiveSupport::JSON.decode(response.last.first)
...@@ -257,7 +264,7 @@ describe Gitlab::Lfs::Router, lib: true do ...@@ -257,7 +264,7 @@ describe Gitlab::Lfs::Router, lib: true do
'actions' => { 'actions' => {
'download' => { 'download' => {
'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
'header' => { 'Authorization' => @auth } 'header' => { 'Authorization' => auth }
} }
} }
}]) }])
...@@ -265,24 +272,29 @@ describe Gitlab::Lfs::Router, lib: true do ...@@ -265,24 +272,29 @@ describe Gitlab::Lfs::Router, lib: true do
end end
end end
context 'when user does is not member of the project' do context 'when user is authenticated' do
let(:auth) { authorize(user) }
before do before do
@auth = authorize(user) env["HTTP_AUTHORIZATION"] = auth
env["HTTP_AUTHORIZATION"] = @auth project.team << [user, role]
project.team << [user, :guest]
end end
it_behaves_like 'an authorized requests' do
let(:role) { :reporter }
let(:router) { lfs_router_auth }
end
context 'when user does is not member of the project' do
let(:role) { :guest }
it 'responds with 403' do it 'responds with 403' do
expect(lfs_router_auth.try_call.first).to eq(403) expect(lfs_router_auth.try_call.first).to eq(403)
end end
end end
context 'when user does not have download access' do context 'when user does not have download access' do
before do let(:role) { :guest }
@auth = authorize(user)
env["HTTP_AUTHORIZATION"] = @auth
project.team << [user, :guest]
end
it 'responds with 403' do it 'responds with 403' do
expect(lfs_router_auth.try_call.first).to eq(403) expect(lfs_router_auth.try_call.first).to eq(403)
...@@ -290,18 +302,19 @@ describe Gitlab::Lfs::Router, lib: true do ...@@ -290,18 +302,19 @@ describe Gitlab::Lfs::Router, lib: true do
end end
end end
context 'when user is not authenticated' do context 'when CI is authorized' do
let(:auth) { 'gitlab-ci-token:password' }
before do before do
body = { 'operation' => 'download', env["HTTP_AUTHORIZATION"] = auth
'objects' => [ end
{ 'oid' => sample_oid,
'size' => sample_size
}],
}.to_json it_behaves_like 'an authorized requests' do
env['rack.input'] = StringIO.new(body) let(:router) { lfs_router_ci_auth }
end
end end
context 'when user is not authenticated' do
describe 'is accessing public project' do describe 'is accessing public project' do
before do before do
public_project.lfs_objects << lfs_object public_project.lfs_objects << lfs_object
...@@ -338,7 +351,6 @@ describe Gitlab::Lfs::Router, lib: true do ...@@ -338,7 +351,6 @@ describe Gitlab::Lfs::Router, lib: true do
end end
describe 'upload' do describe 'upload' do
describe 'when user is authenticated' do
before do before do
body = { 'operation' => 'upload', body = { 'operation' => 'upload',
'objects' => [ 'objects' => [
...@@ -349,6 +361,7 @@ describe Gitlab::Lfs::Router, lib: true do ...@@ -349,6 +361,7 @@ describe Gitlab::Lfs::Router, lib: true do
env['rack.input'] = StringIO.new(body) env['rack.input'] = StringIO.new(body)
end end
describe 'when request is authenticated' do
describe 'when user has project push access' do describe 'when user has project push access' do
before do before do
@auth = authorize(user) @auth = authorize(user)
...@@ -440,15 +453,15 @@ describe Gitlab::Lfs::Router, lib: true do ...@@ -440,15 +453,15 @@ describe Gitlab::Lfs::Router, lib: true do
expect(lfs_router_auth.try_call.first).to eq(403) expect(lfs_router_auth.try_call.first).to eq(403)
end end
end end
end
context 'when user is not authenticated' do context 'when CI is authorized' do
before do it 'responds with 401' do
env['rack.input'] = StringIO.new( expect(lfs_router_ci_auth.try_call.first).to eq(401)
{ 'objects' => [], 'operation' => 'upload' }.to_json end
) end
end end
context 'when user is not authenticated' do
context 'when user has push access' do context 'when user has push access' do
before do before do
project.team << [user, :master] project.team << [user, :master]
...@@ -465,6 +478,18 @@ describe Gitlab::Lfs::Router, lib: true do ...@@ -465,6 +478,18 @@ describe Gitlab::Lfs::Router, lib: true do
end end
end end
end end
context 'when CI is authorized' do
let(:auth) { 'gitlab-ci-token:password' }
before do
env["HTTP_AUTHORIZATION"] = auth
end
it "responds with status 403" do
expect(lfs_router_public_ci_auth.try_call.first).to eq(401)
end
end
end end
describe 'unsupported' do describe 'unsupported' do
...@@ -490,109 +515,80 @@ describe Gitlab::Lfs::Router, lib: true do ...@@ -490,109 +515,80 @@ describe Gitlab::Lfs::Router, lib: true do
env['REQUEST_METHOD'] = 'PUT' env['REQUEST_METHOD'] = 'PUT'
end end
describe 'to one project' do shared_examples 'unauthorized' do
describe 'when user has push access to the project' do
before do
project.team << [user, :master]
end
describe 'when user is authenticated' do
context 'and request is sent by gitlab-workhorse to authorize the request' do context 'and request is sent by gitlab-workhorse to authorize the request' do
before do before do
header_for_upload_authorize(project) header_for_upload_authorize(router.project)
end
it 'responds with status 200, location of lfs store and object details' do
json_response = ActiveSupport::JSON.decode(lfs_router_auth.try_call.last.first)
expect(lfs_router_auth.try_call.first).to eq(200)
expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload")
expect(json_response['LfsOid']).to eq(sample_oid)
expect(json_response['LfsSize']).to eq(sample_size)
end
end
context 'and request is sent by gitlab-workhorse to finalize the upload' do
before do
headers_for_upload_finalize(project)
end
it 'responds with status 200 and lfs object is linked to the project' do
expect(lfs_router_auth.try_call.first).to eq(200)
expect(lfs_object.projects.pluck(:id)).to include(project.id)
end
end
end
describe 'when user is unauthenticated' do
let(:lfs_router_noauth) { new_lfs_router(project, nil) }
context 'and request is sent by gitlab-workhorse to authorize the request' do
before do
header_for_upload_authorize(project)
end end
it 'responds with status 401' do it 'responds with status 401' do
expect(lfs_router_noauth.try_call.first).to eq(401) expect(router.try_call.first).to eq(401)
end end
end end
context 'and request is sent by gitlab-workhorse to finalize the upload' do context 'and request is sent by gitlab-workhorse to finalize the upload' do
before do before do
headers_for_upload_finalize(project) headers_for_upload_finalize(router.project)
end end
it 'responds with status 401' do it 'responds with status 401' do
expect(lfs_router_noauth.try_call.first).to eq(401) expect(router.try_call.first).to eq(401)
end end
end end
context 'and request is sent with a malformed headers' do context 'and request is sent with a malformed headers' do
before do before do
env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}" env["PATH_INFO"] = "#{router.project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}"
env["HTTP_X_GITLAB_LFS_TMP"] = "cat /etc/passwd" env["HTTP_X_GITLAB_LFS_TMP"] = "cat /etc/passwd"
end end
it 'does not recognize it as a valid lfs command' do it 'does not recognize it as a valid lfs command' do
expect(lfs_router_noauth.try_call).to eq(nil) expect(router.try_call).to eq(nil)
end
end end
end end
end end
describe 'and user does not have push access' do shared_examples 'forbidden' do
describe 'when user is authenticated' do
context 'and request is sent by gitlab-workhorse to authorize the request' do context 'and request is sent by gitlab-workhorse to authorize the request' do
before do before do
header_for_upload_authorize(project) header_for_upload_authorize(router.project)
end end
it 'responds with 403' do it 'responds with 403' do
expect(lfs_router_auth.try_call.first).to eq(403) expect(router.try_call.first).to eq(403)
end end
end end
context 'and request is sent by gitlab-workhorse to finalize the upload' do context 'and request is sent by gitlab-workhorse to finalize the upload' do
before do before do
headers_for_upload_finalize(project) headers_for_upload_finalize(router.project)
end end
it 'responds with 403' do it 'responds with 403' do
expect(lfs_router_auth.try_call.first).to eq(403) expect(router.try_call.first).to eq(403)
end end
end end
end end
describe 'when user is unauthenticated' do describe 'to one project' do
let(:lfs_router_noauth) { new_lfs_router(project, nil) } describe 'when user is authenticated' do
describe 'when user has push access to the project' do
before do
project.team << [user, :developer]
end
context 'and request is sent by gitlab-workhorse to authorize the request' do context 'and request is sent by gitlab-workhorse to authorize the request' do
before do before do
header_for_upload_authorize(project) header_for_upload_authorize(project)
end end
it 'responds with 401' do it 'responds with status 200, location of lfs store and object details' do
expect(lfs_router_noauth.try_call.first).to eq(401) json_response = ActiveSupport::JSON.decode(lfs_router_auth.try_call.last.first)
expect(lfs_router_auth.try_call.first).to eq(200)
expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload")
expect(json_response['LfsOid']).to eq(sample_oid)
expect(json_response['LfsSize']).to eq(sample_size)
end end
end end
...@@ -601,23 +597,42 @@ describe Gitlab::Lfs::Router, lib: true do ...@@ -601,23 +597,42 @@ describe Gitlab::Lfs::Router, lib: true do
headers_for_upload_finalize(project) headers_for_upload_finalize(project)
end end
it 'responds with 401' do it 'responds with status 200 and lfs object is linked to the project' do
expect(lfs_router_noauth.try_call.first).to eq(401) expect(lfs_router_auth.try_call.first).to eq(200)
expect(lfs_object.projects.pluck(:id)).to include(project.id)
end end
end end
end end
describe 'and user does not have push access' do
let(:router) { lfs_router_auth }
it_behaves_like 'forbidden'
end end
end end
describe "to a forked project" do context 'when CI is authenticated' do
let(:router) { lfs_router_ci_auth }
it_behaves_like 'unauthorized'
end
context 'for unauthenticated' do
let(:router) { new_lfs_router(project) }
it_behaves_like 'unauthorized'
end
end
describe 'to a forked project' do
let(:forked_project) { fork_project(public_project, user) } let(:forked_project) { fork_project(public_project, user) }
describe 'when user is authenticated' do
describe 'when user has push access to the project' do describe 'when user has push access to the project' do
before do before do
forked_project.team << [user_two, :master] forked_project.team << [user_two, :developer]
end end
describe 'when user is authenticated' do
context 'and request is sent by gitlab-workhorse to authorize the request' do context 'and request is sent by gitlab-workhorse to authorize the request' do
before do before do
header_for_upload_authorize(forked_project) header_for_upload_authorize(forked_project)
...@@ -645,78 +660,28 @@ describe Gitlab::Lfs::Router, lib: true do ...@@ -645,78 +660,28 @@ describe Gitlab::Lfs::Router, lib: true do
end end
end end
describe 'when user is unauthenticated' do
context 'and request is sent by gitlab-workhorse to authorize the request' do
before do
header_for_upload_authorize(forked_project)
end
it 'responds with status 401' do
expect(lfs_router_forked_noauth.try_call.first).to eq(401)
end
end
context 'and request is sent by gitlab-workhorse to finalize the upload' do
before do
headers_for_upload_finalize(forked_project)
end
it 'responds with status 401' do
expect(lfs_router_forked_noauth.try_call.first).to eq(401)
end
end
end
end
describe 'and user does not have push access' do describe 'and user does not have push access' do
describe 'when user is authenticated' do let(:router) { lfs_router_forked_auth }
context 'and request is sent by gitlab-workhorse to authorize the request' do
before do
header_for_upload_authorize(forked_project)
end
it 'responds with 403' do
expect(lfs_router_forked_auth.try_call.first).to eq(403)
end
end
context 'and request is sent by gitlab-workhorse to finalize the upload' do
before do
headers_for_upload_finalize(forked_project)
end
it 'responds with 403' do it_behaves_like 'forbidden'
expect(lfs_router_forked_auth.try_call.first).to eq(403)
end
end end
end end
describe 'when user is unauthenticated' do context 'when CI is authenticated' do
context 'and request is sent by gitlab-workhorse to authorize the request' do let(:router) { lfs_router_forked_ci_auth }
before do
header_for_upload_authorize(forked_project)
end
it 'responds with 401' do it_behaves_like 'unauthorized'
expect(lfs_router_forked_noauth.try_call.first).to eq(401)
end
end end
context 'and request is sent by gitlab-workhorse to finalize the upload' do context 'for unauthenticated' do
before do let(:router) { lfs_router_forked_noauth }
headers_for_upload_finalize(forked_project)
end
it 'responds with 401' do it_behaves_like 'unauthorized'
expect(lfs_router_forked_noauth.try_call.first).to eq(401)
end
end
end
end end
describe 'and second project not related to fork or a source project' do describe 'and second project not related to fork or a source project' do
let(:second_project) { create(:project) } let(:second_project) { create(:project) }
let(:lfs_router_second_project) { new_lfs_router(second_project, user) } let(:lfs_router_second_project) { new_lfs_router(second_project, user: user) }
before do before do
public_project.lfs_objects << lfs_object public_project.lfs_objects << lfs_object
...@@ -745,8 +710,8 @@ describe Gitlab::Lfs::Router, lib: true do ...@@ -745,8 +710,8 @@ describe Gitlab::Lfs::Router, lib: true do
ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password) ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
end end
def new_lfs_router(project, user) def new_lfs_router(project, user: nil, ci: false)
Gitlab::Lfs::Router.new(project, user, request) Gitlab::Lfs::Router.new(project, user, ci, request)
end end
def header_for_upload_authorize(project) def header_for_upload_authorize(project)
......
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