zed_credentials_provider.rs

  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}