| 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,11 +3,19 @@ |
| 3 | import json | 3 | import json |
| 4 | import os | 4 | import os |
| 5 | from pathlib import Path | 5 | from pathlib import Path |
| 6 | import re | ||
| 6 | import sqlite3 | 7 | import sqlite3 |
| 7 | import sys | 8 | import sys |
| 8 | 9 | ||
| 9 | data = json.load(sys.stdin) | 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 | if path.exists(): | 19 | if path.exists(): |
| 12 | domain = os.environ["STUDIO_DOMAIN"] | 20 | domain = os.environ["STUDIO_DOMAIN"] |
| 13 | old, new = "auth." + domain + "/realms/master", "snowglobe." + domain | 21 | old, new = "auth." + domain + "/realms/master", "snowglobe." + domain |
tools/import-forgejo-to-shale.py+1-1| ... | @@ -155,7 +155,7 @@ def main(): | ... | @@ -155,7 +155,7 @@ def main(): |
| 155 | for folder in ('objects', 'refs'): | 155 | for folder in ('objects', 'refs'): |
| 156 | (directory / folder).mkdir(mode=0o700) | 156 | (directory / folder).mkdir(mode=0o700) |
| 157 | os.chown(directory / folder, stat.st_uid, stat.st_gid) | 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 | access = 'private' if row['is_private'] else 'public' | 159 | access = 'private' if row['is_private'] else 'public' |
| 160 | cursor = db.execute( | 160 | cursor = db.execute( |
| 161 | 'INSERT INTO repositories(uuid,owner,created_on,name,description,' + ','.join(ACCESS) + ',last_updated) ' | 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,6 +83,18 @@ def shale_write_routes(host): |
| 83 | " }", ' respond @shale_unsafe_origin "Forbidden" 403'] | 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 | def render(token): | 98 | def render(token): |
| 87 | with open(os.environ["STUDIO_PROXY_TOKEN_FILE"]) as file: | 99 | with open(os.environ["STUDIO_PROXY_TOKEN_FILE"]) as file: |
| 88 | dashboard_proof = file.read().strip() | 100 | dashboard_proof = file.read().strip() |
| ... | @@ -178,7 +190,8 @@ def render(token): | ... | @@ -178,7 +190,8 @@ def render(token): |
| 178 | if service == "shale" or re.fullmatch(r"shale-preview-[0-9a-f]{8}", service): | 190 | if service == "shale" or re.fullmatch(r"shale-preview-[0-9a-f]{8}", service): |
| 179 | # Keep the Origin guard before every static/proxy handle; | 191 | # Keep the Origin guard before every static/proxy handle; |
| 180 | # otherwise Caddy sorts those handles ahead of respond. | 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 | if re.fullmatch(r"shale-preview-[0-9a-f]{8}", service): | 195 | if re.fullmatch(r"shale-preview-[0-9a-f]{8}", service): |
| 183 | lines += shale_attachment_routes(" ".join(sorted(route["upstreams"])), | 196 | lines += shale_attachment_routes(" ".join(sorted(route["upstreams"])), |
| 184 | "/srv/staging/" + service) | 197 | "/srv/staging/" + service) |