115 lines
3.7 KiB
Rust
115 lines
3.7 KiB
Rust
use std::env;
|
|||
|
|
use std::io::{Read, Write};
|
||
|
|
use std::net::TcpStream;
|
||
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||
|
|
|
||
|
|
const SIGNING_CERT_NAME: &str = "homelab-oidc";
|
||
|
|
const PROVIDERS: &[&str] = &["grafana", "minio"];
|
||
|
|
|
||
|
|
fn main() {
|
||
|
|
let base_url = env::var("AUTHENTIK_BASE_URL")
|
||
|
|
.unwrap_or_else(|_| "http://localhost:7000".into());
|
||
|
|
let token = env::var("AUTHENTIK_BOOTSTRAP_TOKEN")
|
||
|
|
.expect("AUTHENTIK_BOOTSTRAP_TOKEN must be set");
|
||
|
|
|
||
|
|
let host = parse_host(&base_url);
|
||
|
|
|
||
|
|
let ts = SystemTime::now()
|
||
|
|
.duration_since(UNIX_EPOCH)
|
||
|
|
.unwrap()
|
||
|
|
.as_secs();
|
||
|
|
let new_name = format!("{}-{}", SIGNING_CERT_NAME, ts);
|
||
|
|
|
||
|
|
let body = format!(
|
||
|
|
r#"{{"common_name":"{}","validity_days":365,"key_size":4096}}"#,
|
||
|
|
new_name
|
||
|
|
);
|
||
|
|
let resp = http(&host, &token, "POST", "/api/v3/crypto/certificatekeypairs/generate/", Some(&body));
|
||
|
|
let new_pk = extract_str(&resp, "pk").expect("no pk in generate response");
|
||
|
|
println!("created '{}' pk={}", new_name, new_pk);
|
||
|
|
|
||
|
|
for provider in PROVIDERS {
|
||
|
|
let list = http(
|
||
|
|
&host, &token, "GET",
|
||
|
|
&format!("/api/v3/providers/oauth2/?name={}", provider),
|
||
|
|
None,
|
||
|
|
);
|
||
|
|
let provider_pk = match extract_str(&list, "pk") {
|
||
|
|
Some(pk) => pk,
|
||
|
|
None => {
|
||
|
|
eprintln!("WARN: provider '{}' not found — skipping", provider);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
let patch = format!(r#"{{"signing_key":"{}"}}"#, new_pk);
|
||
|
|
http(&host, &token, "PATCH",
|
||
|
|
&format!("/api/v3/providers/oauth2/{}/", provider_pk),
|
||
|
|
Some(&patch),
|
||
|
|
);
|
||
|
|
println!("rotated '{}' → signing_key={}", provider, new_pk);
|
||
|
|
}
|
||
|
|
|
||
|
|
println!("rotation complete");
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_host(base_url: &str) -> String {
|
||
|
|
let stripped = base_url
|
||
|
|
.trim_start_matches("http://")
|
||
|
|
.trim_start_matches("https://");
|
||
|
|
let host = stripped.split('/').next().unwrap_or(stripped);
|
||
|
|
if host.contains(':') {
|
||
|
|
host.to_string()
|
||
|
|
} else {
|
||
|
|
format!("{}:80", host)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn http(host: &str, token: &str, method: &str, path: &str, body: Option<&str>) -> String {
|
||
|
|
let mut stream = TcpStream::connect(host)
|
||
|
|
.unwrap_or_else(|e| panic!("connect {}: {}", host, e));
|
||
|
|
|
||
|
|
let body_str = body.unwrap_or("");
|
||
|
|
let hostname = host.split(':').next().unwrap_or(host);
|
||
|
|
let req = format!(
|
||
|
|
"{method} {path} HTTP/1.1\r\n\
|
||
|
|
Host: {hostname}\r\n\
|
||
|
|
Authorization: Bearer {token}\r\n\
|
||
|
|
Content-Type: application/json\r\n\
|
||
|
|
Content-Length: {len}\r\n\
|
||
|
|
Connection: close\r\n\
|
||
|
|
\r\n\
|
||
|
|
{body_str}",
|
||
|
|
len = body_str.len(),
|
||
|
|
);
|
||
|
|
stream.write_all(req.as_bytes()).unwrap();
|
||
|
|
|
||
|
|
let mut raw = String::new();
|
||
|
|
stream.read_to_string(&mut raw).unwrap();
|
||
|
|
|
||
|
|
let (head, resp_body) = raw.split_once("\r\n\r\n").unwrap_or((&raw, ""));
|
||
|
|
|
||
|
|
let status: u16 = head.lines().next()
|
||
|
|
.and_then(|l| l.split_whitespace().nth(1))
|
||
|
|
.and_then(|s| s.parse().ok())
|
||
|
|
.unwrap_or(0);
|
||
|
|
if status >= 400 {
|
||
|
|
panic!("{} {} → HTTP {} — {}", method, path, status, resp_body.trim());
|
||
|
|
}
|
||
|
|
|
||
|
|
resp_body.to_string()
|
||
|
|
}
|
||
|
|
|
||
|
|
// Extracts the value of the first `"key": <value>` match in raw JSON.
|
||
|
|
// Handles both quoted strings ("pk": "uuid") and bare numbers ("pk": 5).
|
||
|
|
fn extract_str(json: &str, key: &str) -> Option<String> {
|
||
|
|
let needle = format!("\"{}\":", key);
|
||
|
|
let after_colon = json.find(&needle)? + needle.len();
|
||
|
|
let rest = json[after_colon..].trim_start();
|
||
|
|
if let Some(inner) = rest.strip_prefix('"') {
|
||
|
|
Some(inner[..inner.find('"')?].to_string())
|
||
|
|
} else {
|
||
|
|
let end = rest.find(|c: char| c == ',' || c == '}' || c.is_ascii_whitespace())?;
|
||
|
|
Some(rest[..end].to_string())
|
||
|
|
}
|
||
|
|
}
|