Skip to content

Commit f8ae67c

Browse files
author
Michael Harris
authored
Merge pull request #104 from mharrisb1/87-allow-the-user-to-provide-access-key-via-parameter
feat(auth): allow providing raw private key and email instead of filepath
2 parents 3e5e818 + 52f329e commit f8ae67c

10 files changed

Lines changed: 174 additions & 59 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ set(EXTENSION_SOURCES
3636
src/sheets/range.cpp
3737
src/sheets/auth_factory.cpp
3838
src/utils/secret.cpp
39+
src/utils/options.cpp
3940
src/utils/proxy.cpp
4041
src/utils/version.cpp)
4142

docs/pages/index.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ CREATE SECRET (
5050
PROVIDER key_file,
5151
FILEPATH '<path_to_JSON_file_with_private_key>'
5252
);
53+
54+
-- OR by passing the secret directly
55+
CREATE SECRET (
56+
TYPE gsheet,
57+
PROVIDER key_file,
58+
EMAIL '<service_account_email>',
59+
SECRET '<private_key>'
60+
);
5361
```
5462

5563
### HTTP Proxy

src/gsheets_auth.cpp

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
#include <cstdlib>
33
#include <json.hpp>
44

5-
#include "duckdb.hpp"
5+
#include "duckdb/common/exception/binder_exception.hpp"
66

77
#include "gsheets_auth.hpp"
88
#include "gsheets_utils.hpp"
9+
#include "utils/options.hpp"
910

1011
using json = nlohmann::json;
1112

@@ -30,7 +31,6 @@ static void RedactCommonKeys(KeyValueSecret &result) {
3031
result.redact_keys.insert("proxy_password");
3132
}
3233

33-
// TODO: Maybe this should be a KeyValueSecret
3434
static unique_ptr<BaseSecret> CreateGsheetSecretFromAccessToken(ClientContext &context, CreateSecretInput &input) {
3535
auto scope = input.scope;
3636

@@ -63,23 +63,31 @@ static unique_ptr<BaseSecret> CreateGsheetSecretFromOAuth(ClientContext &context
6363
return std::move(result);
6464
}
6565

66-
// TODO: Maybe this should be a KeyValueSecret
6766
static unique_ptr<BaseSecret> CreateGsheetSecretFromKeyFile(ClientContext &context, CreateSecretInput &input) {
6867
auto scope = input.scope;
6968

7069
auto result = make_uniq<KeyValueSecret>(scope, input.type, input.provider, input.name);
7170

72-
// Want to store the private key and email in case the secret is persisted
73-
std::string filepath_key = "filepath";
74-
auto filepath = (input.options.find(filepath_key)->second).ToString();
75-
76-
std::ifstream ifs(filepath);
77-
if (!ifs.is_open()) {
78-
throw IOException("Could not open JSON key file at: " + filepath);
71+
std::string email, secret;
72+
auto filepath = duckdb::sheets::GetStringOption(input.options, "filepath");
73+
if (filepath.empty()) {
74+
email = duckdb::sheets::GetStringOption(input.options, "email");
75+
if (email.empty()) {
76+
throw BinderException("Must provide email if not using filepath");
77+
}
78+
secret = duckdb::sheets::GetStringOption(input.options, "secret");
79+
if (email.empty()) {
80+
throw BinderException("Must provide secret value if not using filepath");
81+
}
82+
} else {
83+
std::ifstream ifs(filepath);
84+
if (!ifs.is_open()) {
85+
throw IOException("Could not open JSON key file at: " + filepath);
86+
}
87+
json credentials_file = json::parse(ifs);
88+
email = credentials_file["client_email"].get<std::string>();
89+
secret = credentials_file["private_key"].get<std::string>();
7990
}
80-
json credentials_file = json::parse(ifs);
81-
std::string email = credentials_file["client_email"].get<std::string>();
82-
std::string secret = credentials_file["private_key"].get<std::string>();
8391

8492
// Manage specific secret option
8593
(*result).secret_map["email"] = Value(email);
@@ -119,6 +127,8 @@ void CreateGsheetSecretFunctions::Register(ExtensionLoader &loader) {
119127
// Register the key_file secret provider
120128
CreateSecretFunction key_file_function = {type, "key_file", CreateGsheetSecretFromKeyFile, {}};
121129
key_file_function.named_parameters["filepath"] = LogicalType::VARCHAR;
130+
key_file_function.named_parameters["email"] = LogicalType::VARCHAR;
131+
key_file_function.named_parameters["secret"] = LogicalType::VARCHAR;
122132
RegisterCommonSecretParameters(key_file_function);
123133

124134
loader.RegisterSecretType(secret_type);

src/gsheets_copy.cpp

Lines changed: 8 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
#include <utility>
22

3-
#include "duckdb/common/case_insensitive_map.hpp"
43
#include "duckdb/common/exception.hpp"
54
#include "duckdb/common/exception/binder_exception.hpp"
65
#include "duckdb/common/file_system.hpp"
@@ -10,6 +9,8 @@
109
#include "gsheets_copy.hpp"
1110
#include "gsheets_utils.hpp"
1211

12+
#include "utils/options.hpp"
13+
1314
#include "sheets/auth_factory.hpp"
1415
#include "sheets/client.hpp"
1516
#include "sheets/exception.hpp"
@@ -26,58 +27,20 @@ GSheetCopyFunction::GSheetCopyFunction() : CopyFunction("gsheet") {
2627
copy_to_sink = GSheetWriteSink;
2728
}
2829

29-
static std::string GetStringOption(const case_insensitive_map_t<vector<Value>> &options, const std::string &name,
30-
const std::string &default_value = "") {
31-
const auto it = options.find(name);
32-
if (it == options.end()) {
33-
return default_value;
34-
}
35-
std::string err;
36-
Value val;
37-
if (!it->second.back().DefaultTryCastAs(LogicalType::VARCHAR, val, &err)) {
38-
throw BinderException(name + " option must be VARCHAR");
39-
}
40-
if (val.IsNull()) {
41-
throw BinderException(name + " option must not be NULL");
42-
}
43-
return StringValue::Get(val);
44-
}
45-
46-
// NOTE: the second value in pair is a flag indicating if the value was set by the user
47-
static std::pair<bool, bool> GetBoolOption(const case_insensitive_map_t<vector<Value>> &options,
48-
const std::string &name, bool default_value = false) {
49-
const auto it = options.find(name);
50-
if (it == options.end()) {
51-
return std::make_pair(default_value, false);
52-
}
53-
if (it->second.size() != 1) {
54-
throw BinderException(name + " option must be a single boolean value");
55-
}
56-
std::string err;
57-
Value val;
58-
if (!it->second.back().DefaultTryCastAs(LogicalType::BOOLEAN, val, &err)) {
59-
throw BinderException(name + " option must be a single boolean value");
60-
}
61-
if (val.IsNull()) {
62-
throw BinderException(name + " option must be a single boolean value");
63-
}
64-
return std::make_pair(BooleanValue::Get(val), true);
65-
}
66-
6730
unique_ptr<FunctionData> GSheetCopyFunction::GSheetWriteBind(ClientContext &context, CopyFunctionBindInput &input,
6831
const vector<string> &names,
6932
const vector<LogicalType> &sql_types) {
7033

7134
string file_path = input.info.file_path;
7235
auto options = input.info.options;
7336

74-
auto sheet = GetStringOption(options, "sheet");
75-
auto range = GetStringOption(options, "range");
76-
bool overwrite_sheet = GetBoolOption(options, "overwrite_sheet", true).first;
77-
bool overwrite_range = GetBoolOption(options, "overwrite_range", false).first;
78-
bool create_if_not_exists = GetBoolOption(options, "create_if_not_exists", false).first;
37+
auto sheet = duckdb::sheets::GetStringOption(options, "sheet");
38+
auto range = duckdb::sheets::GetStringOption(options, "range");
39+
bool overwrite_sheet = duckdb::sheets::GetBoolOption(options, "overwrite_sheet", true).first;
40+
bool overwrite_range = duckdb::sheets::GetBoolOption(options, "overwrite_range", false).first;
41+
bool create_if_not_exists = duckdb::sheets::GetBoolOption(options, "create_if_not_exists", false).first;
7942

80-
auto header_result = GetBoolOption(options, "header", true);
43+
auto header_result = duckdb::sheets::GetBoolOption(options, "header", true);
8144
bool header = header_result.second ? header_result.first : (overwrite_range || overwrite_sheet);
8245

8346
if (create_if_not_exists && sheet.empty()) {

src/include/sheets/util/encoding.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,7 @@ std::string Base64UrlEncode(const unsigned char *data, size_t len);
1616

1717
std::string Base64UrlEncode(const std::string &input);
1818

19+
std::string NormalizePemKey(const std::string &key);
20+
1921
} // namespace sheets
2022
} // namespace duckdb

src/include/utils/options.hpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#pragma once
2+
3+
#include <string>
4+
#include <utility>
5+
6+
#include "duckdb/common/types/value.hpp"
7+
8+
namespace duckdb {
9+
namespace sheets {
10+
11+
std::string GetStringOption(const case_insensitive_map_t<vector<Value>> &options, const std::string &name,
12+
const std::string &default_value = "");
13+
14+
std::string GetStringOption(const case_insensitive_map_t<Value> &options, const std::string &name,
15+
const std::string &default_value = "");
16+
17+
std::pair<bool, bool> GetBoolOption(const case_insensitive_map_t<vector<Value>> &options, const std::string &name,
18+
bool default_value = false);
19+
20+
std::pair<bool, bool> GetBoolOption(const case_insensitive_map_t<Value> &options, const std::string &name,
21+
bool default_value = false);
22+
23+
} // namespace sheets
24+
} // namespace duckdb

src/sheets/auth/service_account_auth.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,10 @@ std::string ServiceAccountAuth::CreateJwt() {
6666
std::string claimsB64 = Base64UrlEncode(claimSet.dump());
6767
std::string signInput = headerB64 + "." + claimsB64;
6868

69+
auto pem = NormalizePemKey(privateKey);
70+
6971
// Parse PEM private key into EVP_PKEY (RAII handles cleanup)
70-
BIOPtr bio(BIO_new_mem_buf(privateKey.c_str(), -1));
72+
BIOPtr bio(BIO_new_mem_buf(pem.c_str(), -1));
7173
if (!bio) {
7274
throw duckdb::IOException("Failed to create BIO for private key");
7375
}

src/sheets/util/encoding.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,15 @@ std::string Base64UrlEncode(const std::string &input) {
4242
return Base64UrlEncode(reinterpret_cast<const unsigned char *>(input.c_str()), input.length());
4343
}
4444

45+
std::string NormalizePemKey(const std::string &key) {
46+
std::string pem = key;
47+
size_t pos = 0;
48+
while ((pos = pem.find("\\n", pos)) != std::string::npos) {
49+
pem.replace(pos, 2, "\n");
50+
pos += 1;
51+
}
52+
return pem;
53+
}
54+
4555
} // namespace sheets
4656
} // namespace duckdb

src/utils/options.cpp

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#include "duckdb/common/exception/binder_exception.hpp"
2+
#include "duckdb/common/types/value.hpp"
3+
4+
#include "utils/options.hpp"
5+
6+
std::string duckdb::sheets::GetStringOption(const case_insensitive_map_t<vector<Value>> &options,
7+
const std::string &name, const std::string &default_value) {
8+
const auto it = options.find(name);
9+
if (it == options.end()) {
10+
return default_value;
11+
}
12+
std::string err;
13+
Value val;
14+
if (!it->second.back().DefaultTryCastAs(LogicalType::VARCHAR, val, &err)) {
15+
throw BinderException(name + " option must be VARCHAR");
16+
}
17+
if (val.IsNull()) {
18+
throw BinderException(name + " option must not be NULL");
19+
}
20+
return StringValue::Get(val);
21+
}
22+
23+
std::string duckdb::sheets::GetStringOption(const case_insensitive_map_t<Value> &options, const std::string &name,
24+
const std::string &default_value) {
25+
const auto it = options.find(name);
26+
if (it == options.end()) {
27+
return default_value;
28+
}
29+
std::string err;
30+
Value val;
31+
if (!it->second.DefaultTryCastAs(LogicalType::VARCHAR, val, &err)) {
32+
throw BinderException(name + " option must be VARCHAR");
33+
}
34+
if (val.IsNull()) {
35+
throw BinderException(name + " option must not be NULL");
36+
}
37+
return StringValue::Get(val);
38+
}
39+
40+
std::pair<bool, bool> duckdb::sheets::GetBoolOption(const case_insensitive_map_t<vector<Value>> &options,
41+
const std::string &name, bool default_value) {
42+
const auto it = options.find(name);
43+
if (it == options.end()) {
44+
return std::make_pair(default_value, false);
45+
}
46+
if (it->second.size() != 1) {
47+
throw BinderException(name + " option must be a single boolean value");
48+
}
49+
std::string err;
50+
Value val;
51+
if (!it->second.back().DefaultTryCastAs(LogicalType::BOOLEAN, val, &err)) {
52+
throw BinderException(name + " option must be a single boolean value");
53+
}
54+
if (val.IsNull()) {
55+
throw BinderException(name + " option must be a single boolean value");
56+
}
57+
return std::make_pair(BooleanValue::Get(val), true);
58+
}
59+
60+
std::pair<bool, bool> duckdb::sheets::GetBoolOption(const case_insensitive_map_t<Value> &options,
61+
const std::string &name, bool default_value) {
62+
const auto it = options.find(name);
63+
if (it == options.end()) {
64+
return std::make_pair(default_value, false);
65+
}
66+
std::string err;
67+
Value val;
68+
if (!it->second.DefaultTryCastAs(LogicalType::BOOLEAN, val, &err)) {
69+
throw BinderException(name + " option must be a single boolean value");
70+
}
71+
if (val.IsNull()) {
72+
throw BinderException(name + " option must be a single boolean value");
73+
}
74+
return std::make_pair(BooleanValue::Get(val), true);
75+
}

test/unit/sheets/util/test_encoding.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,23 @@ TEST_CASE("Base64UrlEncode JWT header", "[encoding]") {
6767
std::string header = R"({"alg":"RS256","typ":"JWT"})";
6868
REQUIRE(duckdb::sheets::Base64UrlEncode(header) == "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9");
6969
}
70+
71+
TEST_CASE("NormalizePemKey Key with literal \\n sequences") {
72+
std::string input = "-----BEGIN PRIVATE KEY-----\\nMIIE...\\n-----END PRIVATE KEY-----\\n";
73+
std::string expected = "-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----\n";
74+
REQUIRE(duckdb::sheets::NormalizePemKey(input) == expected);
75+
}
76+
77+
TEST_CASE("NormalizePemKey Key already has real newlines") {
78+
std::string input = "-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----\n";
79+
REQUIRE(duckdb::sheets::NormalizePemKey(input) == input);
80+
}
81+
82+
TEST_CASE("NormalizePemKey empty string") {
83+
REQUIRE(duckdb::sheets::NormalizePemKey("") == "");
84+
}
85+
86+
TEST_CASE("NormalizePemKey no newlines at all") {
87+
std::string input = "just-a-string-with-no-newlines";
88+
REQUIRE(duckdb::sheets::NormalizePemKey(input) == input);
89+
}

0 commit comments

Comments
 (0)