1use std::collections::HashMap;
2use std::future::Future;
3use std::path::PathBuf;
4use std::pin::Pin;
5use std::sync::{Arc, LazyLock};
6
7use anyhow::Result;
8use credentials_provider::CredentialsProvider;
9use futures::FutureExt as _;
10use gpui::{App, AsyncApp, Global};
11use release_channel::ReleaseChannel;
12
13/// An environment variable whose presence indicates that the system keychain
14/// should be used in development.
15///
16/// By default, running Zed in development uses the development credentials
17/// provider. Setting this environment variable allows you to interact with the
18/// system keychain (for instance, if you need to test something).
19///
20/// Only works in development. Setting this environment variable in other
21/// release channels is a no-op.
22static ZED_DEVELOPMENT_USE_KEYCHAIN: LazyLock<bool> = LazyLock::new(|| {
23 std::env::var("ZED_DEVELOPMENT_USE_KEYCHAIN").is_ok_and(|value| !value.is_empty())
24});
25
26pub struct ZedCredentialsProvider(pub Arc<dyn CredentialsProvider>);
27
28impl Global for ZedCredentialsProvider {}
29
30/// Returns the global [`CredentialsProvider`].
31pub fn init_global(cx: &mut App) {
32 // The `CredentialsProvider` trait has `Send + Sync` bounds on it, so it
33 // seems like this is a false positive from Clippy.
34 #[allow(clippy::arc_with_non_send_sync)]
35 let provider = new(cx);
36 cx.set_global(ZedCredentialsProvider(provider));
37}
38
39pub fn global(cx: &App) -> Arc<dyn CredentialsProvider> {
40 cx.try_global::<ZedCredentialsProvider>()
41 .map(|provider| provider.0.clone())
42 .unwrap_or_else(|| new(cx))
43}
44
45fn new(cx: &App) -> Arc<dyn CredentialsProvider> {
46 let use_development_provider = match ReleaseChannel::try_global(cx) {
47 Some(ReleaseChannel::Dev) => {
48 // In development we default to using the development
49 // credentials provider to avoid getting spammed by relentless
50 // keychain access prompts.
51 //
52 // However, if the `ZED_DEVELOPMENT_USE_KEYCHAIN` environment
53 // variable is set, we will use the actual keychain.
54 !*ZED_DEVELOPMENT_USE_KEYCHAIN
55 }
56 Some(ReleaseChannel::Nightly | ReleaseChannel::Preview | ReleaseChannel::Stable) | None => {
57 false
58 }
59 };
60
61 if use_development_provider {
62 Arc::new(DevelopmentCredentialsProvider::new())
63 } else {
64 Arc::new(KeychainCredentialsProvider)
65 }
66}
67
68/// A credentials provider that stores credentials in the system keychain.
69struct KeychainCredentialsProvider;
70
71impl CredentialsProvider for KeychainCredentialsProvider {
72 fn read_credentials<'a>(
73 &'a self,
74 url: &'a str,
75 cx: &'a AsyncApp,
76 ) -> Pin<Box<dyn Future<Output = Result<Option<(String, Vec<u8>)>>> + 'a>> {
77 async move { cx.update(|cx| cx.read_credentials(url)).await }.boxed_local()
78 }
79
80 fn write_credentials<'a>(
81 &'a self,
82 url: &'a str,
83 username: &'a str,
84 password: &'a [u8],
85 cx: &'a AsyncApp,
86 ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
87 async move {
88 cx.update(move |cx| cx.write_credentials(url, username, password))
89 .await
90 }
91 .boxed_local()
92 }
93
94 fn delete_credentials<'a>(
95 &'a self,
96 url: &'a str,
97 cx: &'a AsyncApp,
98 ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
99 async move { cx.update(move |cx| cx.delete_credentials(url)).await }.boxed_local()
100 }
101}
102
103/// A credentials provider that stores credentials in a local file.
104///
105/// This MUST only be used in development, as this is not a secure way of storing
106/// credentials on user machines.
107///
108/// Its existence is purely to work around the annoyance of having to constantly
109/// re-allow access to the system keychain when developing Zed.
110struct DevelopmentCredentialsProvider {
111 path: PathBuf,
112}
113
114impl DevelopmentCredentialsProvider {
115 fn new() -> Self {
116 let path = paths::config_dir().join("development_credentials");
117
118 Self { path }
119 }
120
121 fn load_credentials(&self) -> Result<HashMap<String, (String, Vec<u8>)>> {
122 let json = std::fs::read(&self.path)?;
123 let credentials: HashMap<String, (String, Vec<u8>)> = serde_json::from_slice(&json)?;
124
125 Ok(credentials)
126 }
127
128 fn save_credentials(&self, credentials: &HashMap<String, (String, Vec<u8>)>) -> Result<()> {
129 let json = serde_json::to_string(credentials)?;
130 std::fs::write(&self.path, json)?;
131
132 Ok(())
133 }
134}
135
136impl CredentialsProvider for DevelopmentCredentialsProvider {
137 fn read_credentials<'a>(
138 &'a self,
139 url: &'a str,
140 _cx: &'a AsyncApp,
141 ) -> Pin<Box<dyn Future<Output = Result<Option<(String, Vec<u8>)>>> + 'a>> {
142 async move {
143 Ok(self
144 .load_credentials()
145 .unwrap_or_default()
146 .get(url)
147 .cloned())
148 }
149 .boxed_local()
150 }
151
152 fn write_credentials<'a>(
153 &'a self,
154 url: &'a str,
155 username: &'a str,
156 password: &'a [u8],
157 _cx: &'a AsyncApp,
158 ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
159 async move {
160 let mut credentials = self.load_credentials().unwrap_or_default();
161 credentials.insert(url.to_string(), (username.to_string(), password.to_vec()));
162
163 self.save_credentials(&credentials)
164 }
165 .boxed_local()
166 }
167
168 fn delete_credentials<'a>(
169 &'a self,
170 url: &'a str,
171 _cx: &'a AsyncApp,
172 ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
173 async move {
174 let mut credentials = self.load_credentials()?;
175 credentials.remove(url);
176
177 self.save_credentials(&credentials)
178 }
179 .boxed_local()
180 }
181}