skipper_core/
credentials.rs1use std::path::PathBuf;
2
3use base64::Engine;
4use serde::{Deserialize, Serialize};
5
6use crate::error::Result;
7
8#[derive(Clone)]
10pub enum Credentials {
11 File(PathBuf),
13 Base64(String),
15 ServiceAccount(Box<ServiceAccountCredentials>),
17}
18
19impl std::fmt::Debug for Credentials {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22 match self {
23 Credentials::File(p) => f.debug_tuple("File").field(p).finish(),
24 Credentials::Base64(_) => f.write_str("Base64(<redacted>)"),
25 Credentials::ServiceAccount(sa) => f
26 .debug_struct("ServiceAccount")
27 .field("client_email", &sa.client_email)
28 .finish_non_exhaustive(),
29 }
30 }
31}
32
33#[derive(Clone, Default, Serialize, Deserialize)]
35pub struct ServiceAccountCredentials {
36 #[serde(rename = "type", default)]
37 pub kind: String,
38 #[serde(default)]
39 pub project_id: String,
40 #[serde(default)]
41 pub private_key_id: String,
42 pub private_key: String,
43 pub client_email: String,
44 #[serde(default)]
45 pub client_id: String,
46 #[serde(default)]
47 pub auth_uri: String,
48 #[serde(default)]
49 pub token_uri: String,
50 #[serde(default)]
51 pub auth_provider_x509_cert_url: String,
52 #[serde(default)]
53 pub client_x509_cert_url: String,
54}
55
56impl Credentials {
57 pub fn resolve(&self) -> Result<Vec<u8>> {
59 Ok(match self {
60 Credentials::File(path) => std::fs::read(path).map_err(|e| {
61 crate::Error(format!(
62 "cannot read credentials file {}: {e}",
63 path.display()
64 ))
65 })?,
66 Credentials::Base64(enc) => {
67 base64::engine::general_purpose::STANDARD.decode(enc.trim())?
68 }
69 Credentials::ServiceAccount(sa) => serde_json::to_vec(sa)?,
70 })
71 }
72
73 pub fn service_account(&self) -> Result<ServiceAccountCredentials> {
75 Ok(serde_json::from_slice(&self.resolve()?)?)
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 const JSON: &str = r#"{"type":"service_account","private_key":"k","client_email":"a@b.c","token_uri":"https://t"}"#;
84
85 #[test]
86 fn base64_and_file_and_inline_resolve_to_same_account() {
87 let b64 = base64::engine::general_purpose::STANDARD.encode(JSON);
88 let dir = tempfile::tempdir().unwrap();
89 let path = dir.path().join("sa.json");
90 std::fs::write(&path, JSON).unwrap();
91
92 for c in [Credentials::Base64(b64), Credentials::File(path)] {
93 let sa = c.service_account().unwrap();
94 assert_eq!(sa.client_email, "a@b.c");
95 assert_eq!(sa.token_uri, "https://t");
96 }
97 let inline = Credentials::ServiceAccount(Box::new(ServiceAccountCredentials {
98 client_email: "x@y.z".into(),
99 private_key: "k".into(),
100 ..Default::default()
101 }));
102 assert_eq!(inline.service_account().unwrap().client_email, "x@y.z");
103 }
104
105 #[test]
106 fn debug_redacts_secrets() {
107 let s = format!("{:?}", Credentials::Base64("c2VjcmV0".into()));
108 assert!(!s.contains("c2VjcmV0"));
109 }
110}