Coverage Report

Created: 2026-10-12 19:36

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
common/cpp/obj-client/auth/gcp/gcp_auth.cpp
Line
Count
Source
1
// Licensed to the Apache Software Foundation (ASF) under one
2
// or more contributor license agreements.  See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership.  The ASF licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License.  You may obtain a copy of the License at
8
//
9
//   http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied.  See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
#include "cpp/obj-client/auth/gcp/gcp_auth.h"
19
20
#include <algorithm>
21
#include <cctype>
22
#include <regex>
23
#include <utility>
24
25
namespace doris {
26
27
28
bool is_valid_gcp_storage_endpoint(std::string_view endpoint) {
28
    // Only service-base XML API endpoints: the S3 clients add the bucket themselves.
29
28
    static const std::regex pattern(
30
28
            R"(https://(storage\.googleapis\.com|[a-z0-9-]+-storage\.googleapis\.com|storage\.[a-z0-9-]+\.rep\.googleapis\.com)(:443)?/?)",
31
28
            std::regex::icase);
32
28
    return std::regex_match(endpoint.begin(), endpoint.end(), pattern);
33
28
}
34
35
42
bool is_valid_gcp_service_account_email(std::string_view email) {
36
42
    constexpr std::string_view user_managed_suffix = ".iam.gserviceaccount.com";
37
42
    constexpr std::string_view compute_engine_domain = "developer.gserviceaccount.com";
38
42
    constexpr std::string_view app_engine_domain = "appspot.gserviceaccount.com";
39
42
    auto at = email.find('@');
40
42
    if (at == std::string_view::npos || at == 0 ||
41
42
        email.find('@', at + 1) != std::string_view::npos ||
42
1.40k
        std::any_of(email.begin(), email.end(), [](unsigned char c) { return std::isspace(c); })) {
43
7
        return false;
44
7
    }
45
46
35
    auto domain = email.substr(at + 1);
47
35
    return domain == compute_engine_domain || domain == app_engine_domain ||
48
35
           (domain.size() > user_managed_suffix.size() && domain.ends_with(user_managed_suffix));
49
42
}
50
51
std::optional<GcpCredentialProviderType> parse_gcp_credential_provider_type(
52
17
        std::string_view provider_type) {
53
17
    std::string normalized(provider_type);
54
17
    normalized.erase(normalized.begin(),
55
17
                     std::find_if(normalized.begin(), normalized.end(),
56
17
                                  [](unsigned char c) { return !std::isspace(c); }));
57
17
    normalized.erase(std::find_if(normalized.rbegin(), normalized.rend(),
58
17
                                  [](unsigned char c) { return !std::isspace(c); })
59
17
                             .base(),
60
17
                     normalized.end());
61
17
    std::transform(normalized.begin(), normalized.end(), normalized.begin(),
62
129
                   [](unsigned char c) { return static_cast<char>(std::toupper(c)); });
63
17
    if (normalized == "DEFAULT") {
64
11
        return GcpCredentialProviderType::Default;
65
11
    }
66
6
    if (normalized == "COMPUTE_ENGINE") {
67
3
        return GcpCredentialProviderType::ComputeEngine;
68
3
    }
69
3
    return std::nullopt;
70
6
}
71
72
31
std::optional<std::string> validate_gcp_credential(const GcpCredentialConfig& credential) {
73
31
    if (credential.impersonation_service_account.empty()) {
74
22
        return std::nullopt;
75
22
    }
76
9
    if (!is_valid_gcp_service_account_email(credential.impersonation_service_account)) {
77
2
        return "Invalid GCP service account email";
78
2
    }
79
7
    return std::nullopt;
80
9
}
81
82
GcpCredentialParseResult parse_gcp_credential_properties(
83
38
        const ObjCredentialPropertyGetter& get_property) {
84
38
    auto provider_type_value = get_property(GCP_CREDENTIAL_PROVIDER_TYPE);
85
38
    auto impersonation_service_account = get_property(GCP_IMPERSONATION_SERVICE_ACCOUNT);
86
38
    if (!provider_type_value.has_value() && !impersonation_service_account.has_value()) {
87
17
        return {};
88
17
    }
89
21
    auto provider_type = provider_type_value.has_value()
90
21
                                 ? parse_gcp_credential_provider_type(*provider_type_value)
91
21
                                 : GcpCredentialProviderType::Default;
92
21
    if (!provider_type.has_value()) {
93
3
        return {.error = "invalid " + std::string(GCP_CREDENTIAL_PROVIDER_TYPE) + " value \"" +
94
3
                         *provider_type_value +
95
3
                         "\"; supported values are DEFAULT and COMPUTE_ENGINE"};
96
3
    }
97
98
18
    GcpCredentialConfig config {.provider_type = *provider_type};
99
18
    if (impersonation_service_account.has_value()) {
100
6
        config.impersonation_service_account = std::move(*impersonation_service_account);
101
6
    }
102
18
    if (auto error = validate_gcp_credential(config); error.has_value()) {
103
2
        return {.error = std::move(error)};
104
2
    }
105
16
    return {.credential = std::move(config)};
106
18
}
107
108
} // namespace doris