Add CredentialsProvider
to silence keychain prompts in development (#25266)
This PR adds a new `CredentialsProvider` trait that abstracts over interacting with the system keychain. We had previously introduced a version of this scoped just to Zed auth in https://github.com/zed-industries/zed/pull/11505. However, after landing https://github.com/zed-industries/zed/pull/25123, we now have a similar issue with the credentials for language model providers that are also stored in the keychain (and thus also produce a spam of popups when running a development build of Zed). This PR takes the existing approach and makes it more generic, such that we can use it everywhere that we need to read/store credentials in the keychain. There are still two credential provider implementations: - `KeychainCredentialsProvider` will interact with the system keychain (using the existing GPUI APIs) - `DevelopmentCredentialsProvider` will use a local file on the file system We only use the `DevelopmentCredentialsProvider` when: 1. We are running a development build of Zed 2. The `ZED_DEVELOPMENT_AUTH` environment variable is set - I am considering removing the need for this and making it the default, but that will be explored in a follow-up PR. Release Notes: - N/A
This commit is contained in:
parent
31aad858f8
commit
21bb7242ea
15 changed files with 401 additions and 226 deletions
21
crates/credentials_provider/Cargo.toml
Normal file
21
crates/credentials_provider/Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "credentials_provider"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/credentials_provider.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
paths.workspace = true
|
||||
release_channel.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
1
crates/credentials_provider/LICENSE-GPL
Symbolic link
1
crates/credentials_provider/LICENSE-GPL
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../LICENSE-GPL
|
187
crates/credentials_provider/src/credentials_provider.rs
Normal file
187
crates/credentials_provider/src/credentials_provider.rs
Normal file
|
@ -0,0 +1,187 @@
|
|||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::path::PathBuf;
|
||||
use std::pin::Pin;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use anyhow::Result;
|
||||
use futures::FutureExt as _;
|
||||
use gpui::{App, AsyncApp};
|
||||
use release_channel::ReleaseChannel;
|
||||
|
||||
/// An environment variable whose presence indicates that the development auth
|
||||
/// provider should be used.
|
||||
///
|
||||
/// Only works in development. Setting this environment variable in other release
|
||||
/// channels is a no-op.
|
||||
pub static ZED_DEVELOPMENT_AUTH: LazyLock<bool> = LazyLock::new(|| {
|
||||
std::env::var("ZED_DEVELOPMENT_AUTH").map_or(false, |value| !value.is_empty())
|
||||
});
|
||||
|
||||
/// A provider for credentials.
|
||||
///
|
||||
/// Used to abstract over reading and writing credentials to some form of
|
||||
/// persistence (like the system keychain).
|
||||
pub trait CredentialsProvider: Send + Sync {
|
||||
/// Reads the credentials from the provider.
|
||||
fn read_credentials<'a>(
|
||||
&'a self,
|
||||
url: &'a str,
|
||||
cx: &'a AsyncApp,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Option<(String, Vec<u8>)>>> + 'a>>;
|
||||
|
||||
/// Writes the credentials to the provider.
|
||||
fn write_credentials<'a>(
|
||||
&'a self,
|
||||
url: &'a str,
|
||||
username: &'a str,
|
||||
password: &'a [u8],
|
||||
cx: &'a AsyncApp,
|
||||
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>>;
|
||||
|
||||
/// Deletes the credentials from the provider.
|
||||
fn delete_credentials<'a>(
|
||||
&'a self,
|
||||
url: &'a str,
|
||||
cx: &'a AsyncApp,
|
||||
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>>;
|
||||
}
|
||||
|
||||
impl dyn CredentialsProvider {
|
||||
/// Returns the global [`CredentialsProvider`].
|
||||
pub fn global(cx: &App) -> Arc<Self> {
|
||||
// The `CredentialsProvider` trait has `Send + Sync` bounds on it, so it
|
||||
// seems like this is a false positive from Clippy.
|
||||
#[allow(clippy::arc_with_non_send_sync)]
|
||||
Self::new(cx)
|
||||
}
|
||||
|
||||
fn new(cx: &App) -> Arc<Self> {
|
||||
let use_development_backend = match ReleaseChannel::try_global(cx) {
|
||||
Some(ReleaseChannel::Dev) => *ZED_DEVELOPMENT_AUTH,
|
||||
Some(ReleaseChannel::Nightly | ReleaseChannel::Preview | ReleaseChannel::Stable)
|
||||
| None => false,
|
||||
};
|
||||
|
||||
if use_development_backend {
|
||||
Arc::new(DevelopmentCredentialsProvider::new())
|
||||
} else {
|
||||
Arc::new(KeychainCredentialsProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A credentials provider that stores credentials in the system keychain.
|
||||
struct KeychainCredentialsProvider;
|
||||
|
||||
impl CredentialsProvider for KeychainCredentialsProvider {
|
||||
fn read_credentials<'a>(
|
||||
&'a self,
|
||||
url: &'a str,
|
||||
cx: &'a AsyncApp,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Option<(String, Vec<u8>)>>> + 'a>> {
|
||||
async move { cx.update(|cx| cx.read_credentials(url))?.await }.boxed_local()
|
||||
}
|
||||
|
||||
fn write_credentials<'a>(
|
||||
&'a self,
|
||||
url: &'a str,
|
||||
username: &'a str,
|
||||
password: &'a [u8],
|
||||
cx: &'a AsyncApp,
|
||||
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
|
||||
async move {
|
||||
cx.update(move |cx| cx.write_credentials(url, username, password))?
|
||||
.await
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
|
||||
fn delete_credentials<'a>(
|
||||
&'a self,
|
||||
url: &'a str,
|
||||
cx: &'a AsyncApp,
|
||||
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
|
||||
async move { cx.update(move |cx| cx.delete_credentials(url))?.await }.boxed_local()
|
||||
}
|
||||
}
|
||||
|
||||
/// A credentials provider that stores credentials in a local file.
|
||||
///
|
||||
/// This MUST only be used in development, as this is not a secure way of storing
|
||||
/// credentials on user machines.
|
||||
///
|
||||
/// Its existence is purely to work around the annoyance of having to constantly
|
||||
/// re-allow access to the system keychain when developing Zed.
|
||||
struct DevelopmentCredentialsProvider {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl DevelopmentCredentialsProvider {
|
||||
fn new() -> Self {
|
||||
let path = paths::config_dir().join("development_credentials");
|
||||
|
||||
Self { path }
|
||||
}
|
||||
|
||||
fn load_credentials(&self) -> Result<HashMap<String, (String, Vec<u8>)>> {
|
||||
let json = std::fs::read(&self.path)?;
|
||||
let credentials: HashMap<String, (String, Vec<u8>)> = serde_json::from_slice(&json)?;
|
||||
|
||||
Ok(credentials)
|
||||
}
|
||||
|
||||
fn save_credentials(&self, credentials: &HashMap<String, (String, Vec<u8>)>) -> Result<()> {
|
||||
let json = serde_json::to_string(credentials)?;
|
||||
std::fs::write(&self.path, json)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl CredentialsProvider for DevelopmentCredentialsProvider {
|
||||
fn read_credentials<'a>(
|
||||
&'a self,
|
||||
url: &'a str,
|
||||
_cx: &'a AsyncApp,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Option<(String, Vec<u8>)>>> + 'a>> {
|
||||
async move {
|
||||
Ok(self
|
||||
.load_credentials()
|
||||
.unwrap_or_default()
|
||||
.get(url)
|
||||
.cloned())
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
|
||||
fn write_credentials<'a>(
|
||||
&'a self,
|
||||
url: &'a str,
|
||||
username: &'a str,
|
||||
password: &'a [u8],
|
||||
_cx: &'a AsyncApp,
|
||||
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
|
||||
async move {
|
||||
let mut credentials = self.load_credentials().unwrap_or_default();
|
||||
credentials.insert(url.to_string(), (username.to_string(), password.to_vec()));
|
||||
|
||||
self.save_credentials(&credentials)
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
|
||||
fn delete_credentials<'a>(
|
||||
&'a self,
|
||||
url: &'a str,
|
||||
_cx: &'a AsyncApp,
|
||||
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
|
||||
async move {
|
||||
let mut credentials = self.load_credentials()?;
|
||||
credentials.remove(url);
|
||||
|
||||
self.save_credentials(&credentials)
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue