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