feat: add auth broker p0 skeleton
This commit is contained in:
+414
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user