feat: add auth broker p0 skeleton

This commit is contained in:
Codex
2026-05-21 13:03:47 +00:00
parent b10fd46414
commit 3d9f8b2f24
11 changed files with 1575 additions and 4 deletions
+414
View File
@@ -0,0 +1,414 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "ascii"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
[[package]]
name = "auth-broker"
version = "0.1.0"
dependencies = [
"chrono",
"serde",
"serde_json",
"tiny_http",
]
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bumpalo"
version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "cc"
version = "1.2.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chrono"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [
"iana-time-zone",
"num-traits",
"windows-link",
]
[[package]]
name = "chunked_transfer"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901"
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "futures-core"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-task"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-util"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "iana-time-zone"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "js-sys"
version = "0.3.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08"
dependencies = [
"cfg-if",
"futures-util",
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tiny_http"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82"
dependencies = [
"ascii",
"chunked_transfer",
"httpdate",
"log",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "wasm-bindgen"
version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441"
dependencies = [
"unicode-ident",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
@@ -0,0 +1,20 @@
[package]
name = "auth-broker"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "auth-broker"
path = "src/main.rs"
[profile.release]
codegen-units = 1
lto = "thin"
opt-level = "z"
strip = true
[dependencies]
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tiny_http = "0.12"
@@ -0,0 +1,24 @@
ARG AUTH_BROKER_RUST_IMAGE=rust:1.85-bookworm
FROM ${AUTH_BROKER_RUST_IMAGE} AS build
WORKDIR /build
ENV CARGO_BUILD_JOBS=1
COPY src/components/microservices/auth-broker/Cargo.toml ./Cargo.toml
COPY src/components/microservices/auth-broker/Cargo.lock ./Cargo.lock
RUN mkdir -p src \
&& printf 'fn main() { println!("dependency cache"); }\n' > src/main.rs \
&& cargo build --release \
&& rm -rf src
COPY src/components/microservices/auth-broker/src ./src
RUN touch src/main.rs && cargo build --release
FROM debian:bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=build /build/target/release/auth-broker /usr/local/bin/auth-broker
EXPOSE 4291
CMD ["auth-broker"]
@@ -0,0 +1,646 @@
use chrono::{SecondsFormat, Utc};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::collections::BTreeSet;
use std::env;
use std::fs::{create_dir_all, OpenOptions};
use std::io::{Read, Write};
use std::path::Path;
use std::time::Instant;
use tiny_http::{Header, Method, Request, Response, Server, StatusCode};
const DEFAULT_ALLOWED_REPO: &str = "pikasTech/unidesk";
const DEFAULT_CREDENTIAL_REF: &str = "github:unidesk-dev";
const MAX_BODY_BYTES: usize = 64 * 1024;
const CAPABILITIES: &[&str] = &[
"github.auth.status",
"github.issue.list",
"github.issue.read",
"github.pr.list",
"github.pr.read",
"github.pr.create",
"github.pr.comment.create",
"github.pr.preflight.dry-run",
];
#[derive(Clone)]
struct Config {
host: String,
port: u16,
credential_ref: String,
credential_kind: String,
credential_configured: bool,
allowed_repos: BTreeSet<String>,
log_file: Option<String>,
}
#[derive(Deserialize)]
struct Caller {
plane: Option<String>,
#[serde(rename = "taskId")]
task_id: Option<String>,
#[serde(rename = "queueId")]
queue_id: Option<String>,
}
#[derive(Deserialize)]
struct BrokerRequest {
#[serde(rename = "requestId")]
request_id: Option<String>,
caller: Option<Caller>,
repo: Option<String>,
operation: Option<String>,
#[serde(rename = "dryRun")]
dry_run: Option<bool>,
params: Option<Value>,
}
impl BrokerRequest {
fn caller_json(&self) -> Value {
caller_json(self.caller.as_ref())
}
}
#[derive(Serialize)]
struct AuditEvent {
#[serde(rename = "requestId")]
request_id: String,
#[serde(rename = "observedAt")]
observed_at: String,
caller: Value,
operation: String,
repo: String,
resource: Value,
#[serde(rename = "dryRun")]
dry_run: bool,
#[serde(rename = "credentialRef")]
credential_ref: String,
#[serde(rename = "credentialKind")]
credential_kind: String,
#[serde(rename = "credentialValuePrinted")]
credential_value_printed: bool,
upstream: Value,
status: u16,
ok: bool,
#[serde(rename = "failureKind")]
failure_kind: Option<String>,
#[serde(rename = "degradedReason")]
degraded_reason: Option<String>,
#[serde(rename = "runnerDisposition")]
runner_disposition: String,
retryable: bool,
#[serde(rename = "durationMs")]
duration_ms: u128,
redaction: Value,
}
fn now_iso() -> String {
Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true)
}
fn env_string(name: &str, fallback: &str) -> String {
env::var(name)
.ok()
.filter(|value| !value.trim().is_empty())
.unwrap_or_else(|| fallback.to_string())
}
fn env_flag(name: &str, fallback: bool) -> bool {
let raw = env::var(name).ok().filter(|value| !value.trim().is_empty());
match raw.as_deref().map(str::to_ascii_lowercase).as_deref() {
Some("1") | Some("true") | Some("yes") | Some("on") => true,
Some("0") | Some("false") | Some("no") | Some("off") => false,
_ => fallback,
}
}
fn config_from_env() -> Config {
let allowed_repos = env_string("AUTH_BROKER_ALLOWED_REPOS", DEFAULT_ALLOWED_REPO)
.split(',')
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
.collect::<BTreeSet<_>>();
let credential_ref = env_string("AUTH_BROKER_GITHUB_CREDENTIAL_REF", DEFAULT_CREDENTIAL_REF);
Config {
host: env_string("HOST", "0.0.0.0"),
port: env_string("PORT", "4291")
.parse::<u16>()
.ok()
.filter(|port| *port > 0)
.unwrap_or(4291),
credential_ref,
credential_kind: "github-rest-token-ref".to_string(),
credential_configured: env_flag("AUTH_BROKER_GITHUB_CONFIGURED", false),
allowed_repos,
log_file: env::var("AUTH_BROKER_AUDIT_LOG")
.ok()
.filter(|value| !value.trim().is_empty()),
}
}
fn json_response(value: Value, status: u16) -> Response<std::io::Cursor<Vec<u8>>> {
let body = serde_json::to_vec_pretty(&value).unwrap_or_else(|_| b"{\"ok\":false}".to_vec());
let header = Header::from_bytes(
&b"Content-Type"[..],
&b"application/json; charset=utf-8"[..],
)
.unwrap();
Response::from_data(body)
.with_status_code(StatusCode(status))
.with_header(header)
}
fn text_response(text: &str, status: u16) -> Response<std::io::Cursor<Vec<u8>>> {
Response::from_string(text.to_string()).with_status_code(StatusCode(status))
}
fn read_body(request: &mut Request) -> Result<String, String> {
let mut body = String::new();
request
.as_reader()
.take(MAX_BODY_BYTES as u64 + 1)
.read_to_string(&mut body)
.map_err(|error| error.to_string())?;
if body.len() > MAX_BODY_BYTES {
return Err("request body exceeds 65536 bytes".to_string());
}
Ok(body)
}
fn health_payload(config: &Config) -> Value {
json!({
"ok": true,
"service": "auth-broker",
"phase": "p0",
"github": {
"configured": config.credential_configured,
"credentialRef": config.credential_ref,
"credentialKind": config.credential_kind,
"valuesPrinted": false
},
"capabilities": CAPABILITIES,
"allowedRepos": config.allowed_repos.iter().collect::<Vec<_>>(),
"redaction": {
"valuesPrinted": false,
"secretKeysBlocked": ["token", "secret", "authorization", "cookie"]
}
})
}
fn request_id(input: Option<&String>) -> String {
input
.filter(|value| !value.trim().is_empty())
.cloned()
.unwrap_or_else(|| format!("authbroker-{}", Utc::now().timestamp_millis()))
}
fn caller_json(caller: Option<&Caller>) -> Value {
json!({
"plane": caller.and_then(|value| value.plane.clone()).unwrap_or_else(|| "unknown".to_string()),
"taskId": caller.and_then(|value| value.task_id.clone()),
"queueId": caller.and_then(|value| value.queue_id.clone())
})
}
fn operation_allowed(operation: &str) -> bool {
CAPABILITIES.iter().any(|item| *item == operation) && operation != "github.pr.preflight.dry-run"
}
fn resource_from_params(operation: &str, params: Option<&Value>) -> Value {
let Some(Value::Object(map)) = params else {
return json!(null);
};
match operation {
"github.issue.read" | "github.pr.read" | "github.pr.comment.create" => {
json!({ "number": map.get("number").and_then(Value::as_i64) })
}
"github.pr.create" => json!({
"base": map.get("base").and_then(Value::as_str),
"head": map.get("head").and_then(Value::as_str),
"titleChars": map.get("title").and_then(Value::as_str).map(str::len),
"bodyChars": map.get("body").and_then(Value::as_str).map(str::len)
}),
_ => json!(null),
}
}
fn upstream_plan(operation: &str, repo: &str, resource: &Value) -> Value {
let path = match operation {
"github.auth.status" => "/rate_limit".to_string(),
"github.issue.list" => format!("/repos/{repo}/issues"),
"github.issue.read" => format!(
"/repos/{repo}/issues/{}",
resource
.get("number")
.and_then(Value::as_i64)
.unwrap_or_default()
),
"github.pr.list" => format!("/repos/{repo}/pulls"),
"github.pr.read" => format!(
"/repos/{repo}/pulls/{}",
resource
.get("number")
.and_then(Value::as_i64)
.unwrap_or_default()
),
"github.pr.create" => format!("/repos/{repo}/pulls"),
"github.pr.comment.create" => format!(
"/repos/{repo}/issues/{}/comments",
resource
.get("number")
.and_then(Value::as_i64)
.unwrap_or_default()
),
_ => "/unsupported".to_string(),
};
let method = match operation {
"github.pr.create" | "github.pr.comment.create" => "POST",
_ => "GET",
};
json!({ "method": method, "path": path })
}
fn failure_response(
config: &Config,
request_id: &str,
failure_kind: &str,
status: u16,
runner_disposition: &str,
retryable: bool,
message: &str,
) -> Value {
json!({
"ok": false,
"requestId": request_id,
"failureKind": failure_kind,
"degradedReason": message,
"runnerDisposition": runner_disposition,
"retryable": retryable,
"message": message,
"credential": {
"credentialRef": config.credential_ref,
"credentialKind": config.credential_kind,
"configured": config.credential_configured,
"valuesPrinted": false
},
"status": status,
"next": [
"configure broker-held credential reference outside runner env",
"retry only after broker health reports github.configured=true"
],
"redaction": {
"valuesPrinted": false
}
})
}
fn dry_run_required(operation: &str, dry_run: bool) -> bool {
matches!(operation, "github.pr.create" | "github.pr.comment.create") && !dry_run
}
fn handle_github(config: &Config, mut request: Request) {
let start = Instant::now();
let mut status = 200;
let body = match read_body(&mut request) {
Ok(body) => body,
Err(message) => {
let id = request_id(None);
status = 400;
let result = failure_response(
config,
&id,
"validation-failed",
status,
"business-failed",
false,
&message,
);
let audit = audit_from_response(config, &result, start.elapsed().as_millis(), status);
write_audit(config, &audit);
let _ = request.respond(json_response(result, status));
return;
}
};
let parsed = serde_json::from_str::<BrokerRequest>(&body).map_err(|error| error.to_string());
let result = match parsed {
Ok(input) => {
let id = request_id(input.request_id.as_ref());
let repo = input
.repo
.clone()
.unwrap_or_else(|| DEFAULT_ALLOWED_REPO.to_string());
let operation = input.operation.clone().unwrap_or_default();
let dry_run = input.dry_run.unwrap_or(false);
let caller = input.caller_json();
if operation.is_empty() {
status = 400;
failure_response(
config,
&id,
"validation-failed",
status,
"business-failed",
false,
"operation is required",
)
} else if !config.allowed_repos.contains(&repo) {
status = 403;
failure_response(
config,
&id,
"repo-not-allowed",
status,
"business-failed",
false,
"repo is not allowed",
)
} else if !operation_allowed(&operation) {
status = 403;
failure_response(
config,
&id,
"operation-not-allowed",
status,
"business-failed",
false,
"operation is not allowed",
)
} else if !config.credential_configured {
status = 503;
failure_response(
config,
&id,
"auth-not-configured",
status,
"infra-blocked",
false,
"broker GitHub credential reference is not configured",
)
} else if dry_run_required(&operation, dry_run) {
status = 409;
failure_response(
config,
&id,
"dry-run-required",
status,
"business-failed",
false,
"P0 mutation operations require dryRun=true",
)
} else {
let resource = resource_from_params(&operation, input.params.as_ref());
let upstream = upstream_plan(&operation, &repo, &resource);
let writes_remote = false;
json!({
"ok": true,
"requestId": id,
"runnerDisposition": "ready",
"failureKind": null,
"degradedReason": null,
"repo": repo,
"caller": caller,
"operation": operation,
"dryRun": dry_run,
"planned": true,
"writesRemote": writes_remote,
"credential": {
"credentialRef": config.credential_ref,
"credentialKind": config.credential_kind,
"configured": true,
"valuesPrinted": false
},
"upstream": upstream,
"resource": resource,
"audit": {
"credentialValuePrinted": false,
"redaction": { "valuesPrinted": false }
}
})
}
}
Err(message) => {
let id = request_id(None);
status = 400;
failure_response(
config,
&id,
"validation-failed",
status,
"business-failed",
false,
&message,
)
}
};
let audit = audit_from_response(config, &result, start.elapsed().as_millis(), status);
write_audit(config, &audit);
let _ = request.respond(json_response(result, status));
}
fn handle_pr_preflight(config: &Config, mut request: Request) {
let start = Instant::now();
let mut status = 200;
let body = read_body(&mut request)
.ok()
.and_then(|body| serde_json::from_str::<Value>(&body).ok())
.unwrap_or_else(|| json!({}));
let id_source = body
.get("requestId")
.and_then(Value::as_str)
.map(String::from);
let id = request_id(id_source.as_ref());
let repo = body
.get("repo")
.and_then(Value::as_str)
.unwrap_or(DEFAULT_ALLOWED_REPO)
.to_string();
let result = if !config.allowed_repos.contains(&repo) {
status = 403;
failure_response(
config,
&id,
"repo-not-allowed",
status,
"business-failed",
false,
"repo is not allowed",
)
} else if !config.credential_configured {
status = 503;
failure_response(
config,
&id,
"auth-not-configured",
status,
"infra-blocked",
false,
"broker GitHub credential reference is not configured",
)
} else {
let base = body.get("base").and_then(Value::as_str).unwrap_or("master");
let head = body
.get("head")
.and_then(Value::as_str)
.unwrap_or("<head-branch>");
json!({
"ok": true,
"requestId": id,
"runnerDisposition": "ready",
"failureKind": null,
"degradedReason": null,
"tokenCoverage": {
"ok": true,
"source": "auth-broker",
"scope": "broker-held-github-credential",
"runnerEnvTokenRequired": false,
"valuesPrinted": false
},
"prCapabilityContract": {
"targetBranch": base,
"headBranch": head,
"systemGhBinaryRequiredForWrites": false,
"preflightCreatesPr": false,
"preflightMergesPr": false,
"brokerProxy": {
"ok": true,
"operations": ["github.auth.status", "github.issue.read", "github.pr.read", "github.pr.create"],
"writesRemote": false
},
"pushDryRun": {
"runnerLocal": true,
"coveredByBroker": false
}
},
"redaction": {
"valuesPrinted": false
}
})
};
let audit = audit_from_response(config, &result, start.elapsed().as_millis(), status);
write_audit(config, &audit);
let _ = request.respond(json_response(result, status));
}
fn audit_from_response(
config: &Config,
response: &Value,
duration_ms: u128,
status: u16,
) -> AuditEvent {
let request_id = response
.get("requestId")
.and_then(Value::as_str)
.unwrap_or("unknown")
.to_string();
let operation = response
.get("operation")
.and_then(Value::as_str)
.unwrap_or("github.pr.preflight.dry-run")
.to_string();
let repo = response
.get("repo")
.and_then(Value::as_str)
.unwrap_or(DEFAULT_ALLOWED_REPO)
.to_string();
AuditEvent {
request_id,
observed_at: now_iso(),
caller: response
.get("caller")
.cloned()
.unwrap_or_else(|| json!({ "plane": "unknown", "taskId": null, "queueId": null })),
operation,
repo,
resource: response
.get("resource")
.cloned()
.unwrap_or_else(|| json!(null)),
dry_run: response
.get("dryRun")
.and_then(Value::as_bool)
.unwrap_or(true),
credential_ref: config.credential_ref.clone(),
credential_kind: config.credential_kind.clone(),
credential_value_printed: false,
upstream: response
.get("upstream")
.cloned()
.unwrap_or_else(|| json!(null)),
status,
ok: response.get("ok").and_then(Value::as_bool).unwrap_or(false),
failure_kind: response
.get("failureKind")
.and_then(Value::as_str)
.map(str::to_string),
degraded_reason: response
.get("degradedReason")
.and_then(Value::as_str)
.map(str::to_string),
runner_disposition: response
.get("runnerDisposition")
.and_then(Value::as_str)
.unwrap_or("infra-blocked")
.to_string(),
retryable: response
.get("retryable")
.and_then(Value::as_bool)
.unwrap_or(false),
duration_ms,
redaction: json!({ "valuesPrinted": false }),
}
}
fn write_audit(config: &Config, event: &AuditEvent) {
let Ok(line) = serde_json::to_string(event) else {
return;
};
if let Some(path) = &config.log_file {
if let Some(parent) = Path::new(path).parent() {
let _ = create_dir_all(parent);
}
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
let _ = writeln!(file, "{line}");
return;
}
}
println!("{line}");
}
fn route(config: &Config, request: Request) {
let method = request.method().clone();
let path = request.url().split('?').next().unwrap_or("/").to_string();
match (method, path.as_str()) {
(Method::Get, "/health") => {
let _ = request.respond(json_response(health_payload(config), 200));
}
(Method::Post, "/v1/github/gh") => handle_github(config, request),
(Method::Post, "/v1/github/pr-preflight") => handle_pr_preflight(config, request),
_ => {
let _ = request.respond(text_response("not found", 404));
}
}
}
fn main() {
let config = config_from_env();
let address = format!("{}:{}", config.host, config.port);
let server = Server::http(&address)
.unwrap_or_else(|error| panic!("auth-broker listen failed on {address}: {error}"));
println!(
"{}",
json!({
"ok": true,
"service": "auth-broker",
"event": "started",
"address": address,
"credentialRef": config.credential_ref,
"credentialValuePrinted": false
})
);
for request in server.incoming_requests() {
route(&config, request);
}
}