Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ lance-io = { version = "=2.0.0-beta.4", path = "./rust/lance-io", default-featur
lance-linalg = { version = "=2.0.0-beta.4", path = "./rust/lance-linalg" }
lance-namespace = { version = "=2.0.0-beta.4", path = "./rust/lance-namespace" }
lance-namespace-impls = { version = "=2.0.0-beta.4", path = "./rust/lance-namespace-impls" }
lance-namespace-reqwest-client = "=0.4.0"
lance-namespace-reqwest-client = { version = "=0.4.5" }
lance-table = { version = "=2.0.0-beta.4", path = "./rust/lance-table" }
lance-test-macros = { version = "=2.0.0-beta.4", path = "./rust/lance-test-macros" }
lance-testing = { version = "=2.0.0-beta.4", path = "./rust/lance-testing" }
Expand Down
6 changes: 4 additions & 2 deletions java/lance-jni/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions java/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,12 @@
<dependency>
<groupId>org.lance</groupId>
<artifactId>lance-namespace-core</artifactId>
<version>0.4.0</version>
<version>0.4.5</version>
</dependency>
<dependency>
<groupId>org.lance</groupId>
<artifactId>lance-namespace-apache-client</artifactId>
<version>0.4.0</version>
<version>0.4.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
Expand Down
6 changes: 4 additions & 2 deletions python/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[project]
name = "pylance"
dynamic = ["version"]
dependencies = ["pyarrow>=14", "numpy>=1.22", "lance-namespace>=0.4.0"]
dependencies = ["pyarrow>=14", "numpy>=1.22", "lance-namespace>=0.4.5"]
description = "python wrapper for Lance columnar format"
authors = [{ name = "Lance Devs", email = "dev@lance.org" }]
license = { file = "LICENSE" }
Expand Down
3 changes: 1 addition & 2 deletions rust/lance-io/src/object_store/storage_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,7 @@ impl StorageOptionsProvider for LanceNamespaceStorageOptionsProvider {
async fn fetch_storage_options(&self) -> Result<Option<HashMap<String, String>>> {
let request = DescribeTableRequest {
id: Some(self.table_id.clone()),
version: None,
with_table_uri: None,
..Default::default()
};

let response = self
Expand Down
10 changes: 6 additions & 4 deletions rust/lance-namespace-impls/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ dir-azure = ["lance-io/azure", "lance/azure"]
dir-oss = ["lance-io/oss", "lance/oss"]
dir-huggingface = ["lance-io/huggingface", "lance/huggingface"]
# Credential vending features
credential-vendor-aws = ["dep:aws-sdk-sts", "dep:aws-config", "dep:aws-credential-types"]
credential-vendor-gcp = ["dep:google-cloud-auth", "dep:reqwest", "dep:serde"]
credential-vendor-azure = ["dep:azure_core", "dep:azure_identity", "dep:azure_storage", "dep:azure_storage_blobs", "dep:time"]
credential-vendor-aws = ["dep:aws-sdk-sts", "dep:aws-config", "dep:aws-credential-types", "dep:sha2", "dep:base64"]
credential-vendor-gcp = ["dep:google-cloud-auth", "dep:reqwest", "dep:serde", "dep:sha2", "dep:base64"]
credential-vendor-azure = ["dep:azure_core", "dep:azure_identity", "dep:azure_storage", "dep:azure_storage_blobs", "dep:time", "dep:sha2", "dep:base64", "dep:reqwest"]

[dependencies]
lance-namespace.workspace = true
Expand Down Expand Up @@ -66,10 +66,12 @@ log.workspace = true
rand.workspace = true
chrono.workspace = true

# AWS credential vending dependencies (optional, enabled by "dir-aws" feature)
# AWS credential vending dependencies (optional, enabled by "credential-vendor-aws" feature)
aws-sdk-sts = { version = "1.38.0", optional = true }
aws-config = { workspace = true, optional = true }
aws-credential-types = { workspace = true, optional = true }
sha2 = { version = "0.10", optional = true }
base64 = { version = "0.22", optional = true }

# GCP credential vending dependencies (optional, enabled by "dir-gcp" feature)
google-cloud-auth = { version = "0.18", optional = true }
Expand Down
92 changes: 85 additions & 7 deletions rust/lance-namespace-impls/src/credentials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,22 @@ pub mod azure;
#[cfg(feature = "credential-vendor-gcp")]
pub mod gcp;

/// Credential caching module.
/// Available when any credential vendor feature is enabled.
#[cfg(any(
feature = "credential-vendor-aws",
feature = "credential-vendor-azure",
feature = "credential-vendor-gcp"
))]
pub mod cache;

use std::collections::HashMap;
use std::str::FromStr;

use async_trait::async_trait;
use lance_core::Result;
use lance_io::object_store::uri_to_url;
use lance_namespace::models::Identity;

/// Default credential duration: 1 hour (3600000 milliseconds)
pub const DEFAULT_CREDENTIAL_DURATION_MILLIS: u64 = 3600 * 1000;
Expand Down Expand Up @@ -188,6 +198,18 @@ pub const ENABLED: &str = "enabled";
/// Common property key for permission level (short form).
pub const PERMISSION: &str = "permission";

/// Common property key to enable credential caching (short form).
/// Default: true. Set to "false" to disable caching.
pub const CACHE_ENABLED: &str = "cache_enabled";

/// Common property key for API key salt (short form).
/// Used to hash API keys before comparison: SHA256(api_key + ":" + salt)
pub const API_KEY_SALT: &str = "api_key_salt";

/// Property key prefix for API key hash to permission mappings (short form).
/// Format: `api_key_hash.<sha256_hash> = "<permission>"`
pub const API_KEY_HASH_PREFIX: &str = "api_key_hash.";

/// AWS-specific property keys (short form, without prefix)
#[cfg(feature = "credential-vendor-aws")]
pub mod aws_props {
Expand All @@ -204,6 +226,14 @@ pub mod aws_props {
#[cfg(feature = "credential-vendor-gcp")]
pub mod gcp_props {
pub const SERVICE_ACCOUNT: &str = "gcp_service_account";

/// Workload Identity Provider resource name for OIDC token exchange.
/// Format: //iam.googleapis.com/projects/{project}/locations/global/workloadIdentityPools/{pool}/providers/{provider}
pub const WORKLOAD_IDENTITY_PROVIDER: &str = "gcp_workload_identity_provider";

/// Service account to impersonate after Workload Identity Federation (optional).
/// If not set, uses the federated identity directly.
pub const IMPERSONATION_SERVICE_ACCOUNT: &str = "gcp_impersonation_service_account";
}

/// Azure-specific property keys (short form, without prefix)
Expand All @@ -215,6 +245,10 @@ pub mod azure_props {
/// Azure credential duration in milliseconds.
/// Default: 3600000 (1 hour). Azure SAS tokens can be valid up to 7 days.
pub const DURATION_MILLIS: &str = "azure_duration_millis";

/// Client ID of the Azure AD App Registration for Workload Identity Federation.
/// Required when using auth_token identity for OIDC token exchange.
pub const FEDERATED_CLIENT_ID: &str = "azure_federated_client_id";
}

/// Vended credentials with expiration information.
Expand Down Expand Up @@ -271,16 +305,30 @@ pub trait CredentialVendor: Send + Sync + std::fmt::Debug {
/// Vend credentials for accessing the specified table location.
///
/// The permission level (read/write/admin) is determined by the vendor's
/// configuration, not per-request.
/// configuration, not per-request. When identity is provided, the vendor
/// may use different authentication flows:
///
/// - `auth_token`: Use AssumeRoleWithWebIdentity (AWS validates the token)
/// - `api_key`: Validate against configured API key hashes and use AssumeRole
/// - `None`: Use static configuration with AssumeRole
///
/// # Arguments
///
/// * `table_location` - The table URI to vend credentials for
/// * `identity` - Optional identity from the request (api_key OR auth_token, mutually exclusive)
///
/// # Returns
///
/// Returns vended credentials with expiration information.
async fn vend_credentials(&self, table_location: &str) -> Result<VendedCredentials>;
///
/// # Errors
///
/// Returns error if identity validation fails (no fallback to static config).
async fn vend_credentials(
&self,
table_location: &str,
identity: Option<&Identity>,
) -> Result<VendedCredentials>;

/// Returns the cloud provider name (e.g., "aws", "gcp", "azure").
fn provider_name(&self) -> &'static str;
Expand Down Expand Up @@ -349,21 +397,50 @@ pub async fn create_credential_vendor_for_location(
) -> Result<Option<Box<dyn CredentialVendor>>> {
let provider = detect_provider_from_uri(table_location);

match provider {
let vendor: Option<Box<dyn CredentialVendor>> = match provider {
#[cfg(feature = "credential-vendor-aws")]
"aws" => create_aws_vendor(properties).await,
"aws" => create_aws_vendor(properties).await?,

#[cfg(feature = "credential-vendor-gcp")]
"gcp" => create_gcp_vendor(properties).await,
"gcp" => create_gcp_vendor(properties).await?,

#[cfg(feature = "credential-vendor-azure")]
"azure" => create_azure_vendor(properties),
"azure" => create_azure_vendor(properties)?,

_ => None,
};

_ => Ok(None),
// Wrap with caching if enabled (default: true)
#[cfg(any(
feature = "credential-vendor-aws",
feature = "credential-vendor-azure",
feature = "credential-vendor-gcp"
))]
if let Some(v) = vendor {
let cache_enabled = properties
.get(CACHE_ENABLED)
.map(|s| !s.eq_ignore_ascii_case("false"))
.unwrap_or(true);

if cache_enabled {
return Ok(Some(Box::new(cache::CachingCredentialVendor::new(v))));
} else {
return Ok(Some(v));
}
}

#[cfg(not(any(
feature = "credential-vendor-aws",
feature = "credential-vendor-azure",
feature = "credential-vendor-gcp"
)))]
let _ = vendor;

Ok(None)
}

/// Parse permission from properties, defaulting to Read
#[allow(dead_code)]
fn parse_permission(properties: &HashMap<String, String>) -> VendedPermission {
properties
.get(PERMISSION)
Expand All @@ -372,6 +449,7 @@ fn parse_permission(properties: &HashMap<String, String>) -> VendedPermission {
}

/// Parse duration from properties using a vendor-specific key, defaulting to DEFAULT_CREDENTIAL_DURATION_MILLIS
#[allow(dead_code)]
fn parse_duration_millis(properties: &HashMap<String, String>, key: &str) -> u64 {
properties
.get(key)
Expand Down
Loading
Loading