Skip to content

Commit 236b5fc

Browse files
authored
feat(git): support Basic Auth for git-receive-pack (#2089)
Git CLI uses HTTP Basic Authentication when pushing, with the token as the password.
1 parent 5848109 commit 236b5fc

12 files changed

Lines changed: 300 additions & 101 deletions

File tree

.github/workflows/orion-server-deploy.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,12 @@ jobs:
7979
GCP_IMAGE_BASE="us-central1-docker.pkg.dev/${{ env.GCP_PROJECT_ID }}/${{ env.REPOSITORY }}"
8080
8181
TAG="${{ env.IMAGE_TAG_BASE }}-$ARCH_SUFFIX"
82-
AWS_CACHE_IMAGE="$AWS_IMAGE_BASE:buildcache-$ARCH_SUFFIX"
8382
CACHE_SCOPE="orion-server-$ARCH_SUFFIX"
8483
8584
docker buildx build \
8685
--platform "$PLATFORM" \
8786
--cache-from type=gha,scope=$CACHE_SCOPE \
88-
--cache-from type=registry,ref=$AWS_CACHE_IMAGE \
8987
--cache-to type=gha,mode=max,scope=$CACHE_SCOPE \
90-
--cache-to type=registry,ref=$AWS_CACHE_IMAGE,mode=max \
9188
--provenance=false \
9289
--sbom=false \
9390
-f orion-server/Dockerfile \

Cargo.lock

Lines changed: 18 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ futures-util = "0.3.32"
6262
axum = { version = "0.8.9", features = ["macros", "json"] }
6363
axum-extra = "0.12.6"
6464
russh = "0.55.0"
65-
tower-http = "0.6.10"
65+
tower-http = "0.6.11"
6666
tower = "0.5.3"
6767
tower-sessions = { version = "0.15", features = ["memory-store"] }
6868
time = { version = "0.3.47", features = ["serde"] }
@@ -97,7 +97,7 @@ uuid = "1.23.1"
9797
regex = "1.12.3"
9898
ed25519-dalek = "2.2.0"
9999
ctrlc = "3.5.2"
100-
cedar-policy = "4.10.0"
100+
cedar-policy = "4.11.0"
101101
secp256k1 = "0.31.1"
102102
pgp = "0.19.0"
103103
base64 = "0.22.1"
@@ -106,7 +106,7 @@ utoipa = { version = "5.5.0", features = ["chrono"] }
106106
utoipa-axum = "0.2.0"
107107
utoipa-swagger-ui = "9.0.2"
108108
tempfile = "3.27.0"
109-
dashmap = "6.1.0"
109+
dashmap = "6.2.1"
110110
once_cell = "1.21.4"
111111
serial_test = "3.4.0"
112112
sysinfo = "0.39.2"

mono/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ utoipa = { workspace = true, features = ["axum_extras"] }
7171
utoipa-axum = { workspace = true }
7272
utoipa-swagger-ui = { workspace = true, features = ["axum"] }
7373
once_cell = { workspace = true }
74+
base64 = { workspace = true }
7475

7576

7677
[target.'cfg(not(windows))'.dependencies]

mono/Dockerfile

Lines changed: 29 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
# ---------- planner stage ----------
2-
# This stage analyzes the Cargo workspace and produces a dependency recipe.
3-
# The recipe will change ONLY when Cargo.toml changes.
1+
# ────── Stage 0: Chef ──────
2+
# Downloads and installs cargo-chef (prebuilt binary, no Rust compilation needed).
43
FROM rust:1.95.0 AS chef
54

65
ARG TARGETARCH=amd64
@@ -11,7 +10,6 @@ WORKDIR /opt/mega
1110
# Prebuilt cargo-chef (avoids compiling it on every cold planner layer).
1211
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl \
1312
&& rm -rf /var/lib/apt/lists/* \
14-
&& set -eux \
1513
&& case "$TARGETARCH" in \
1614
amd64) chef_arch=x86_64-unknown-linux-musl ;; \
1715
arm64) chef_arch=aarch64-unknown-linux-gnu ;; \
@@ -20,6 +18,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates
2018
&& curl -fsSL "https://github.com/LukeMathWalker/cargo-chef/releases/download/v${CARGO_CHEF_VERSION}/cargo-chef-${chef_arch}.tar.gz" \
2119
| tar xz -C /usr/local/cargo/bin
2220

21+
# ────── Stage 1: Planner ──────
22+
# Copies all Cargo.toml files; cargo-chef emits recipe.json describing the full dependency graph.
23+
# This stage is cached unless a Cargo.toml changes.
24+
FROM chef AS planner
25+
26+
WORKDIR /opt/mega
27+
2328
COPY Cargo.toml ./
2429
COPY api-model/Cargo.toml api-model/
2530
COPY ceres/Cargo.toml ceres/
@@ -38,14 +43,13 @@ COPY orion-server/bellatrix/Cargo.toml orion-server/bellatrix/
3843
COPY saturn/Cargo.toml saturn/
3944
COPY vault/Cargo.toml vault/
4045

46+
# Named cache mounts let the builder stage inherit this cache via `from=planner`.
47+
RUN --mount=type=cache,target=/usr/local/cargo/registry,id=mono-registry \
48+
--mount=type=cache,target=/usr/local/cargo/git,id=mono-git \
49+
cargo chef prepare --bin mono --recipe-path recipe.json
4150

42-
# Generate a recipe describing the dependency graph
43-
RUN cargo chef prepare --bin mono --recipe-path recipe.json
44-
45-
46-
# ---------- builder stage ----------
47-
# This stage builds dependencies first (cached),
48-
# then builds the actual application binary.
51+
# ────── Stage 2: Builder ──────
52+
# Installs system libs, builds all dependencies (cached), then builds the mono binary.
4953
FROM chef AS builder
5054

5155
WORKDIR /opt/mega
@@ -60,56 +64,34 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
6064
libprotobuf-dev \
6165
&& rm -rf /var/lib/apt/lists/*
6266

63-
# Faster linking (release deps + final mono link).
67+
# Faster linking for release builds.
6468
ENV RUSTFLAGS="-C link-arg=-fuse-ld=mold"
6569

66-
# Copy the dependency recipe from the planner stage
67-
COPY --from=chef /opt/mega/recipe.json recipe.json
70+
# Inherit registry/git caches from the planner stage.
71+
COPY --from=planner /opt/mega/recipe.json recipe.json
6872

69-
# Build and cache all Rust dependencies
70-
# This layer will be reused as long as dependencies do not change
71-
RUN --mount=type=cache,target=/usr/local/cargo/registry \
72-
--mount=type=cache,target=/usr/local/cargo/git \
73-
cargo chef cook --bin mono --release --recipe-path recipe.json
73+
RUN --mount=type=cache,target=/usr/local/cargo/registry,id=mono-registry,from=planner \
74+
--mount=type=cache,target=/usr/local/cargo/git,id=mono-git,from=planner \
75+
cargo chef cook --release --recipe-path recipe.json
7476

75-
# Copy only the workspace sources AFTER dependencies are cached.
76-
# This avoids sending the entire repo context (which can be multiple GB) to the builder.
77-
COPY Cargo.toml ./
78-
COPY api-model ./api-model
79-
COPY ceres ./ceres
80-
COPY common ./common
81-
COPY config ./config
82-
COPY context ./context
83-
COPY io-orbit ./io-orbit
84-
COPY jupiter ./jupiter
85-
COPY mono ./mono
86-
COPY orion ./orion
87-
COPY orion-server ./orion-server
88-
COPY saturn ./saturn
89-
COPY vault ./vault
90-
91-
# Build the mono binary (fixed release)
92-
#
93-
# NOTE: Build output must be persisted into the image layer for the runtime stage `COPY`.
94-
# A buildkit cache mount is not committed into the layer, so we build into a cached dir
95-
# then copy the final binary into `/opt/mega/target/...` (regular filesystem).
96-
RUN --mount=type=cache,target=/usr/local/cargo/registry \
97-
--mount=type=cache,target=/usr/local/cargo/git \
77+
COPY . .
78+
79+
# Persist built binary into the image layer (buildkit cache mounts evaporate after build).
80+
RUN --mount=type=cache,target=/usr/local/cargo/registry,id=mono-registry,from=planner \
81+
--mount=type=cache,target=/usr/local/cargo/git,id=mono-git,from=planner \
9882
--mount=type=cache,target=/opt/mega/target-cache \
9983
CARGO_TARGET_DIR=/opt/mega/target-cache cargo build --release -p mono && \
10084
mkdir -p /opt/mega/target/release && \
10185
cp /opt/mega/target-cache/release/mono /opt/mega/target/release/mono
10286

103-
104-
# ---------- runtime stage ----------
105-
# This is the minimal runtime image containing only the binary and runtime deps
106-
FROM debian:bookworm-slim
87+
# ────── Stage 3: Runtime ──────
88+
FROM debian:trixie-slim
10789

10890
# Install runtime dependencies
10991
RUN apt-get update && apt-get install -y --no-install-recommends \
11092
ca-certificates \
11193
less \
112-
libssl3 \
94+
libssl3t64 \
11395
&& rm -rf /var/lib/apt/lists/*
11496

11597
# Copy the compiled binary and startup script

mono/src/git_protocol/http.rs

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use axum::{
55
body::Body,
66
http::{HeaderValue, Request, Response},
77
};
8+
use base64::Engine;
89
use bytes::{Bytes, BytesMut};
910
use ceres::{
1011
api_service::state::ProtocolApiState,
@@ -52,29 +53,50 @@ fn auth_failed() -> Result<Response<Body>, ProtocolError> {
5253
.status(401)
5354
.header(
5455
http::header::WWW_AUTHENTICATE,
55-
HeaderValue::from_static("Bearer realm=\"Mega\""),
56+
HeaderValue::from_static("Basic realm=\"Mega\", Bearer realm=\"Mega\""),
5657
)
5758
.body(Body::empty())
5859
.unwrap();
5960
Ok(resp)
6061
}
6162

63+
/// Parses Basic Auth header, returning the password (which is the token).
64+
/// The username is ignored since we only care about the token in the password field.
65+
fn basic_auth_password_from_authorization_value(value: &str) -> Option<String> {
66+
let stripped = value
67+
.strip_prefix("Basic ")
68+
.or_else(|| value.strip_prefix("basic "))?;
69+
let decoded = base64::engine::general_purpose::STANDARD
70+
.decode(stripped.trim())
71+
.ok()?;
72+
let decoded_str = String::from_utf8(decoded).ok()?;
73+
// Basic auth format: "username:password"
74+
Some(decoded_str.split(':').nth(1)?.to_owned())
75+
}
76+
6277
/// Uses [`crate::api::oauth::login_user_from_mono_access_token`] (same as [`crate::api::oauth::AccessTokenUser`]).
63-
async fn git_receive_pack_bearer_auth(
78+
/// Supports both Bearer tokens and Basic Auth (with token as password).
79+
async fn git_receive_pack_auth(
6480
state: &ProtocolApiState,
6581
pack_protocol: &mut SmartSession,
6682
headers: &http::HeaderMap,
6783
) -> Result<bool, ProtocolError> {
68-
let token = headers
69-
.get(AUTHORIZATION)
70-
.and_then(|v| v.to_str().ok())
71-
.and_then(bearer_token_from_authorization_value);
84+
let auth_header = headers.get(AUTHORIZATION).and_then(|v| v.to_str().ok());
85+
86+
// Try Bearer token first
87+
let token = auth_header.and_then(bearer_token_from_authorization_value);
88+
89+
// If no Bearer token, try Basic Auth (token as password)
90+
let token = token
91+
.map(String::from)
92+
.or_else(|| auth_header.and_then(basic_auth_password_from_authorization_value));
93+
7294
let Some(token) = token else {
7395
return Ok(false);
7496
};
7597

7698
let Some(user) =
77-
login_user_from_mono_access_token(&state.storage.user_storage(), token).await?
99+
login_user_from_mono_access_token(&state.storage.user_storage(), &token).await?
78100
else {
79101
return Ok(false);
80102
};
@@ -179,7 +201,7 @@ pub async fn git_receive_pack(
179201
) -> Result<Response<Body>, ProtocolError> {
180202
let mut pack_protocol =
181203
SmartSession::new(repo_path, ServiceType::ReceivePack, TransportProtocol::Http);
182-
if !git_receive_pack_bearer_auth(state, &mut pack_protocol, req.headers()).await? {
204+
if !git_receive_pack_auth(state, &mut pack_protocol, req.headers()).await? {
183205
return auth_failed();
184206
}
185207
// Convert the request body into a data stream.

moon/.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
lts/*
1+
20

0 commit comments

Comments
 (0)