use crate::cloud::cloud_browse::Environment;
use crate::cloud::source::ProviderKind;
use crate::config::{CloudConfig, CloudConnectionConfig, DatasetAccess, DatasetAuth};
use std::collections::HashMap;
use std::sync::{Arc, Mutex, OnceLock};
pub const DEFAULT_S3: &str = "s3-default";
pub const DEFAULT_GCS: &str = "gcs-default";
pub const DEFAULT_AZURE_LOGIN: &str = "az";
pub const DEFAULT_AZURE_ENV: &str = "azure-env";
pub fn is_within(url: &str, root: &str) -> bool {
let url = crate::cloud::source::canonical_cloud_place(url);
let root = crate::cloud::source::canonical_cloud_place(root);
url == root
|| url
.strip_prefix(&root)
.is_some_and(|rest| rest.starts_with('/'))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Tier {
Config,
Environment,
Tools,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct S3Settings {
pub endpoint: Option<String>,
pub access_key_id: Option<String>,
pub secret_access_key: Option<String>,
pub session_token: Option<String>,
pub region: Option<String>,
pub virtual_hosted: Option<bool>,
pub from_env: bool,
pub skip_signature: bool,
}
impl S3Settings {
pub fn from_config(cloud: &CloudConfig) -> Self {
Self {
endpoint: cloud.s3_endpoint_url.clone(),
access_key_id: cloud.s3_access_key_id.clone(),
secret_access_key: cloud.s3_secret_access_key.clone(),
session_token: None,
region: cloud.s3_region.clone(),
virtual_hosted: None,
from_env: true,
skip_signature: false,
}
}
pub fn virtual_hosted_style(&self) -> bool {
self.virtual_hosted.unwrap_or(self.endpoint.is_none())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Source {
pub id: String,
pub label: String,
pub kind: ProviderKind,
pub tier: Tier,
pub origin: String,
pub s3: S3Settings,
pub azure: crate::cloud::azure::AzureSettings,
pub project: Option<String>,
pub profile: Option<String>,
pub buckets: Vec<String>,
pub problem: Option<String>,
pub gcloud: Option<String>,
pub secret_command: Option<String>,
pub google_credentials: Option<std::path::PathBuf>,
}
impl Source {
pub fn new(
kind: ProviderKind,
id: impl Into<String>,
tier: Tier,
origin: impl Into<String>,
) -> Self {
Source {
id: id.into(),
label: String::new(),
kind,
tier,
origin: origin.into(),
s3: S3Settings::default(),
azure: Default::default(),
project: None,
profile: None,
buckets: Vec::new(),
problem: None,
gcloud: None,
secret_command: None,
google_credentials: None,
}
}
fn default_for(
kind: ProviderKind,
config: &CloudConfig,
tier: Tier,
origin: impl Into<String>,
) -> Self {
let id = match kind {
ProviderKind::S3 => DEFAULT_S3,
ProviderKind::Gcs => DEFAULT_GCS,
ProviderKind::Azure => DEFAULT_AZURE_LOGIN,
};
let s3 = match kind {
ProviderKind::S3 => S3Settings::from_config(config),
ProviderKind::Gcs | ProviderKind::Azure => S3Settings::default(),
};
Source {
s3,
..Source::new(kind, id, tier, origin)
}
}
pub fn named_in_urls(&self) -> bool {
self.kind == ProviderKind::S3 && self.id != DEFAULT_S3 && self.s3.endpoint.is_some()
}
pub fn bucket_url(&self, bucket: &str) -> String {
if matches!(self.kind, ProviderKind::Azure | ProviderKind::Gcs) {
return format!("cloud://{}/{bucket}", self.id);
}
let scheme = self.kind.scheme();
if self.named_in_urls() {
format!("{scheme}://{}@{bucket}", self.id)
} else {
format!("{scheme}://{bucket}")
}
}
pub fn detail(&self) -> Option<String> {
match (&self.project, &self.profile) {
(Some(project), _) => Some(format!("project: {project}")),
(None, Some(profile)) => Some(format!("profile: {profile}")),
(None, None) => self.s3.endpoint.as_deref().and_then(endpoint_host),
}
}
pub fn fingerprint(&self) -> String {
[
if self.kind == ProviderKind::Gcs {
"gs-projects"
} else {
self.kind.scheme()
},
self.gcloud.as_deref().unwrap_or(""),
self.s3.endpoint.as_deref().unwrap_or(""),
self.s3.access_key_id.as_deref().unwrap_or(""),
self.project.as_deref().unwrap_or(""),
self.profile.as_deref().unwrap_or(""),
self.azure.account.as_deref().unwrap_or(""),
]
.join("|")
}
}
pub fn endpoint_host(endpoint: &str) -> Option<String> {
let rest = endpoint
.split_once("://")
.map(|(_, rest)| rest)
.unwrap_or(endpoint);
let host = rest.split(['/', '?', '#']).next()?.trim();
(!host.is_empty()).then(|| host.to_string())
}
pub fn discover(config: &CloudConfig, env: &Environment<'_>) -> Vec<Source> {
let profiles = crate::cloud::aws_profiles::load(env);
let active = crate::cloud::aws_profiles::active_profile(env);
let mut default_uses_profile = false;
let mut sources: Vec<Source> = crate::cloud::cloud_browse::detect(config, env)
.into_iter()
.map(|provider| {
let tier = match provider.note.as_str() {
"datui config" => Tier::Config,
"~/.aws" | "gcloud" => Tier::Tools,
_ => Tier::Environment,
};
let mut source = Source {
project: provider.project,
..Source::default_for(provider.kind, config, tier, provider.note.clone())
};
if provider.kind == ProviderKind::S3
&& matches!(provider.note.as_str(), "AWS_PROFILE" | "~/.aws")
{
default_uses_profile = true;
source.profile = Some(active.clone());
if let Some(profile) = profiles.iter().find(|p| p.name == active) {
fill_from_profile(&mut source.s3, profile, env.var);
}
}
source.label = match source.kind {
ProviderKind::S3 if source.s3.endpoint.is_none() => "Amazon S3",
ProviderKind::S3 => "S3-compatible",
ProviderKind::Gcs => "Google Cloud",
ProviderKind::Azure => "Azure",
}
.to_string();
source
})
.collect();
if let Some(google) = sources.iter_mut().find(|s| s.id == DEFAULT_GCS)
&& let Some(path) = (env.var)("GOOGLE_APPLICATION_CREDENTIALS")
{
google.google_credentials = Some(std::path::PathBuf::from(path));
}
if let Some(s3) = sources.iter_mut().find(|s| s.id == DEFAULT_S3)
&& s3.s3.access_key_id.is_some()
&& s3.s3.access_key_id == (env.var)("AWS_ACCESS_KEY_ID")
{
s3.s3.session_token = (env.var)("AWS_SESSION_TOKEN");
}
gcloud_sources(&mut sources, env);
for profile in profiles.iter().filter(|p| p.has_credentials()) {
if default_uses_profile && profile.name == active {
continue;
}
let mut s3 = S3Settings::default();
fill_from_profile(&mut s3, profile, env.var);
sources.push(Source {
label: profile.name.clone(),
s3,
profile: Some(profile.name.clone()),
..Source::new(
ProviderKind::S3,
profile_source_id(&profile.name),
Tier::Tools,
"aws profile",
)
});
}
for source in tool_sources(env) {
if !sources.iter().any(|s| s.id == source.id) {
sources.push(source);
}
}
sources.extend(azure_sources(config, env));
for configured in &config.connections {
let source = configured_source(configured, env);
match sources.iter_mut().find(|s| s.id == source.id) {
Some(existing) => *existing = source,
None => sources.push(source),
}
}
sources.sort_by(|a, b| {
a.tier
.cmp(&b.tier)
.then_with(|| a.label.to_lowercase().cmp(&b.label.to_lowercase()))
.then_with(|| a.id.cmp(&b.id))
});
let mut kept: Vec<Source> = Vec::new();
for source in sources {
let same_as = kept.iter_mut().find(|k| {
k.kind == ProviderKind::S3
&& source.kind == ProviderKind::S3
&& k.s3.access_key_id.is_some()
&& k.s3.access_key_id == source.s3.access_key_id
&& normalized_endpoint(&k.s3) == normalized_endpoint(&source.s3)
});
match same_as {
Some(existing) => {
if !existing.origin.contains(&source.origin) {
existing.origin = format!("{}, {}", existing.origin, source.origin);
}
}
None => kept.push(source),
}
}
kept
}
fn gcloud_sources(sources: &mut Vec<Source>, env: &Environment<'_>) {
let configurations = crate::cloud::gcloud::configurations(env);
let active_name = crate::cloud::gcloud::active_name(env);
let active_configuration = configurations
.iter()
.find(|c| c.name == active_name && c.account.is_some());
match sources.iter_mut().find(|s| s.id == DEFAULT_GCS) {
Some(default) => {
if let Some(kind) = crate::cloud::cloud_browse::unreadable_google_login(env) {
match active_configuration {
Some(configuration) => {
default.gcloud = Some(configuration.name.clone());
default.origin = "gcloud".to_string();
}
None => default.problem = Some(format!("unsupported login: {kind}")),
}
}
if default.project.is_none() {
default.project = active_configuration.and_then(|c| c.project.clone());
}
}
None => {
if let Some(configuration) = active_configuration {
sources.push(Source {
label: "Google Cloud".to_string(),
project: crate::cloud::cloud_browse::gcp_project(env)
.or_else(|| configuration.project.clone()),
gcloud: Some(configuration.name.clone()),
..Source::new(ProviderKind::Gcs, DEFAULT_GCS, Tier::Tools, "gcloud")
});
}
}
}
let mut accounts_seen: Vec<String> = active_configuration
.and_then(|c| c.account.clone())
.into_iter()
.collect();
for configuration in &configurations {
let Some(account) = &configuration.account else {
continue;
};
if accounts_seen.contains(account) {
continue;
}
accounts_seen.push(account.clone());
sources.push(Source {
label: configuration.name.clone(),
project: configuration.project.clone(),
gcloud: Some(configuration.name.clone()),
..Source::new(
ProviderKind::Gcs,
slug_id("gcloud", &configuration.name),
Tier::Tools,
"gcloud configuration",
)
});
}
}
fn tool_sources(env: &Environment<'_>) -> Vec<Source> {
let mut sources: Vec<Source> = Vec::new();
for path in crate::cloud::s3_tools::mc_config_paths(env) {
if let Some(text) = (env.read)(&path) {
for server in crate::cloud::s3_tools::parse_mc_config(&text) {
sources.push(tool_source(server, Tier::Tools));
}
}
}
for server in crate::cloud::s3_tools::mc_hosts(&(env.all_vars)()) {
let source = tool_source(server, Tier::Environment);
sources.retain(|s| s.id != source.id);
sources.push(source);
}
if let Some(server) = crate::cloud::s3_tools::s3cfg_path(env)
.and_then(|path| (env.read)(&path))
.and_then(|text| crate::cloud::s3_tools::parse_s3cfg(&text))
{
sources.push(tool_source(server, Tier::Tools));
}
sources
}
fn azure_sources(config: &CloudConfig, env: &Environment<'_>) -> Vec<Source> {
use crate::cloud::azure::{AzureAuth, AzureSettings};
let mut sources = Vec::new();
let from_environment = crate::cloud::azure::from_environment(env.var).or_else(|| {
crate::cloud::cloud_browse::instance_identity(config, env)
.azure
.then(|| {
let settings = AzureSettings {
account: (env.var)("AZURE_STORAGE_ACCOUNT_NAME"),
auth: AzureAuth::ManagedIdentity,
..Default::default()
};
(settings, "managed identity".to_string())
})
});
if let Some((settings, origin)) = from_environment {
sources.push(Source {
label: settings
.account
.clone()
.unwrap_or_else(|| "Azure".to_string()),
azure: settings,
..Source::new(
ProviderKind::Azure,
DEFAULT_AZURE_ENV,
Tier::Environment,
origin,
)
});
}
let az = crate::cloud::azure::az_login_evidence(env);
let powershell = crate::cloud::azure::powershell_login_evidence(env);
let not_signed_in = crate::cloud::azure::not_signed_in(env);
if az || powershell || not_signed_in.is_some() {
let auth = if az || !powershell {
AzureAuth::AzCli
} else {
AzureAuth::PowerShell
};
let origin = match not_signed_in {
Some(_) => "not signed in",
None => auth.describe(),
};
let login = Source::new(
ProviderKind::Azure,
DEFAULT_AZURE_LOGIN,
Tier::Tools,
origin,
);
sources.push(Source {
label: "Azure".to_string(),
problem: not_signed_in,
azure: AzureSettings {
auth,
..Default::default()
},
..login
});
}
sources
}
fn normalized_endpoint(s3: &S3Settings) -> String {
s3.endpoint
.as_deref()
.unwrap_or("")
.trim_end_matches('/')
.to_ascii_lowercase()
}
fn tool_source(server: crate::cloud::s3_tools::ToolServer, tier: Tier) -> Source {
let id = if server.origin == "s3cmd" {
"s3cfg".to_string()
} else {
slug_id("mc", &server.name)
};
let label = if server.origin == "s3cmd" {
server
.endpoint
.as_deref()
.and_then(endpoint_host)
.unwrap_or_else(|| "s3cmd".to_string())
} else {
server.name.clone()
};
Source {
label,
s3: S3Settings {
endpoint: server.endpoint,
access_key_id: Some(server.access_key_id),
secret_access_key: Some(server.secret_access_key),
session_token: server.session_token,
region: server.region,
virtual_hosted: server.virtual_hosted,
from_env: false,
skip_signature: false,
},
..Source::new(ProviderKind::S3, id, tier, server.origin)
}
}
pub fn profile_source_id(profile: &str) -> String {
slug_id("aws", profile)
}
fn slug_id(prefix: &str, name: &str) -> String {
let slug: String = name
.chars()
.map(|c| {
let c = c.to_ascii_lowercase();
if c.is_ascii_lowercase() || c.is_ascii_digit() {
c
} else {
'-'
}
})
.collect();
let mut id = format!("{prefix}-{}", slug.trim_matches('-'));
id.truncate(40);
id
}
fn fill_from_profile(
s3: &mut S3Settings,
profile: &crate::cloud::aws_profiles::Profile,
var: &dyn Fn(&str) -> Option<String>,
) {
if s3.endpoint.is_none() {
s3.endpoint = profile.s3_endpoint(var);
}
if s3.region.is_none() {
s3.region = profile.region.clone();
}
}
impl Source {
pub fn with_credentials(mut self, env: &Environment<'_>) -> Result<Source, String> {
if let Some(problem) = &self.problem {
return Err(problem.clone());
}
if let Some(command) = &self.secret_command
&& self.s3.secret_access_key.is_none()
{
self.s3.secret_access_key = Some(crate::cloud::cloud_command::secret(command, env)?);
}
let Some(name) = self.profile.clone() else {
return Ok(self);
};
if self.s3.access_key_id.is_some() {
return Ok(self);
}
let profiles = crate::cloud::aws_profiles::load(env);
let profile = profiles
.iter()
.find(|p| p.name == name)
.ok_or_else(|| format!("profile {name} is not in the AWS config"))?;
let credentials = crate::cloud::aws_profiles::credentials(profile, env)?;
self.s3.access_key_id = Some(credentials.access_key_id);
self.s3.secret_access_key = Some(credentials.secret_access_key);
self.s3.session_token = credentials.session_token;
self.s3.from_env = false;
Ok(self)
}
}
fn configured_source(configured: &CloudConnectionConfig, env: &Environment<'_>) -> Source {
if configured.kind.as_deref() == Some("azure") {
return configured_azure_source(configured, env);
}
if configured.kind.as_deref() == Some("gcs") {
let google_credentials = configured
.credentials_file
.as_deref()
.map(|file| expand_home(file, env));
let problem = google_credentials
.as_ref()
.filter(|path| !(env.exists)(path))
.map(|path| format!("credentials_file {} does not exist", path.display()));
let file_project = google_credentials
.as_ref()
.and_then(|path| (env.read)(path))
.and_then(|text| google_file_project(&text));
return Source {
label: configured
.label
.clone()
.unwrap_or_else(|| configured.name.clone()),
project: configured
.project
.clone()
.or(file_project)
.or_else(|| crate::cloud::cloud_browse::gcp_project(env)),
buckets: configured.buckets.clone(),
problem,
gcloud: configured.configuration.clone(),
google_credentials,
..Source::new(
ProviderKind::Gcs,
configured.name.clone(),
Tier::Config,
"datui config".to_string(),
)
};
}
let var = env.var;
let mut problem = None;
let mut from_named = |name: &Option<String>| -> Option<String> {
let name = name.as_deref()?;
match var(name).map(|v| v.trim().to_string()) {
Some(value) if !value.is_empty() => Some(value),
_ => {
problem.get_or_insert_with(|| format!("{name} is not set"));
None
}
}
};
let mut s3 = S3Settings {
endpoint: configured.endpoint_url.clone(),
access_key_id: from_named(&configured.access_key_id_env),
secret_access_key: from_named(&configured.secret_access_key_env),
session_token: from_named(&configured.session_token_env),
region: configured.region.clone(),
virtual_hosted: configured.addressing.as_deref().map(|a| a == "virtual"),
from_env: false,
skip_signature: false,
};
if let Some(name) = &configured.profile {
match crate::cloud::aws_profiles::load(env)
.iter()
.find(|p| &p.name == name)
{
Some(profile) => fill_from_profile(&mut s3, profile, var),
None => {
problem.get_or_insert_with(|| format!("profile {name} is not in the AWS config"));
}
}
}
Source {
label: configured
.label
.clone()
.unwrap_or_else(|| configured.name.clone()),
s3,
profile: configured.profile.clone(),
buckets: configured.buckets.clone(),
problem,
secret_command: configured.secret_command.clone(),
..Source::new(
ProviderKind::S3,
configured.name.clone(),
Tier::Config,
"datui config".to_string(),
)
}
}
fn expand_home(file: &str, env: &Environment<'_>) -> std::path::PathBuf {
match (
file.strip_prefix("~/").or_else(|| file.strip_prefix("~\\")),
&env.home,
) {
(Some(rest), Some(home)) => home.join(rest),
_ => std::path::PathBuf::from(file),
}
}
fn google_file_project(text: &str) -> Option<String> {
let value: serde_json::Value = serde_json::from_str(text).ok()?;
["project_id", "quota_project_id"]
.iter()
.find_map(|key| value.get(*key)?.as_str().map(str::to_string))
.filter(|p| !p.is_empty())
}
fn configured_azure_source(configured: &CloudConnectionConfig, env: &Environment<'_>) -> Source {
use crate::cloud::azure::{AzureAuth, AzureSettings};
let named = |name: &Option<String>| -> Option<Result<String, String>> {
let name = name.as_deref()?;
Some(
(env.var)(name)
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty())
.ok_or_else(|| format!("{name} is not set")),
)
};
let mut problem = None;
let mut settings = AzureSettings {
account: configured.account.clone(),
auth: AzureAuth::AzCli,
..Default::default()
};
if let Some(key) = named(&configured.account_key_env) {
match key {
Ok(key) => settings.auth = AzureAuth::Key(key),
Err(e) => problem = Some(e),
}
} else if let Some(sas) = named(&configured.sas_env) {
match sas {
Ok(sas) => settings.auth = AzureAuth::Sas(sas.trim_start_matches('?').to_string()),
Err(e) => problem = Some(e),
}
} else if let Some(text) = named(&configured.connection_string_env) {
match text.map(|t| crate::cloud::azure::parse_connection_string(&t)) {
Ok(Some(parsed)) => {
settings = AzureSettings {
account: configured.account.clone().or(parsed.account.clone()),
..parsed
}
}
Ok(None) => {
problem = Some("the connection string names no account and key or SAS".to_string())
}
Err(e) => problem = Some(e),
}
} else if let Some(command) = &configured.secret_command {
settings.auth = AzureAuth::KeyCommand(command.clone());
} else if !crate::cloud::azure::az_login_evidence(env)
&& crate::cloud::azure::powershell_login_evidence(env)
{
settings.auth = AzureAuth::PowerShell;
}
Source {
label: configured
.label
.clone()
.unwrap_or_else(|| configured.name.clone()),
azure: settings,
problem,
..Source::new(
ProviderKind::Azure,
configured.name.clone(),
Tier::Config,
"datui config".to_string(),
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Resolved {
pub url: String,
pub kind: ProviderKind,
pub source_id: String,
pub s3: S3Settings,
pub azure: crate::cloud::azure::AzureSettings,
pub signing: Signing,
pub place: String,
pub gcloud: Option<(String, String)>,
pub google_credentials: Option<std::path::PathBuf>,
pub login_error: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Signing {
Signed,
Unsigned,
Try,
}
impl Resolved {
pub fn unsigned(mut self) -> Self {
self.s3 = S3Settings {
endpoint: self.s3.endpoint.take(),
region: self.s3.region.take(),
virtual_hosted: self.s3.virtual_hosted,
skip_signature: true,
..Default::default()
};
self.azure.auth = crate::cloud::azure::AzureAuth::None;
self.gcloud = None;
self.google_credentials = None;
self.signing = Signing::Unsigned;
self
}
}
pub fn access_key(url: &str) -> Option<String> {
if let Some((account, container, _)) = crate::cloud::source::azure_parts(url) {
return Some(format!("abfss://{container}@{account}"));
}
let (id, _) = crate::cloud::source::split_source_id(url);
let (kind, bucket, _) = crate::cloud::cloud_browse::split_bucket_url(url)?;
Some(match id {
Some(id) => format!("{}://{id}@{bucket}", kind.scheme()),
None => format!("{}://{bucket}", kind.scheme()),
})
}
fn access() -> &'static Mutex<HashMap<String, bool>> {
static MAP: OnceLock<Mutex<HashMap<String, bool>>> = OnceLock::new();
MAP.get_or_init(Default::default)
}
pub fn remember_access(place: &str, unsigned: bool) {
if let Ok(mut map) = access().lock() {
map.insert(place.to_string(), unsigned);
}
}
pub fn known_access(url: &str) -> Option<bool> {
let key = access_key(url)?;
access().lock().ok()?.get(&key).copied()
}
fn bucket_sources() -> &'static Mutex<HashMap<String, String>> {
static MAP: OnceLock<Mutex<HashMap<String, String>>> = OnceLock::new();
MAP.get_or_init(Default::default)
}
pub fn remember_bucket(source: &Source, bucket: &str) {
if source.named_in_urls() || source.id == DEFAULT_S3 || source.id == DEFAULT_GCS {
return;
}
let key = format!("{}://{bucket}", source.kind.scheme());
if let Ok(mut map) = bucket_sources().lock() {
map.insert(key, source.id.clone());
}
}
pub fn remember_listed(source: &Source, buckets: &[String]) {
if source.kind != ProviderKind::S3 {
return;
}
for bucket in buckets {
remember_bucket(source, bucket);
}
}
pub fn on_home(sources: Vec<Source>, config: &CloudConfig) -> Vec<Source> {
let Some(discover) = &config.discover else {
return sources;
};
sources
.into_iter()
.filter(|source| {
config.connections.iter().any(|c| c.name == source.id)
|| discover.allows(source.kind.name())
})
.collect()
}
fn remembered(kind: ProviderKind, bucket: &str) -> Option<String> {
let key = format!("{}://{bucket}", kind.scheme());
bucket_sources().lock().ok()?.get(&key).cloned()
}
pub fn resolve(url: &str, config: &CloudConfig) -> Result<Resolved, String> {
let sources = session_sources(config);
let mut resolved = resolve_among(url, config, &sources, &Environment::current())?;
if resolved.kind == ProviderKind::S3
&& resolved.s3.endpoint.is_none()
&& let Some((_, bucket, _)) = crate::cloud::cloud_browse::split_bucket_url(&resolved.url)
&& let Some(region) = crate::cloud::cloud_browse::s3_bucket_region(&bucket)
{
resolved.s3.region = Some(region);
}
Ok(resolved)
}
pub fn resolve_for_open(url: &str, config: &CloudConfig) -> Result<Resolved, String> {
let resolved = settle_signing(resolve(url, config)?);
if let Some(error) = &resolved.login_error
&& crate::cloud::cloud_browse::probe_unsigned(&resolved) == Some(false)
{
return Err(error.clone());
}
Ok(with_azure_key_if_refused(resolved, config))
}
fn with_azure_key_if_refused(resolved: Resolved, config: &CloudConfig) -> Resolved {
let enabled = config.use_azure_account_keys;
if resolved.kind != ProviderKind::Azure
|| resolved.signing == Signing::Unsigned
|| resolved.azure.identity.is_none()
|| !matches!(
resolved.azure.auth,
crate::cloud::azure::AzureAuth::Bearer(_)
)
|| !enabled
{
return resolved;
}
let Some((account, container, path)) = crate::cloud::source::azure_parts(&resolved.url) else {
return resolved;
};
if crate::cloud::azure::token_reads(&account) {
return resolved;
}
match crate::cloud::azure::check_read(&account, &container, &path, &resolved.azure) {
Ok(()) => {
crate::cloud::azure::remember_token_reads(&account);
resolved
}
Err(refusal) => match crate::cloud::azure::with_account_key(
&account,
&resolved.azure,
&refusal,
enabled,
&Environment::current(),
) {
Ok(azure) => Resolved { azure, ..resolved },
Err(_) => resolved,
},
}
}
fn settle_signing(resolved: Resolved) -> Resolved {
if resolved.signing != Signing::Try {
return resolved;
}
match crate::cloud::cloud_browse::probe_unsigned(&resolved) {
Some(true) => {
remember_access(&resolved.place, true);
resolved.unsigned()
}
Some(false) => {
remember_access(&resolved.place, false);
Resolved {
signing: Signing::Signed,
..resolved
}
}
None => resolved,
}
}
pub fn expand_azure_short_url(
path: &std::path::Path,
config: &CloudConfig,
browsing: Option<&std::path::Path>,
) -> Result<std::path::PathBuf, String> {
let text = path.to_string_lossy();
let Some((scheme, rest)) = text.split_once("://") else {
return Ok(path.to_path_buf());
};
if !crate::cloud::source::is_azure_short_scheme(scheme) {
return Ok(path.to_path_buf());
}
let (container, key) = rest.split_once('/').unwrap_or((rest, ""));
if container.contains('@')
&& let Some((account, container, key)) =
crate::cloud::source::azure_parts(&format!("abfss://{rest}"))
{
return Ok(std::path::PathBuf::from(crate::cloud::source::azure_url(
&account, &container, &key,
)));
}
if container.is_empty() {
return Err(format!("{text} names no container"));
}
let from_browsing = browsing.and_then(|place| {
crate::home::cloud_account(place)
.map(|(_, account)| account)
.or_else(|| {
crate::cloud::source::azure_parts(&place.to_string_lossy()).map(|(a, _, _)| a)
})
});
let configured: Vec<&str> = config
.connections
.iter()
.filter(|s| s.kind.as_deref() == Some("azure"))
.filter_map(|s| s.account.as_deref())
.collect();
let account = from_browsing
.or_else(|| {
crate::cloud::azure::from_environment(&|k| std::env::var(k).ok())
.and_then(|(settings, _)| settings.account)
})
.or_else(|| (configured.len() == 1).then(|| configured[0].to_string()))
.ok_or_else(|| {
format!(
"{text} does not say which storage account. Use \
abfss://{container}@<account>.dfs.core.windows.net/{key}"
)
})?;
Ok(std::path::PathBuf::from(crate::cloud::source::azure_url(
&account, container, key,
)))
}
fn configured_access<'a>(url: &str, config: &'a CloudConfig) -> Option<&'a DatasetAccess> {
let (id, plain) = crate::cloud::source::split_source_id(url);
if id.is_some() {
return None;
}
config
.dataset_access
.iter()
.filter(|access| is_within(&plain, &access.url))
.rev()
.max_by_key(|access| crate::cloud::source::canonical_cloud_place(&access.url).len())
}
pub fn resolve_with(
url: &str,
config: &CloudConfig,
env: &Environment<'_>,
) -> Result<Resolved, String> {
resolve_among(url, config, &discover(config, env), env)
}
#[derive(Debug, Default)]
pub struct SessionSources(Mutex<Option<(CloudConfig, Arc<[Source]>)>>);
impl SessionSources {
pub fn get(
&self,
config: &CloudConfig,
discover: impl FnOnce() -> Vec<Source>,
) -> Arc<[Source]> {
if let Ok(kept) = self.0.lock()
&& let Some((asked, sources)) = kept.as_ref()
&& asked == config
{
return sources.clone();
}
self.refresh(config, discover)
}
pub fn refresh(
&self,
config: &CloudConfig,
discover: impl FnOnce() -> Vec<Source>,
) -> Arc<[Source]> {
let sources: Arc<[Source]> = discover().into();
if let Ok(mut kept) = self.0.lock() {
*kept = Some((config.clone(), sources.clone()));
}
sources
}
}
fn session() -> &'static SessionSources {
static SESSION: OnceLock<SessionSources> = OnceLock::new();
SESSION.get_or_init(SessionSources::default)
}
pub fn session_sources(config: &CloudConfig) -> Arc<[Source]> {
session().get(config, || discover(config, &Environment::current()))
}
pub fn rediscover(config: &CloudConfig) -> Arc<[Source]> {
session().refresh(config, || discover(config, &Environment::current()))
}
fn resolve_among(
url: &str,
config: &CloudConfig,
sources: &[Source],
env: &Environment<'_>,
) -> Result<Resolved, String> {
let place = access_key(url).ok_or_else(|| format!("not an object-store URL: {url}"))?;
let configured = configured_access(url, config);
if let Some(DatasetAccess {
auth: DatasetAuth::Anonymous,
catalog,
..
}) = configured
{
let resolved = match crate::cloud::source::azure_parts(url) {
Some((account, container, path)) => Resolved {
url: crate::cloud::source::azure_url(&account, &container, &path),
kind: ProviderKind::Azure,
source_id: catalog.clone(),
s3: S3Settings::default(),
azure: Default::default(),
signing: Signing::Unsigned,
place,
gcloud: None,
google_credentials: None,
login_error: None,
},
None => {
let (kind, _, _) = crate::cloud::cloud_browse::split_bucket_url(url)
.ok_or_else(|| format!("not an object-store URL: {url}"))?;
Resolved {
url: url.to_string(),
kind,
source_id: catalog.clone(),
s3: S3Settings::default(),
azure: Default::default(),
signing: Signing::Unsigned,
place,
gcloud: None,
google_credentials: None,
login_error: None,
}
}
};
return Ok(resolved.unsigned());
}
let connection = match configured {
Some(DatasetAccess {
auth: DatasetAuth::Connection(name),
..
}) => match sources.iter().find(|s| &s.id == name) {
Some(source) => Some(source.clone()),
None => return Err(format!("no connection is named \"{name}\"")),
},
_ => None,
};
let known = access()
.lock()
.ok()
.and_then(|map| map.get(&place).copied())
.filter(|_| connection.is_none());
if let Some(parts) = crate::cloud::source::azure_parts(url) {
return resolve_azure(&parts, sources, connection.as_ref(), known, place, env);
}
let (id, plain) = crate::cloud::source::split_source_id(url);
let (kind, bucket, _) = crate::cloud::cloud_browse::split_bucket_url(&plain)
.ok_or_else(|| format!("not an object-store URL: {url}"))?;
let find = |id: &str| sources.iter().find(|s| s.id == id).cloned();
let mut owned = true;
let mut no_login = false;
let source = match (id, connection) {
(None, Some(source)) => source,
(Some(id), _) => {
let Some(source) = find(id) else {
return Err(unknown_source(id, sources));
};
if !source.named_in_urls() {
return Err(format!(
"\"{id}\" has no endpoint, so it is not S3-compatible and its URLs are \
plain s3://bucket/key"
));
}
source
}
(None, None) => match remembered(kind, &bucket)
.and_then(|id| find(&id))
.filter(|s| s.kind == kind)
{
Some(source) => source,
None => {
let default = Source::default_for(kind, config, Tier::Environment, "");
owned = false;
no_login = find(&default.id).is_none();
find(&default.id).unwrap_or(default)
}
},
};
let signing = match known {
Some(true) => Signing::Unsigned,
Some(false) => Signing::Signed,
None if no_login => Signing::Unsigned,
None if owned => Signing::Signed,
None => Signing::Try,
};
let resolved = Resolved {
url: plain.into_owned(),
kind,
source_id: source.id.clone(),
s3: source.s3.clone(),
azure: Default::default(),
signing,
place,
gcloud: None,
google_credentials: None,
login_error: None,
};
if signing == Signing::Unsigned {
return Ok(resolved.unsigned());
}
let id = source.id.clone();
let gcloud = match &source.gcloud {
Some(configuration) if kind == ProviderKind::Gcs => {
match crate::cloud::gcloud::token(configuration, env) {
Ok((token, _)) => Some((configuration.clone(), token)),
Err(e) => return login_failed(resolved, format!("source \"{id}\": {e}")),
}
}
_ => None,
};
let source = match source.with_credentials(env) {
Ok(source) => source,
Err(e) => return login_failed(resolved, format!("source \"{id}\": {e}")),
};
let google_credentials = match kind {
ProviderKind::Gcs => source.google_credentials.clone(),
_ => None,
};
Ok(Resolved {
s3: source.s3,
gcloud,
google_credentials,
..resolved
})
}
fn login_failed(resolved: Resolved, error: String) -> Result<Resolved, String> {
if resolved.signing != Signing::Try {
return Err(error);
}
Ok(Resolved {
login_error: Some(error),
..resolved.unsigned()
})
}
fn resolve_azure(
(account, container, path): &(String, String, String),
sources: &[Source],
connection: Option<&Source>,
known: Option<bool>,
place: String,
env: &Environment<'_>,
) -> Result<Resolved, String> {
let named = connection.or_else(|| {
sources
.iter()
.find(|s| s.kind == ProviderKind::Azure && s.azure.account.as_ref() == Some(account))
});
let login = sources
.iter()
.filter(|s| s.problem.is_none())
.find(|s| s.id == DEFAULT_AZURE_LOGIN)
.or_else(|| {
sources.iter().find(|s| {
s.id == DEFAULT_AZURE_ENV
&& s.problem.is_none()
&& s.azure.account.is_none()
&& s.azure.auth.is_identity()
})
});
let signing = match (known, named, login) {
(Some(true), _, _) | (None, None, None) => Signing::Unsigned,
(Some(false), _, _) | (None, Some(_), _) => Signing::Signed,
(None, None, Some(_)) => Signing::Try,
};
let resolved = Resolved {
url: crate::cloud::source::azure_url(account, container, path),
kind: ProviderKind::Azure,
source_id: String::new(),
s3: S3Settings::default(),
azure: Default::default(),
signing,
place,
gcloud: None,
google_credentials: None,
login_error: None,
};
match named.or(login) {
Some(source) if signing != Signing::Unsigned => {
if source.azure.auth.is_identity()
&& let Some(key) = crate::cloud::azure::remembered_key(account)
{
return Ok(Resolved {
source_id: source.id.clone(),
azure: crate::cloud::azure::AzureSettings {
identity: Some(source.azure.auth.clone()),
auth: crate::cloud::azure::AzureAuth::Key(key),
..source.azure.clone()
},
signing: Signing::Signed,
..resolved
});
}
match source.azure.clone().with_token(env) {
Ok(azure) => Ok(Resolved {
source_id: source.id.clone(),
azure,
..resolved
}),
Err(e) => login_failed(resolved, format!("source \"{}\": {e}", source.id)),
}
}
_ => Ok(resolved.unsigned()),
}
}
fn unknown_source(id: &str, sources: &[Source]) -> String {
let names: Vec<&str> = sources
.iter()
.filter(|s| s.named_in_urls())
.map(|s| s.id.as_str())
.collect();
if names.is_empty() {
format!("no S3-compatible source is named \"{id}\"")
} else {
format!(
"no S3-compatible source is named \"{id}\". Sources: {}",
names.join(", ")
)
}
}
#[cfg(test)]
mod tests;