Commit 98f57443 authored by Rubén Dávila's avatar Rubén Dávila

Address feedback from last code review.

- Added new compound unique key for lfs_file_locks
- Improved uniqueness validation for creation of locks.
- Added authorization checks for Lock and Unlock services
- Improve description in controler specs
parent c0e2317c
...@@ -3,7 +3,6 @@ class LfsFileLock < ActiveRecord::Base ...@@ -3,7 +3,6 @@ class LfsFileLock < ActiveRecord::Base
belongs_to :user belongs_to :user
validates :project_id, :user_id, :path, presence: true validates :project_id, :user_id, :path, presence: true
validates :path, uniqueness: { scope: [:project_id] }
def can_be_unlocked_by?(current_user, forced = false) def can_be_unlocked_by?(current_user, forced = false)
return true if current_user.id == user_id return true if current_user.id == user_id
......
...@@ -3,11 +3,15 @@ module Lfs ...@@ -3,11 +3,15 @@ module Lfs
prepend EE::Lfs::LockFileService prepend EE::Lfs::LockFileService
def execute def execute
if current_lock unless can?(current_user, :push_code, project)
error('already created lock', 409, current_lock) raise Gitlab::GitAccess::UnauthorizedError, 'You have no permissions'
else
create_lock!
end end
create_lock!
rescue ActiveRecord::RecordNotUnique
error('already created lock', 409, current_lock)
rescue Gitlab::GitAccess::UnauthorizedError => ex
error(ex.message, 403)
rescue => ex rescue => ex
error(ex.message, 500) error(ex.message, 500)
end end
...@@ -15,7 +19,7 @@ module Lfs ...@@ -15,7 +19,7 @@ module Lfs
private private
def current_lock def current_lock
@current_lock ||= project.lfs_file_locks.find_by(path: params[:path]) project.lfs_file_locks.find_by(path: params[:path])
end end
def create_lock! def create_lock!
......
...@@ -3,11 +3,15 @@ module Lfs ...@@ -3,11 +3,15 @@ module Lfs
prepend EE::Lfs::UnlockFileService prepend EE::Lfs::UnlockFileService
def execute def execute
@lock = project.lfs_file_locks.find_by(id: params[:id]) unless can?(current_user, :push_code, project)
raise Gitlab::GitAccess::UnauthorizedError, 'You have no permissions'
return error('Lock not found', 404) unless @lock end
unlock_file unlock_file
rescue Gitlab::GitAccess::UnauthorizedError => ex
error(ex.message, 403)
rescue ActiveRecord::RecordNotFound
error('Lock not found', 404)
rescue => ex rescue => ex
error(ex.message, 500) error(ex.message, 500)
end end
...@@ -16,15 +20,16 @@ module Lfs ...@@ -16,15 +20,16 @@ module Lfs
def unlock_file def unlock_file
forced = params[:force] == true forced = params[:force] == true
lock = project.lfs_file_locks.find(params[:id])
if @lock.can_be_unlocked_by?(current_user, forced) if lock.can_be_unlocked_by?(current_user, forced)
@lock.destroy! lock.destroy!
success(lock: @lock, http_status: :ok) success(lock: lock, http_status: :ok)
elsif forced elsif forced
error('You must have master access to force delete a lock', 403) error('You must have master access to force delete a lock', 403)
else else
error("#{@lock.path} is locked by GitLab User #{@lock.user_id}", 403) error("#{lock.path} is locked by GitLab User #{lock.user_id}", 403)
end end
end end
end end
......
...@@ -8,5 +8,7 @@ class CreateLfsFileLocks < ActiveRecord::Migration ...@@ -8,5 +8,7 @@ class CreateLfsFileLocks < ActiveRecord::Migration
t.string :path t.string :path
t.datetime :created_at, null: false t.datetime :created_at, null: false
end end
add_index :lfs_file_locks, [:path, :project_id], unique: true
end end
end end
...@@ -1309,6 +1309,7 @@ ActiveRecord::Schema.define(version: 20180204200836) do ...@@ -1309,6 +1309,7 @@ ActiveRecord::Schema.define(version: 20180204200836) do
t.datetime "created_at", null: false t.datetime "created_at", null: false
end end
add_index "lfs_file_locks", ["path", "project_id"], name: "index_lfs_file_locks_on_path_and_project_id", unique: true, using: :btree
add_index "lfs_file_locks", ["project_id"], name: "index_lfs_file_locks_on_project_id", using: :btree add_index "lfs_file_locks", ["project_id"], name: "index_lfs_file_locks_on_project_id", using: :btree
add_index "lfs_file_locks", ["user_id"], name: "index_lfs_file_locks_on_user_id", using: :btree add_index "lfs_file_locks", ["user_id"], name: "index_lfs_file_locks_on_user_id", using: :btree
......
...@@ -10,7 +10,6 @@ describe LfsFileLock do ...@@ -10,7 +10,6 @@ describe LfsFileLock do
it { is_expected.to validate_presence_of(:project_id) } it { is_expected.to validate_presence_of(:project_id) }
it { is_expected.to validate_presence_of(:user_id) } it { is_expected.to validate_presence_of(:user_id) }
it { is_expected.to validate_presence_of(:path) } it { is_expected.to validate_presence_of(:path) }
it { is_expected.to validate_uniqueness_of(:path).scoped_to(:project_id) }
describe '#can_be_unlocked_by?' do describe '#can_be_unlocked_by?' do
let(:developer) { create(:user) } let(:developer) { create(:user) }
......
...@@ -46,7 +46,7 @@ describe 'Git LFS File Locking API' do ...@@ -46,7 +46,7 @@ describe 'Git LFS File Locking API' do
lock_file('README.md', developer) lock_file('README.md', developer)
end end
it 'responds with 409' do it 'return an error message' do
post_lfs_json url, body, headers post_lfs_json url, body, headers
expect(response).to have_gitlab_http_status(409) expect(response).to have_gitlab_http_status(409)
...@@ -54,10 +54,16 @@ describe 'Git LFS File Locking API' do ...@@ -54,10 +54,16 @@ describe 'Git LFS File Locking API' do
expect(json_response.keys).to match_array(%w(lock message documentation_url)) expect(json_response.keys).to match_array(%w(lock message documentation_url))
expect(json_response['message']).to match(/already created lock/) expect(json_response['message']).to match(/already created lock/)
end end
it 'returns the existen lock' do
post_lfs_json url, body, headers
expect(json_response['lock']['path']).to eq('README.md')
end
end end
context 'without an existent lock' do context 'without an existent lock' do
it 'responds with 201' do it 'creates the lock' do
post_lfs_json url, body, headers post_lfs_json url, body, headers
expect(response).to have_gitlab_http_status(201) expect(response).to have_gitlab_http_status(201)
...@@ -73,7 +79,7 @@ describe 'Git LFS File Locking API' do ...@@ -73,7 +79,7 @@ describe 'Git LFS File Locking API' do
include_examples 'unauthorized request' include_examples 'unauthorized request'
it 'responds with 200' do it 'returns the list of locked files' do
lock_file('README.md', developer) lock_file('README.md', developer)
lock_file('README', developer) lock_file('README', developer)
...@@ -92,7 +98,7 @@ describe 'Git LFS File Locking API' do ...@@ -92,7 +98,7 @@ describe 'Git LFS File Locking API' do
include_examples 'unauthorized request' include_examples 'unauthorized request'
it 'responds with 200' do it 'returns the list of locked files grouped by owner' do
lock_file('README.md', master) lock_file('README.md', master)
lock_file('README', developer) lock_file('README', developer)
...@@ -115,10 +121,14 @@ describe 'Git LFS File Locking API' do ...@@ -115,10 +121,14 @@ describe 'Git LFS File Locking API' do
include_examples 'unauthorized request' include_examples 'unauthorized request'
context 'with an existent lock' do context 'with an existent lock' do
it 'responds with 200' do it 'deletes the lock' do
post_lfs_json url, nil, headers post_lfs_json url, nil, headers
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
end
it 'returns the deleted lock' do
post_lfs_json url, nil, headers
expect(json_response['lock'].keys).to match_array(%w(id path locked_at owner)) expect(json_response['lock'].keys).to match_array(%w(id path locked_at owner))
end end
......
require 'spec_helper' require 'spec_helper'
describe Lfs::LockFileService do describe Lfs::LockFileService do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:user) { create(:user) } let(:current_user) { create(:user) }
subject { described_class.new(project, user, params) } subject { described_class.new(project, current_user, params) }
describe '#execute' do describe '#execute' do
let(:params) { { path: 'README.md' } } let(:params) { { path: 'README.md' } }
context 'with an existent lock' do context 'when not authorized' do
let!(:lock) { create(:lfs_file_lock, project: project) }
it "doesn't succeed" do it "doesn't succeed" do
expect(subject.execute[:status]).to eq(:error) result = subject.execute
end
it "doesn't create the Lock" do expect(result[:status]).to eq(:error)
expect do expect(result[:http_status]).to eq(403)
subject.execute expect(result[:message]).to eq('You have no permissions')
end.not_to change { LfsFileLock.count }
end end
end end
context 'without an existent lock' do context 'when authorized' do
it "succeeds" do before do
expect(subject.execute[:status]).to eq(:success) project.add_developer(current_user)
end end
it "creates the Lock" do context 'with an existent lock' do
expect do let!(:lock) { create(:lfs_file_lock, project: project) }
subject.execute
end.to change { LfsFileLock.count }.by(1) it "doesn't succeed" do
expect(subject.execute[:status]).to eq(:error)
end
it "doesn't create the Lock" do
expect do
subject.execute
end.not_to change { LfsFileLock.count }
end
end end
end
context 'when an error is raised' do context 'without an existent lock' do
it "doesn't succeed" do it "succeeds" do
allow_any_instance_of(described_class).to receive(:create_lock!).and_raise(StandardError) expect(subject.execute[:status]).to eq(:success)
end
it "creates the Lock" do
expect do
subject.execute
end.to change { LfsFileLock.count }.by(1)
end
end
context 'when an error is raised' do
it "doesn't succeed" do
allow_any_instance_of(described_class).to receive(:create_lock!).and_raise(StandardError)
expect(subject.execute[:status]).to eq(:error) expect(subject.execute[:status]).to eq(:error)
end
end end
end end
end end
......
...@@ -3,85 +3,101 @@ require 'spec_helper' ...@@ -3,85 +3,101 @@ require 'spec_helper'
describe Lfs::UnlockFileService do describe Lfs::UnlockFileService do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:current_user) { create(:user) } let(:current_user) { create(:user) }
let(:lock_author) { create(:user) } let(:lock_author) { create(:user) }
let!(:lock) { create(:lfs_file_lock, user: lock_author, project: project) } let!(:lock) { create(:lfs_file_lock, user: lock_author, project: project) }
let(:params) { {} } let(:params) { {} }
subject { described_class.new(project, current_user, params) } subject { described_class.new(project, current_user, params) }
describe '#execute' do describe '#execute' do
context 'when lock does not exists' do context 'when not authorized' do
let(:params) { { id: 123 } }
it "doesn't succeed" do it "doesn't succeed" do
result = subject.execute result = subject.execute
expect(result[:status]).to eq(:error) expect(result[:status]).to eq(:error)
expect(result[:http_status]).to eq(404) expect(result[:http_status]).to eq(403)
expect(result[:message]).to eq('You have no permissions')
end end
end end
context 'when unlocked by the author' do context 'when authorized' do
let(:current_user) { lock_author } before do
let(:params) { { id: lock.id } } project.add_developer(current_user)
it "succeeds" do
result = subject.execute
expect(result[:status]).to eq(:success)
expect(result[:lock]).to be_present
end end
end
context 'when unlocked by a different user' do
let(:current_user) { create(:user) }
let(:params) { { id: lock.id } }
it "doesn't succeed" do context 'when lock does not exists' do
result = subject.execute let(:params) { { id: 123 } }
it "doesn't succeed" do
result = subject.execute
expect(result[:status]).to eq(:error) expect(result[:status]).to eq(:error)
expect(result[:message]).to match(/is locked by GitLab User #{lock_author.id}/) expect(result[:http_status]).to eq(404)
expect(result[:http_status]).to eq(403) end
end end
end
context 'when forced' do context 'when unlocked by the author' do
let(:developer) { create(:user) } let(:current_user) { lock_author }
let(:master) { create(:user) } let(:params) { { id: lock.id } }
before do it "succeeds" do
project.add_developer(developer) result = subject.execute
project.add_master(master)
end
context 'by a regular user' do expect(result[:status]).to eq(:success)
let(:current_user) { developer } expect(result[:lock]).to be_present
let(:params) do
{ id: lock.id,
force: true }
end end
end
context 'when unlocked by a different user' do
let(:current_user) { create(:user) }
let(:params) { { id: lock.id } }
it "doesn't succeed" do it "doesn't succeed" do
result = subject.execute result = subject.execute
expect(result[:status]).to eq(:error) expect(result[:status]).to eq(:error)
expect(result[:message]).to match(/You must have master access/) expect(result[:message]).to match(/is locked by GitLab User #{lock_author.id}/)
expect(result[:http_status]).to eq(403) expect(result[:http_status]).to eq(403)
end end
end end
context 'by a master user' do context 'when forced' do
let(:current_user) { master } let(:developer) { create(:user) }
let(:params) do let(:master) { create(:user) }
{ id: lock.id,
force: true } before do
project.add_developer(developer)
project.add_master(master)
end end
it "succeeds" do context 'by a regular user' do
result = subject.execute let(:current_user) { developer }
let(:params) do
{ id: lock.id,
force: true }
end
expect(result[:status]).to eq(:success) it "doesn't succeed" do
expect(result[:lock]).to be_present result = subject.execute
expect(result[:status]).to eq(:error)
expect(result[:message]).to match(/You must have master access/)
expect(result[:http_status]).to eq(403)
end
end
context 'by a master user' do
let(:current_user) { master }
let(:params) do
{ id: lock.id,
force: true }
end
it "succeeds" do
result = subject.execute
expect(result[:status]).to eq(:success)
expect(result[:lock]).to be_present
end
end end
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