diff --git a/src/iceberg/catalog/rest/auth/auth_manager.cc b/src/iceberg/catalog/rest/auth/auth_manager.cc index af02f747a..14946aef6 100644 --- a/src/iceberg/catalog/rest/auth/auth_manager.cc +++ b/src/iceberg/catalog/rest/auth/auth_manager.cc @@ -19,7 +19,11 @@ #include "iceberg/catalog/rest/auth/auth_manager.h" +#include "iceberg/catalog/rest/auth/auth_manager_internal.h" +#include "iceberg/catalog/rest/auth/auth_properties.h" #include "iceberg/catalog/rest/auth/auth_session.h" +#include "iceberg/util/macros.h" +#include "iceberg/util/transform_util.h" namespace iceberg::rest::auth { @@ -45,4 +49,45 @@ Result> AuthManager::TableSession( return parent; } +/// \brief Authentication manager that performs no authentication. +class NoopAuthManager : public AuthManager { + public: + Result> CatalogSession( + [[maybe_unused]] HttpClient& client, + [[maybe_unused]] const std::unordered_map& properties) + override { + return AuthSession::MakeDefault({}); + } +}; + +Result> MakeNoopAuthManager( + [[maybe_unused]] std::string_view name, + [[maybe_unused]] const std::unordered_map& properties) { + return std::make_unique(); +} + +/// \brief Authentication manager that performs basic authentication. +class BasicAuthManager : public AuthManager { + public: + Result> CatalogSession( + [[maybe_unused]] HttpClient& client, + const std::unordered_map& properties) override { + auto username_it = properties.find(AuthProperties::kBasicUsername); + ICEBERG_PRECHECK(username_it != properties.end() && !username_it->second.empty(), + "Missing required property '{}'", AuthProperties::kBasicUsername); + auto password_it = properties.find(AuthProperties::kBasicPassword); + ICEBERG_PRECHECK(password_it != properties.end() && !password_it->second.empty(), + "Missing required property '{}'", AuthProperties::kBasicPassword); + std::string credential = username_it->second + ":" + password_it->second; + return AuthSession::MakeDefault( + {{"Authorization", "Basic " + TransformUtil::Base64Encode(credential)}}); + } +}; + +Result> MakeBasicAuthManager( + [[maybe_unused]] std::string_view name, + [[maybe_unused]] const std::unordered_map& properties) { + return std::make_unique(); +} + } // namespace iceberg::rest::auth diff --git a/src/iceberg/catalog/rest/auth/auth_manager_internal.h b/src/iceberg/catalog/rest/auth/auth_manager_internal.h new file mode 100644 index 000000000..96e452390 --- /dev/null +++ b/src/iceberg/catalog/rest/auth/auth_manager_internal.h @@ -0,0 +1,45 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include +#include + +#include "iceberg/catalog/rest/auth/auth_manager.h" +#include "iceberg/result.h" + +/// \file iceberg/catalog/rest/auth/auth_manager_internal.h +/// \brief Internal factory functions for built-in AuthManager implementations. + +namespace iceberg::rest::auth { + +/// \brief Create a no-op authentication manager (no authentication). +Result> MakeNoopAuthManager( + std::string_view name, + const std::unordered_map& properties); + +/// \brief Create a basic authentication manager. +Result> MakeBasicAuthManager( + std::string_view name, + const std::unordered_map& properties); + +} // namespace iceberg::rest::auth diff --git a/src/iceberg/catalog/rest/auth/auth_managers.cc b/src/iceberg/catalog/rest/auth/auth_managers.cc index c1fe45f87..d0bf24844 100644 --- a/src/iceberg/catalog/rest/auth/auth_managers.cc +++ b/src/iceberg/catalog/rest/auth/auth_managers.cc @@ -21,8 +21,8 @@ #include +#include "iceberg/catalog/rest/auth/auth_manager_internal.h" #include "iceberg/catalog/rest/auth/auth_properties.h" -#include "iceberg/catalog/rest/auth/auth_session.h" #include "iceberg/util/string_util.h" namespace iceberg::rest::auth { @@ -61,33 +61,10 @@ std::string InferAuthType( return AuthProperties::kAuthTypeNone; } -/// \brief Authentication manager that performs no authentication. -class NoopAuthManager : public AuthManager { - public: - static Result> Make( - [[maybe_unused]] std::string_view name, - [[maybe_unused]] const std::unordered_map& properties) { - return std::make_unique(); - } - - Result> CatalogSession( - [[maybe_unused]] HttpClient& client, - [[maybe_unused]] const std::unordered_map& properties) - override { - return AuthSession::MakeDefault({}); - } -}; - -template -AuthManagerFactory MakeAuthFactory() { - return - [](std::string_view name, const std::unordered_map& props) - -> Result> { return T::Make(name, props); }; -} - AuthManagerRegistry CreateDefaultRegistry() { return { - {AuthProperties::kAuthTypeNone, MakeAuthFactory()}, + {AuthProperties::kAuthTypeNone, MakeNoopAuthManager}, + {AuthProperties::kAuthTypeBasic, MakeBasicAuthManager}, }; } diff --git a/src/iceberg/test/auth_manager_test.cc b/src/iceberg/test/auth_manager_test.cc index c6e9f1231..82db393d0 100644 --- a/src/iceberg/test/auth_manager_test.cc +++ b/src/iceberg/test/auth_manager_test.cc @@ -80,6 +80,90 @@ TEST_F(AuthManagerTest, UnknownAuthTypeReturnsInvalidArgument) { EXPECT_THAT(result, HasErrorMessage("Unknown authentication type")); } +// Verifies loading BasicAuthManager with valid credentials +TEST_F(AuthManagerTest, LoadBasicAuthManager) { + std::unordered_map properties = { + {AuthProperties::kAuthType, "basic"}, + {AuthProperties::kBasicUsername, "admin"}, + {AuthProperties::kBasicPassword, "secret"}}; + + auto manager_result = AuthManagers::Load("test-catalog", properties); + ASSERT_THAT(manager_result, IsOk()); + + auto session_result = manager_result.value()->CatalogSession(client_, properties); + ASSERT_THAT(session_result, IsOk()); + + std::unordered_map headers; + EXPECT_THAT(session_result.value()->Authenticate(headers), IsOk()); + // base64("admin:secret") == "YWRtaW46c2VjcmV0" + EXPECT_EQ(headers["Authorization"], "Basic YWRtaW46c2VjcmV0"); +} + +// Verifies BasicAuthManager is case-insensitive for auth type +TEST_F(AuthManagerTest, BasicAuthTypeCaseInsensitive) { + for (const auto& auth_type : {"BASIC", "Basic", "bAsIc"}) { + std::unordered_map properties = { + {AuthProperties::kAuthType, auth_type}, + {AuthProperties::kBasicUsername, "user"}, + {AuthProperties::kBasicPassword, "pass"}}; + auto manager_result = AuthManagers::Load("test-catalog", properties); + ASSERT_THAT(manager_result, IsOk()) << "Failed for auth type: " << auth_type; + + auto session_result = manager_result.value()->CatalogSession(client_, properties); + ASSERT_THAT(session_result, IsOk()) << "Failed for auth type: " << auth_type; + + std::unordered_map headers; + EXPECT_THAT(session_result.value()->Authenticate(headers), IsOk()); + // base64("user:pass") == "dXNlcjpwYXNz" + EXPECT_EQ(headers["Authorization"], "Basic dXNlcjpwYXNz"); + } +} + +// Verifies BasicAuthManager fails when username is missing +TEST_F(AuthManagerTest, BasicAuthMissingUsername) { + std::unordered_map properties = { + {AuthProperties::kAuthType, "basic"}, {AuthProperties::kBasicPassword, "secret"}}; + + auto manager_result = AuthManagers::Load("test-catalog", properties); + ASSERT_THAT(manager_result, IsOk()); + + auto session_result = manager_result.value()->CatalogSession(client_, properties); + EXPECT_THAT(session_result, IsError(ErrorKind::kInvalidArgument)); + EXPECT_THAT(session_result, HasErrorMessage("Missing required property")); +} + +// Verifies BasicAuthManager fails when password is missing +TEST_F(AuthManagerTest, BasicAuthMissingPassword) { + std::unordered_map properties = { + {AuthProperties::kAuthType, "basic"}, {AuthProperties::kBasicUsername, "admin"}}; + + auto manager_result = AuthManagers::Load("test-catalog", properties); + ASSERT_THAT(manager_result, IsOk()); + + auto session_result = manager_result.value()->CatalogSession(client_, properties); + EXPECT_THAT(session_result, IsError(ErrorKind::kInvalidArgument)); + EXPECT_THAT(session_result, HasErrorMessage("Missing required property")); +} + +// Verifies BasicAuthManager handles special characters in credentials +TEST_F(AuthManagerTest, BasicAuthSpecialCharacters) { + std::unordered_map properties = { + {AuthProperties::kAuthType, "basic"}, + {AuthProperties::kBasicUsername, "user@domain.com"}, + {AuthProperties::kBasicPassword, "p@ss:w0rd!"}}; + + auto manager_result = AuthManagers::Load("test-catalog", properties); + ASSERT_THAT(manager_result, IsOk()); + + auto session_result = manager_result.value()->CatalogSession(client_, properties); + ASSERT_THAT(session_result, IsOk()); + + std::unordered_map headers; + EXPECT_THAT(session_result.value()->Authenticate(headers), IsOk()); + // base64("user@domain.com:p@ss:w0rd!") == "dXNlckBkb21haW4uY29tOnBAc3M6dzByZCE=" + EXPECT_EQ(headers["Authorization"], "Basic dXNlckBkb21haW4uY29tOnBAc3M6dzByZCE="); +} + // Verifies custom auth manager registration TEST_F(AuthManagerTest, RegisterCustomAuthManager) { AuthManagers::Register(