firebase_admin/core/
credentials.rs1use crate::core::error::CoreError;
4use serde::Deserialize;
5
6#[derive(Clone, Deserialize)]
12pub struct ServiceAccountKey {
13 pub client_email: String,
15 pub private_key: String,
17 pub project_id: String,
19 pub private_key_id: String,
21}
22
23impl std::fmt::Debug for ServiceAccountKey {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 f.debug_struct("ServiceAccountKey")
26 .field("client_email", &self.client_email)
27 .field("private_key", &"[redacted]")
28 .field("project_id", &self.project_id)
29 .field("private_key_id", &self.private_key_id)
30 .finish()
31 }
32}
33
34impl ServiceAccountKey {
35 pub fn from_json(json: &str) -> Result<Self, CoreError> {
37 serde_json::from_str(json).map_err(CoreError::Deserialize)
38 }
39
40 pub fn from_file(path: impl AsRef<std::path::Path>) -> Result<Self, CoreError> {
42 let contents = std::fs::read_to_string(path)
43 .map_err(|e| CoreError::Credentials(format!("failed to read key file: {e}")))?;
44 Self::from_json(&contents)
45 }
46}
47
48#[derive(Debug, Clone)]
50pub enum Credentials {
51 ServiceAccount(Box<ServiceAccountKey>),
53 #[cfg(any(feature = "live-user-management", feature = "live-messaging"))]
55 ApplicationDefault,
56 Emulator,
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 const VALID_KEY_JSON: &str = r#"{
65 "client_email": "test@test-project.iam.gserviceaccount.com",
66 "private_key": "-----BEGIN PRIVATE KEY-----\nMIIB\n-----END PRIVATE KEY-----\n",
67 "project_id": "test-project",
68 "private_key_id": "abc123"
69 }"#;
70
71 #[test]
72 fn parses_a_valid_key_from_json() {
73 let key = ServiceAccountKey::from_json(VALID_KEY_JSON).unwrap();
74 assert_eq!(
75 key.client_email,
76 "test@test-project.iam.gserviceaccount.com"
77 );
78 assert_eq!(key.project_id, "test-project");
79 assert_eq!(key.private_key_id, "abc123");
80 }
81
82 #[test]
83 fn rejects_malformed_json() {
84 let err = ServiceAccountKey::from_json("not json").unwrap_err();
85 assert!(matches!(err, CoreError::Deserialize(_)));
86 }
87
88 #[test]
89 fn rejects_json_missing_required_fields() {
90 let err = ServiceAccountKey::from_json(r#"{"client_email": "only-this@example.com"}"#)
91 .unwrap_err();
92 assert!(matches!(err, CoreError::Deserialize(_)));
93 }
94
95 #[test]
96 fn from_file_surfaces_a_credentials_error_for_a_missing_path() {
97 let err = ServiceAccountKey::from_file("/does/not/exist.json").unwrap_err();
98 assert!(matches!(err, CoreError::Credentials(_)));
99 }
100
101 #[test]
102 fn debug_output_redacts_the_private_key() {
103 let key = ServiceAccountKey::from_json(VALID_KEY_JSON).unwrap();
104 let debug_output = format!("{key:?}");
105 assert!(!debug_output.contains("BEGIN PRIVATE KEY"));
106 assert!(debug_output.contains("[redacted]"));
107 assert!(debug_output.contains("test@test-project.iam.gserviceaccount.com"));
108 }
109}