| author | |
| committer | |
| log | d7e04000d859a9030b8475d00cf8b411b7343956 |
| tree | 825bfa749b2b0b3f38d1a145a0c56b256c587c28 |
| parent | 58a05c402b8eaf144eb87dba875171880a10b09d |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
Enable the same receive-pack setting Shale uses for its own repositories while retaining Shale's push ACL. Translate forwarded CGI errors into real HTTP errors at the Git route.
Validated owner discovery and a real jj push on a disposable config clone; anonymous, invalid and non-owner discovery and writes stay denied.
Assisted-by: gpt-6.1-sol3 files changed, 24 insertions(+), 3 deletions(-)
service/shale/prepare.py+9-1| ... | ... | @@ -3,11 +3,19 @@ |
| 3 | 3 | import json |
| 4 | 4 | import os |
| 5 | 5 | from pathlib import Path |
| 6 | import re | |
| 6 | 7 | import sqlite3 |
| 7 | 8 | import sys |
| 8 | 9 | |
| 9 | 10 | data = json.load(sys.stdin) |
| 10 | path = Path(data["hostRoot"]) / "data/astheno.shale.db" | |
| 11 | root = Path(data["hostRoot"]) | |
| 12 | for config in (root / "repositories_owned").glob("*/config"): | |
| 13 | text = config.read_text() | |
| 14 | if not re.search(r"(?im)^\s*receivepack\s*=", text): | |
| 15 | # Shale authorizes pushes before CGI; its own repository initializer enables this. | |
| 16 | with config.open("a") as file: | |
| 17 | file.write("\n[http]\n\treceivepack = true\n") | |
| 18 | path = root / "data/astheno.shale.db" | |
| 11 | 19 | if path.exists(): |
| 12 | 20 | domain = os.environ["STUDIO_DOMAIN"] |
| 13 | 21 | old, new = "auth." + domain + "/realms/master", "snowglobe." + domain |
tools/import-forgejo-to-shale.py+1-1| ... | ... | @@ -155,7 +155,7 @@ def main(): |
| 155 | 155 | for folder in ('objects', 'refs'): |
| 156 | 156 | (directory / folder).mkdir(mode=0o700) |
| 157 | 157 | os.chown(directory / folder, stat.st_uid, stat.st_gid) |
| 158 | (directory / 'config').write_text('[core]\nrepositoryformatversion = 0\nbare = true\n') | |
| 158 | (directory / 'config').write_text('[core]\nrepositoryformatversion = 0\nbare = true\n[http]\nreceivepack = true\n') | |
| 159 | 159 | access = 'private' if row['is_private'] else 'public' |
| 160 | 160 | cursor = db.execute( |
| 161 | 161 | 'INSERT INTO repositories(uuid,owner,created_on,name,description,' + ','.join(ACCESS) + ',last_updated) ' |
tools/router.py+14-1| ... | ... | @@ -83,6 +83,18 @@ def shale_write_routes(host): |
| 83 | 83 | " }", ' respond @shale_unsafe_origin "Forbidden" 403'] |
| 84 | 84 | |
| 85 | 85 | |
| 86 | def shale_git_routes(upstreams): | |
| 87 | lines = [" @shale_git path */info/refs */git-upload-pack */git-receive-pack", | |
| 88 | " handle @shale_git {", f" reverse_proxy {upstreams} {{"] | |
| 89 | # Shale forwards CGI Status as a header instead of the HTTP status. | |
| 90 | for status in (403, 404, 500): | |
| 91 | lines += [f' @cgi_{status} header Status "{status} *"', | |
| 92 | f" handle_response @cgi_{status} {{", | |
| 93 | " header {", " -Status", " defer", " }", | |
| 94 | f" copy_response {status}", " }"] | |
| 95 | return [*lines, " }", " }"] | |
| 96 | ||
| 97 | ||
| 86 | 98 | def render(token): |
| 87 | 99 | with open(os.environ["STUDIO_PROXY_TOKEN_FILE"]) as file: |
| 88 | 100 | dashboard_proof = file.read().strip() |
| ... | ... | @@ -178,7 +190,8 @@ def render(token): |
| 178 | 190 | if service == "shale" or re.fullmatch(r"shale-preview-[0-9a-f]{8}", service): |
| 179 | 191 | # Keep the Origin guard before every static/proxy handle; |
| 180 | 192 | # otherwise Caddy sorts those handles ahead of respond. |
| 181 | lines += [" route {", *shale_write_routes(host)] | |
| 193 | lines += [" route {", *shale_write_routes(host), | |
| 194 | *shale_git_routes(" ".join(sorted(route["upstreams"])))] | |
| 182 | 195 | if re.fullmatch(r"shale-preview-[0-9a-f]{8}", service): |
| 183 | 196 | lines += shale_attachment_routes(" ".join(sorted(route["upstreams"])), |
| 184 | 197 | "/srv/staging/" + service) |