| 1 | # frozen_string_literal: true |
| 2 | |
| 3 | require "rails_helper" |
| 4 | require "tmpdir" |
| 5 | require "fileutils" |
| 6 | |
| 7 | # Mirror repos are read-only over git: a push (receive-pack) must be rejected |
| 8 | # with a message the user actually sees, while clone/fetch and normal repos are |
| 9 | # unaffected. |
| 10 | RSpec.describe "Git HTTP mirror read-only enforcement", type: :request do |
| 11 | let(:user) { User.create!(smbcloud_id: 55, email: "m@example.com", username: "mirroruser") } |
| 12 | |
| 13 | around do |example| |
| 14 | Dir.mktmpdir("git-http-mirror") do |tmp| |
| 15 | @bare = File.join(tmp, "mirrored.git") |
| 16 | system("git", "init", "--bare", "-q", @bare, exception: true) |
| 17 | example.run |
| 18 | end |
| 19 | end |
| 20 | |
| 21 | let!(:mirror) do |
| 22 | user.repositories.create!( |
| 23 | name: "mirrored", disk_path: @bare, default_branch: "main", |
| 24 | mirror: true, upstream_url: "https://github.com/owner/mirrored.git", mirror_status: "ok" |
| 25 | ) |
| 26 | end |
| 27 | |
| 28 | it "rejects a push to a mirror with a clear git error message" do |
| 29 | get "/mirroruser/mirrored.git/info/refs", params: { service: "git-receive-pack" } |
| 30 | |
| 31 | expect(response).to have_http_status(:ok) |
| 32 | expect(response.body).to include("ERR") |
| 33 | expect(response.body).to include("read-only mirror") |
| 34 | expect(response.body).to include("Detach") |
| 35 | end |
| 36 | |
| 37 | it "also blocks the receive-pack RPC endpoint directly" do |
| 38 | post "/mirroruser/mirrored.git/git-receive-pack" |
| 39 | # The advertisement carries the ERR packet rather than proxying to git. |
| 40 | expect(response.body).to include("read-only mirror") |
| 41 | end |
| 42 | |
| 43 | it "still allows fetch/clone (upload-pack) from a public mirror" do |
| 44 | get "/mirroruser/mirrored.git/info/refs", params: { service: "git-upload-pack" } |
| 45 | expect(response).to have_http_status(:ok) |
| 46 | expect(response.body).not_to include("read-only mirror") |
| 47 | end |
| 48 | |
| 49 | it "does not block pushes on a normal (non-mirror) repo" do |
| 50 | normal_dir = File.join(Dir.tmpdir, "normal-#{SecureRandom.hex(4)}.git") |
| 51 | system("git", "init", "--bare", "-q", normal_dir, exception: true) |
| 52 | user.repositories.create!(name: "normal", disk_path: normal_dir, default_branch: "main") |
| 53 | |
| 54 | get "/mirroruser/normal.git/info/refs", params: { service: "git-receive-pack" } |
| 55 | # No mirror rejection; instead it falls through to auth (401 challenge). |
| 56 | expect(response.body).not_to include("read-only mirror") |
| 57 | expect(response).to have_http_status(:unauthorized) |
| 58 | ensure |
| 59 | FileUtils.rm_rf(normal_dir) if normal_dir |
| 60 | end |
| 61 | end |