pharos_native/runtime/social_connections/
settings.rs1use super::*;
3use pharos_runtime_security::credentials::{CredentialBackend, CredentialError};
4
5pub(super) fn reference(scope: &ConnectionScope, provider: SocialProvider) -> String {
7 let value = format!(
8 "{}\0{}\0{}\0{}",
9 scope.application(),
10 scope.namespace(),
11 scope.owner(),
12 provider.key()
13 );
14 format!(
15 "social.registration.{}",
16 dynamic::hex_digest(&Sha256::digest(value.as_bytes()))
17 )
18}
19
20pub(super) fn load(
22 app: &AppRuntime,
23 scope: &ConnectionScope,
24 provider: SocialProvider,
25) -> Result<Option<Registration>, SocialError> {
26 if let Some(value) = load_shared(app, scope, provider)? {
27 return Ok(Some(value));
28 }
29 let vault = vault(app)?;
30 let bytes = match vault.get_secret(&reference(scope, provider)) {
31 Ok(bytes) => zeroize::Zeroizing::new(bytes),
32 Err(CredentialError::CredentialUnavailable) => return Ok(None),
33 Err(_) => return Err(SocialError::Configuration),
34 };
35 let value: Registration =
36 serde_json::from_slice(&bytes).map_err(|_| SocialError::Configuration)?;
37 if value.application != scope.application()
38 || value.namespace != scope.namespace()
39 || value.provider != provider.key()
40 || value.client_secret_env.is_some()
41 {
42 return Err(SocialError::Configuration);
43 }
44 Ok(Some(value))
45}
46
47fn shared_reference(scope: &ConnectionScope, provider: SocialProvider) -> String {
49 let value = format!(
50 "{}\0{}\0{}",
51 scope.application(),
52 scope.namespace(),
53 provider.key()
54 );
55 format!(
56 "social.application.{}",
57 dynamic::hex_digest(&Sha256::digest(value.as_bytes()))
58 )
59}
60
61fn load_shared(
63 app: &AppRuntime,
64 scope: &ConnectionScope,
65 provider: SocialProvider,
66) -> Result<Option<Registration>, SocialError> {
67 let bytes = match vault(app)?.get_secret(&shared_reference(scope, provider)) {
68 Ok(bytes) => zeroize::Zeroizing::new(bytes),
69 Err(CredentialError::CredentialUnavailable) => return Ok(None),
70 Err(_) => return Err(SocialError::Configuration),
71 };
72 let value: Registration =
73 serde_json::from_slice(&bytes).map_err(|_| SocialError::Configuration)?;
74 if value.application != scope.application()
75 || value.namespace != scope.namespace()
76 || value.provider != provider.key()
77 || value.client_secret_env.is_some()
78 {
79 return Err(SocialError::Configuration);
80 }
81 Ok(Some(value))
82}
83
84pub(super) fn can_setup(
86 app: &AppRuntime,
87 declaration: &Value,
88 owner: i64,
89) -> Result<bool, SocialError> {
90 let Some(capability) = declaration.get("setup_capability").and_then(Value::as_str) else {
91 return Ok(false);
92 };
93 if capability.is_empty()
94 || matches!(
95 capability,
96 "public" | "customer" | "authenticated_user" | "account_login" | "account_logout"
97 )
98 {
99 return Err(SocialError::Configuration);
100 }
101 actor_has_route_policy(app, owner, capability).map_err(|_| SocialError::Transport)
102}
103
104pub(super) fn callback_url(
106 app: &AppRuntime,
107 request: &http::HttpRequest,
108 provider: SocialProvider,
109 preview_path: Option<&str>,
110) -> Result<String, SocialError> {
111 let hosts = request
112 .headers
113 .lines()
114 .filter_map(|line| {
115 let (name, value) = line.split_once(':')?;
116 name.trim()
117 .eq_ignore_ascii_case("host")
118 .then_some(value.trim())
119 })
120 .collect::<Vec<_>>();
121 if hosts.len() != 1
122 || hosts[0].contains(['/', '\\', '?', '#', '@'])
123 || hosts[0].chars().any(char::is_whitespace)
124 {
125 return Err(SocialError::Configuration);
126 }
127 let scheme = dynamic::request_origin_scheme(
128 Some(request),
129 super::super::connection::NativeTransportType::Tcp,
130 None,
131 None,
132 );
133 let secure_referrer = request
134 .headers
135 .lines()
136 .filter_map(|line| line.split_once(':'))
137 .any(|(name, value)| {
138 name.trim().eq_ignore_ascii_case("referer")
139 && Url::parse(value.trim()).is_ok_and(|url| {
140 url.scheme() == "https" && url.authority().eq_ignore_ascii_case(hosts[0])
141 })
142 });
143 let scheme = if secure_referrer { "https" } else { scheme };
144 let path = preview_path
145 .map(|path| path.replace("{provider}", provider.key()))
146 .unwrap_or_else(|| {
147 app_scope_path(
148 app,
149 &format!("/__pharos/social/{}/callback", provider.key()),
150 )
151 });
152 let value = format!("{scheme}://{}{path}", hosts[0]);
153 let parsed = Url::parse(&value).map_err(|_| SocialError::Configuration)?;
154 if value.len() > 4096
155 || parsed.fragment().is_some()
156 || !parsed.username().is_empty()
157 || parsed.password().is_some()
158 || (parsed.scheme() != "https"
159 && !(parsed.scheme() == "http"
160 && matches!(parsed.host_str(), Some("localhost" | "127.0.0.1" | "[::1]"))))
161 {
162 return Err(SocialError::Configuration);
163 }
164 Ok(value)
165}
166
167pub(super) fn save(
169 app: &AppRuntime,
170 scope: &ConnectionScope,
171 provider: SocialProvider,
172 client_id: &str,
173 client_secret: &str,
174 callback: &str,
175) -> Result<(), SocialError> {
176 if client_id.is_empty()
177 || client_id.len() > 512
178 || !client_id.bytes().all(|byte| byte.is_ascii_graphic())
179 || client_secret.len() > 8192
180 || client_secret.chars().any(char::is_control)
181 {
182 return Err(SocialError::Configuration);
183 }
184 let secret = if client_secret.is_empty() {
185 None
186 } else {
187 Some(SecretValue::new(client_secret.to_owned()).map_err(|_| SocialError::Configuration)?)
188 };
189 let revision = pharos_runtime_social::repository::random_revision()?;
190 SocialClient::new(provider, client_id, callback, &revision, secret.as_ref())?;
191 let _pending = pending_registry().lock().map_err(|_| SocialError::Busy)?;
193 if registration(app, scope, provider)?.is_some() {
194 return Err(SocialError::Busy);
195 }
196 let vault = vault(app)?;
197 let secret_ref = secret.as_ref().map(|_| {
198 format!(
199 "social.client.{}",
200 dynamic::hex_digest(&Sha256::digest(
201 format!("{}\0{revision}", shared_reference(scope, provider)).as_bytes()
202 ))
203 )
204 });
205 if let Some(key) = &secret_ref {
206 vault
207 .put_secret(key, client_secret.as_bytes())
208 .map_err(|_| SocialError::Transport)?;
209 }
210 let registration = Registration {
211 application: scope.application().into(),
212 namespace: scope.namespace().into(),
213 provider: provider.key().into(),
214 client_id: client_id.into(),
215 callback: callback.into(),
216 revision,
217 client_secret_env: None,
218 client_secret_ref: secret_ref.clone(),
219 instagram_account_id: None,
220 };
221 let bytes = zeroize::Zeroizing::new(
222 serde_json::to_vec(®istration).map_err(|_| SocialError::Configuration)?,
223 );
224 if vault
225 .put_secret(&shared_reference(scope, provider), &bytes)
226 .is_err()
227 {
228 if let Some(key) = &secret_ref {
229 let _ = vault.unset_secret(key);
230 }
231 return Err(SocialError::Transport);
232 }
233 Ok(())
234}