diff --git a/app/models/repository/git.rb b/app/models/repository/git.rb index 52b91fc36a9..191347c758a 100644 --- a/app/models/repository/git.rb +++ b/app/models/repository/git.rb @@ -182,10 +182,17 @@ class Repository::Git < Repository private def validity_of_local_url + return if managed? + parsed = URI.parse root_url.presence || url if parsed.scheme == "ssh" errors.add :url, :must_not_be_ssh end + + if OpenProject::SCM::LocalPathValidator.points_to_openproject_directory?(url) || + OpenProject::SCM::LocalPathValidator.points_to_openproject_directory?(root_url) + errors.add :url, :must_not_point_to_openproject_directory + end rescue StandardError => e Rails.logger.error "Failed to parse repository url for validation: #{e}" errors.add :url, :invalid_url diff --git a/app/models/repository/subversion.rb b/app/models/repository/subversion.rb index 0f2098c45ce..06c48a67cd6 100644 --- a/app/models/repository/subversion.rb +++ b/app/models/repository/subversion.rb @@ -33,6 +33,7 @@ require "open_project/scm/adapters/subversion" class Repository::Subversion < Repository validates :url, presence: true validates :url, format: { with: /\A(http|https|svn(\+[^\s:\/\\]+)?|file):\/\/.+\z/i } + validate :validity_of_local_url def self.scm_adapter_class OpenProject::SCM::Adapters::Subversion @@ -136,6 +137,15 @@ class Repository::Subversion < Repository private + def validity_of_local_url + return if managed? + + if OpenProject::SCM::LocalPathValidator.points_to_openproject_directory?(url) || + OpenProject::SCM::LocalPathValidator.points_to_openproject_directory?(root_url) + errors.add :url, :must_not_point_to_openproject_directory + end + end + # Returns the relative url of the repository # Eg: root_url = file:///var/svn/foo # url = file:///var/svn/foo/bar diff --git a/config/locales/en.yml b/config/locales/en.yml index 7f5a669513c..ec0fd4ca10f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2078,6 +2078,7 @@ en: not_whitelisted: "is not allowed by the configuration." invalid_url: "is not a valid repository URL or path." must_not_be_ssh: "must not be an SSH url." + must_not_point_to_openproject_directory: "must not point to an OpenProject-managed repository directory." no_directory: "is not a directory." role: attributes: diff --git a/lib/open_project/scm/local_path_validator.rb b/lib/open_project/scm/local_path_validator.rb new file mode 100644 index 00000000000..1cf410ae9d9 --- /dev/null +++ b/lib/open_project/scm/local_path_validator.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module OpenProject + module SCM + module LocalPathValidator + module_function + + def points_to_openproject_directory?(value) + path = local_path(value) + return false if path.blank? + + forbidden_roots.any? { |root| path_within_root?(path, root) } + end + + def local_path(value) + return if value.blank? + + parsed = URI.parse(value) + + if parsed.scheme == "file" + return File.expand_path(parsed.path) + end + + return File.expand_path(value) if parsed.scheme.nil? && value.start_with?("/") + rescue URI::Error + return + end + + def forbidden_roots + roots = [ + OpenProject::Configuration.scm_local_checkout_path, + Repository::Git.managed_root, + Repository::Subversion.managed_root + ] + + roots.compact_blank.map { |root| File.expand_path(root) }.uniq + end + + def path_within_root?(path, root) + path == root || path.start_with?("#{root}/") + end + end + end +end diff --git a/spec/models/repository/git_spec.rb b/spec/models/repository/git_spec.rb index f855bb1f21b..5136033f613 100644 --- a/spec/models/repository/git_spec.rb +++ b/spec/models/repository/git_spec.rb @@ -155,6 +155,10 @@ RSpec.describe Repository::Git do describe "URL validation" do let(:instance) { build(:repository_git, url:) } + let(:checkout_root) { "/tmp/git-checkouts" } + let(:managed_git_root) { "/tmp/managed-git" } + let(:managed_svn_root) { "/tmp/managed-svn" } + let(:config) { { manages: managed_git_root } } shared_examples "repository url is valid" do it "is valid" do @@ -188,6 +192,60 @@ RSpec.describe Repository::Git do expect(instance.errors.full_messages).to eq(["URL must not be an SSH url."]) end end + + context "path points to OpenProject-managed directory" do + let(:url) { "#{managed_git_root}/sample-project/.git" } + + before do + allow(OpenProject::Configuration).to receive(:scm_local_checkout_path).and_return(checkout_root) + allow(Repository::Subversion).to receive(:managed_root).and_return(managed_svn_root) + end + + it "is invalid" do + expect(instance).not_to be_valid + expect(instance.errors.full_messages).to include("URL must not point to an OpenProject-managed repository directory.") + end + end + + context "file URL points to OpenProject-managed directory" do + let(:url) { "file://#{checkout_root}/sample-project/.git" } + + before do + allow(OpenProject::Configuration).to receive(:scm_local_checkout_path).and_return(checkout_root) + allow(Repository::Subversion).to receive(:managed_root).and_return(managed_svn_root) + end + + it "is invalid" do + expect(instance).not_to be_valid + expect(instance.errors.full_messages).to include("URL must not point to an OpenProject-managed repository directory.") + end + end + + context "root_url points to OpenProject-managed directory" do + let(:url) { "https://example.org/repo.git" } + + before do + instance.root_url = "#{managed_svn_root}/sample-project" + allow(OpenProject::Configuration).to receive(:scm_local_checkout_path).and_return(checkout_root) + allow(Repository::Subversion).to receive(:managed_root).and_return(managed_svn_root) + end + + it "is invalid" do + expect(instance).not_to be_valid + expect(instance.errors.full_messages).to include("URL must not point to an OpenProject-managed repository directory.") + end + end + + context "path only matches root prefix" do + let(:url) { "#{checkout_root}-other/repo.git" } + + before do + allow(OpenProject::Configuration).to receive(:scm_local_checkout_path).and_return(checkout_root) + allow(Repository::Subversion).to receive(:managed_root).and_return(managed_svn_root) + end + + it_behaves_like "repository url is valid" + end end describe "with an actual repository" do diff --git a/spec/models/repository/subversion_spec.rb b/spec/models/repository/subversion_spec.rb index 61d860a16b1..8d6368a8025 100644 --- a/spec/models/repository/subversion_spec.rb +++ b/spec/models/repository/subversion_spec.rb @@ -147,6 +147,48 @@ RSpec.describe Repository::Subversion do end end + describe "URL validation" do + let(:checkout_root) { "/tmp/svn-checkouts" } + let(:managed_git_root) { "/tmp/managed-git" } + let(:managed_svn_root) { "/tmp/managed-svn" } + let(:config) { { manages: managed_svn_root } } + + before do + allow(OpenProject::Configuration).to receive(:scm_local_checkout_path).and_return(checkout_root) + allow(Repository::Git).to receive(:managed_root).and_return(managed_git_root) + end + + context "file URL points to OpenProject-managed directory" do + let(:instance) { build(:repository_subversion, url: "file://#{managed_git_root}/sample-project/.git") } + + it "is invalid" do + expect(instance).not_to be_valid + expect(instance.errors.full_messages).to include("URL must not point to an OpenProject-managed repository directory.") + end + end + + context "root_url points to OpenProject-managed directory" do + let(:instance) { build(:repository_subversion, url: "https://example.org/svn/repo") } + + before do + instance.root_url = "file://#{checkout_root}/sample-project" + end + + it "is invalid" do + expect(instance).not_to be_valid + expect(instance.errors.full_messages).to include("URL must not point to an OpenProject-managed repository directory.") + end + end + + context "with non-managed file URL" do + let(:instance) { build(:repository_subversion, url: "file:///srv/svn/public") } + + it "is valid" do + expect(instance).to be_valid + end + end + end + describe "with a remote repository" do let(:instance) do build(:repository_subversion,