From 2fdd879f14112b9929046e3ec27de9840e7a30a0 Mon Sep 17 00:00:00 2001 From: Alex Stephen Date: Tue, 15 Jul 2025 17:18:13 -0700 Subject: [PATCH] Add initial AuthManager support --- Cargo.lock | 1 + crates/catalog/rest/Cargo.toml | 2 + crates/catalog/rest/src/auth/mod.rs | 60 +++++++++++++++++++++++++++++ crates/catalog/rest/src/lib.rs | 2 + crates/catalog/rest/tests/auth.rs | 38 ++++++++++++++++++ 5 files changed, 103 insertions(+) create mode 100644 crates/catalog/rest/src/auth/mod.rs create mode 100644 crates/catalog/rest/tests/auth.rs diff --git a/Cargo.lock b/Cargo.lock index ca88923c43..ad6cdd5c11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3612,6 +3612,7 @@ name = "iceberg-catalog-rest" version = "0.5.1" dependencies = [ "async-trait", + "base64 0.22.1", "chrono", "ctor", "http 1.3.1", diff --git a/crates/catalog/rest/Cargo.toml b/crates/catalog/rest/Cargo.toml index 916b5ccf75..ef04f31a75 100644 --- a/crates/catalog/rest/Cargo.toml +++ b/crates/catalog/rest/Cargo.toml @@ -30,6 +30,7 @@ repository = { workspace = true } [dependencies] async-trait = { workspace = true } +base64 = { workspace = true } chrono = { workspace = true } http = { workspace = true } iceberg = { workspace = true } @@ -44,6 +45,7 @@ typed-builder = { workspace = true } uuid = { workspace = true, features = ["v4"] } [dev-dependencies] +base64 = { workspace = true } ctor = { workspace = true } iceberg_test_utils = { path = "../../test_utils", features = ["tests"] } mockito = { workspace = true } diff --git a/crates/catalog/rest/src/auth/mod.rs b/crates/catalog/rest/src/auth/mod.rs new file mode 100644 index 0000000000..0f1df0eecc --- /dev/null +++ b/crates/catalog/rest/src/auth/mod.rs @@ -0,0 +1,60 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use async_trait::async_trait; +use base64::Engine; +use base64::engine::general_purpose; + +/// Trait for authentication managers that supply authorization headers. +#[async_trait] +pub trait AuthManager: Send + Sync + std::fmt::Debug { + /// Return the Authorization header value, or None if not applicable. + async fn auth_header(&self) -> Option; +} + +/// An `AuthManager` that performs no authentication. +#[derive(Debug, Default)] +pub struct NoopAuthManager; + +#[async_trait] +impl AuthManager for NoopAuthManager { + async fn auth_header(&self) -> Option { + None + } +} + +/// An `AuthManager` for basic authentication. +#[derive(Debug)] +pub struct BasicAuthManager { + token: String, +} + +impl BasicAuthManager { + /// Create a new `BasicAuthManager`. + pub fn new(username: &str, password: &str) -> Self { + let credentials = format!("{}:{}", username, password); + let token = general_purpose::STANDARD.encode(credentials.as_bytes()); + BasicAuthManager { token } + } +} + +#[async_trait] +impl AuthManager for BasicAuthManager { + async fn auth_header(&self) -> Option { + Some(format!("Basic {}", self.token)) + } +} diff --git a/crates/catalog/rest/src/lib.rs b/crates/catalog/rest/src/lib.rs index f94ee87815..a0ae53dedf 100644 --- a/crates/catalog/rest/src/lib.rs +++ b/crates/catalog/rest/src/lib.rs @@ -19,6 +19,8 @@ #![deny(missing_docs)] +/// Authentication for the REST catalog. +pub mod auth; mod catalog; mod client; mod types; diff --git a/crates/catalog/rest/tests/auth.rs b/crates/catalog/rest/tests/auth.rs new file mode 100644 index 0000000000..e230e0dcfd --- /dev/null +++ b/crates/catalog/rest/tests/auth.rs @@ -0,0 +1,38 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use base64::Engine; +use iceberg_catalog_rest::auth::{AuthManager, BasicAuthManager, NoopAuthManager}; + +#[tokio::test] +async fn test_noop_auth_manager() { + let auth_manager = NoopAuthManager; + let header = auth_manager.auth_header().await; + assert!(header.is_none()); +} + +#[tokio::test] +async fn test_basic_auth_manager() { + let username = "testuser"; + let password = "testpassword"; + let auth_manager = BasicAuthManager::new(username, password); + let header = auth_manager.auth_header().await; + assert!(header.is_some()); + let expected_token = base64::engine::general_purpose::STANDARD + .encode(format!("{}:{}", username, password).as_bytes()); + assert_eq!(header.unwrap(), format!("Basic {}", expected_token)); +}