2022-04-19 12:31:34 +02:00
|
|
|
# Copyright 2021 Collate
|
|
|
|
# Licensed 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.
|
|
|
|
"""
|
|
|
|
Credentials helper module
|
|
|
|
"""
|
2022-11-08 14:13:49 +05:30
|
|
|
import base64
|
2022-04-19 12:31:34 +02:00
|
|
|
import json
|
|
|
|
import os
|
|
|
|
import tempfile
|
2024-03-22 14:28:42 +05:30
|
|
|
from typing import Dict, List, Optional, Union
|
2022-08-19 11:19:20 +02:00
|
|
|
|
|
|
|
from cryptography.hazmat.primitives import serialization
|
2023-08-20 22:50:02 +09:00
|
|
|
from google import auth
|
|
|
|
from google.auth import impersonated_credentials
|
2022-04-19 12:31:34 +02:00
|
|
|
|
2023-06-06 11:57:00 +05:30
|
|
|
from metadata.generated.schema.security.credentials.gcpCredentials import (
|
|
|
|
GCPCredentials,
|
|
|
|
GcpCredentialsPath,
|
2023-03-03 19:10:01 +01:00
|
|
|
)
|
2024-03-22 14:28:42 +05:30
|
|
|
from metadata.generated.schema.security.credentials.gcpExternalAccount import (
|
|
|
|
GcpCredentialsValuesExternalAccount,
|
|
|
|
)
|
2023-06-06 11:57:00 +05:30
|
|
|
from metadata.generated.schema.security.credentials.gcpValues import (
|
|
|
|
GcpCredentialsValues,
|
2022-04-19 12:31:34 +02:00
|
|
|
)
|
2022-06-01 13:52:55 +05:30
|
|
|
from metadata.utils.logger import utils_logger
|
|
|
|
|
|
|
|
logger = utils_logger()
|
2022-04-19 12:31:34 +02:00
|
|
|
|
|
|
|
GOOGLE_CREDENTIALS = "GOOGLE_APPLICATION_CREDENTIALS"
|
|
|
|
|
2023-08-20 22:50:02 +09:00
|
|
|
GOOGLE_CLOUD_SCOPES = [
|
|
|
|
"https://www.googleapis.com/auth/cloud-platform",
|
|
|
|
"https://www.googleapis.com/auth/drive",
|
|
|
|
]
|
|
|
|
|
2022-04-19 12:31:34 +02:00
|
|
|
|
2023-06-06 11:57:00 +05:30
|
|
|
class InvalidGcpConfigException(Exception):
|
2022-04-19 12:31:34 +02:00
|
|
|
"""
|
2023-06-06 11:57:00 +05:30
|
|
|
Raised when we have errors trying to set GCP credentials
|
2022-04-19 12:31:34 +02:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
2022-08-19 11:19:20 +02:00
|
|
|
class InvalidPrivateKeyException(Exception):
|
|
|
|
"""
|
|
|
|
If the key cannot be serialised
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def validate_private_key(private_key: str) -> None:
|
|
|
|
"""
|
|
|
|
Make sure that a private key can be properly parsed
|
|
|
|
by cryptography backends
|
|
|
|
:param private_key: key to validate
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
serialization.load_pem_private_key(private_key.encode(), password=None)
|
|
|
|
except ValueError as err:
|
2022-08-29 06:46:06 +02:00
|
|
|
msg = f"Cannot serialise key: {err}"
|
2022-10-10 16:23:47 +05:30
|
|
|
raise InvalidPrivateKeyException(msg) from err
|
2022-08-19 11:19:20 +02:00
|
|
|
|
|
|
|
|
2022-04-19 12:31:34 +02:00
|
|
|
def create_credential_tmp_file(credentials: dict) -> str:
|
|
|
|
"""
|
|
|
|
Given a credentials' dict, store it in a tmp file
|
|
|
|
:param credentials: dictionary to store
|
|
|
|
:return: path to find the file
|
|
|
|
"""
|
2022-10-10 16:23:47 +05:30
|
|
|
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
|
2022-04-19 12:31:34 +02:00
|
|
|
cred_json = json.dumps(credentials, indent=4, separators=(",", ": "))
|
2022-10-10 16:23:47 +05:30
|
|
|
temp_file.write(cred_json.encode())
|
2023-07-27 23:24:55 +05:30
|
|
|
# Get the path of the temporary file
|
|
|
|
temp_file_path = temp_file.name
|
2022-04-19 12:31:34 +02:00
|
|
|
|
2023-07-27 23:24:55 +05:30
|
|
|
# The temporary file will be automatically closed when exiting the "with" block,
|
|
|
|
# but we can explicitly close it here to free up resources immediately.
|
|
|
|
temp_file.close()
|
|
|
|
|
|
|
|
# Return the path of the temporary file
|
|
|
|
return temp_file_path
|
2022-04-19 12:31:34 +02:00
|
|
|
|
|
|
|
|
2024-03-22 14:28:42 +05:30
|
|
|
def build_google_credentials_dict(
|
|
|
|
gcp_values: Union[GcpCredentialsValues, GcpCredentialsValuesExternalAccount]
|
|
|
|
) -> Dict[str, str]:
|
2022-08-19 11:19:20 +02:00
|
|
|
"""
|
2023-06-06 11:57:00 +05:30
|
|
|
Given GcPCredentialsValues, build a dictionary as the JSON file
|
|
|
|
downloaded from GCP with the service_account
|
|
|
|
:param gcp_values: GCP credentials
|
2022-08-19 11:19:20 +02:00
|
|
|
:return: Dictionary with credentials
|
|
|
|
"""
|
2024-03-16 23:29:02 +09:00
|
|
|
if gcp_values.type == "service_account":
|
|
|
|
private_key_str = gcp_values.privateKey.get_secret_value()
|
|
|
|
# adding the replace string here to escape line break if passed from env
|
|
|
|
private_key_str = private_key_str.replace("\\n", "\n")
|
|
|
|
validate_private_key(private_key_str)
|
|
|
|
|
|
|
|
return {
|
|
|
|
"type": gcp_values.type,
|
|
|
|
"project_id": gcp_values.projectId.__root__,
|
|
|
|
"private_key_id": gcp_values.privateKeyId,
|
|
|
|
"private_key": private_key_str,
|
|
|
|
"client_email": gcp_values.clientEmail,
|
|
|
|
"client_id": gcp_values.clientId,
|
|
|
|
"auth_uri": str(gcp_values.authUri),
|
|
|
|
"token_uri": str(gcp_values.tokenUri),
|
|
|
|
"auth_provider_x509_cert_url": str(gcp_values.authProviderX509CertUrl),
|
|
|
|
"client_x509_cert_url": str(gcp_values.clientX509CertUrl),
|
|
|
|
}
|
|
|
|
if gcp_values.type == "external_account":
|
|
|
|
return {
|
|
|
|
"type": gcp_values.type,
|
|
|
|
"audience": gcp_values.audience,
|
|
|
|
"subject_token_type": gcp_values.subjectTokenType,
|
|
|
|
"token_url": gcp_values.tokenURL,
|
|
|
|
"credential_source": gcp_values.credentialSource,
|
|
|
|
}
|
|
|
|
|
|
|
|
raise InvalidGcpConfigException(
|
|
|
|
f"Error not support credential type {gcp_values.type}"
|
|
|
|
)
|
2022-08-19 11:19:20 +02:00
|
|
|
|
|
|
|
|
2023-06-06 11:57:00 +05:30
|
|
|
def set_google_credentials(gcp_credentials: GCPCredentials) -> None:
|
2022-04-19 12:31:34 +02:00
|
|
|
"""
|
2023-06-06 11:57:00 +05:30
|
|
|
Set GCP credentials environment variable
|
|
|
|
:param gcp_credentials: GCPCredentials
|
2022-04-19 12:31:34 +02:00
|
|
|
"""
|
2023-06-06 11:57:00 +05:30
|
|
|
if isinstance(gcp_credentials.gcpConfig, GcpCredentialsPath):
|
|
|
|
os.environ[GOOGLE_CREDENTIALS] = str(gcp_credentials.gcpConfig.__root__)
|
2022-04-19 12:31:34 +02:00
|
|
|
return
|
2022-08-19 11:19:20 +02:00
|
|
|
|
2023-06-06 11:57:00 +05:30
|
|
|
if gcp_credentials.gcpConfig.projectId is None:
|
2022-06-01 13:52:55 +05:30
|
|
|
logger.info(
|
2022-08-19 11:19:20 +02:00
|
|
|
"No credentials available, using the current environment permissions authenticated via gcloud SDK."
|
2022-06-01 13:52:55 +05:30
|
|
|
)
|
|
|
|
return
|
2022-08-19 11:19:20 +02:00
|
|
|
|
2023-06-06 11:57:00 +05:30
|
|
|
if isinstance(gcp_credentials.gcpConfig, GcpCredentialsValues):
|
2022-09-20 17:36:10 +05:30
|
|
|
if (
|
2023-06-06 11:57:00 +05:30
|
|
|
gcp_credentials.gcpConfig.projectId
|
|
|
|
and not gcp_credentials.gcpConfig.privateKey
|
2022-09-20 17:36:10 +05:30
|
|
|
):
|
|
|
|
logger.info(
|
|
|
|
"Overriding default projectid, using the current environment permissions authenticated via gcloud SDK."
|
|
|
|
)
|
|
|
|
return
|
2023-07-27 23:24:55 +05:30
|
|
|
|
2023-06-06 11:57:00 +05:30
|
|
|
credentials_dict = build_google_credentials_dict(gcp_credentials.gcpConfig)
|
2022-04-19 12:31:34 +02:00
|
|
|
tmp_credentials_file = create_credential_tmp_file(credentials=credentials_dict)
|
|
|
|
os.environ[GOOGLE_CREDENTIALS] = tmp_credentials_file
|
|
|
|
return
|
|
|
|
|
2023-06-06 11:57:00 +05:30
|
|
|
raise InvalidGcpConfigException(
|
|
|
|
f"Error trying to set GCP credentials with {gcp_credentials}."
|
2022-12-02 20:12:06 +05:30
|
|
|
" Check https://docs.open-metadata.org/connectors/database/bigquery "
|
2022-04-19 12:31:34 +02:00
|
|
|
)
|
2022-11-08 14:13:49 +05:30
|
|
|
|
|
|
|
|
|
|
|
def generate_http_basic_token(username, password):
|
|
|
|
"""
|
|
|
|
Generates a HTTP basic token from username and password
|
|
|
|
Returns a token string (not a byte)
|
|
|
|
"""
|
|
|
|
token = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("utf-8")
|
|
|
|
return token
|
2023-08-20 22:50:02 +09:00
|
|
|
|
|
|
|
|
|
|
|
def get_gcp_default_credentials(
|
2023-08-21 14:52:15 +05:30
|
|
|
quota_project_id: Optional[str] = None,
|
|
|
|
scopes: Optional[List[str]] = None,
|
2023-08-20 22:50:02 +09:00
|
|
|
) -> auth.credentials.Credentials:
|
|
|
|
"""Get the default credentials
|
|
|
|
|
|
|
|
Args:
|
|
|
|
quota_project_id: quota project ID
|
|
|
|
scopes: Google Cloud sscopes
|
|
|
|
"""
|
2023-08-21 14:52:15 +05:30
|
|
|
scopes = scopes or GOOGLE_CLOUD_SCOPES
|
2023-08-20 22:50:02 +09:00
|
|
|
credentials, _ = auth.default(quota_project_id=quota_project_id, scopes=scopes)
|
|
|
|
return credentials
|
|
|
|
|
|
|
|
|
|
|
|
def get_gcp_impersonate_credentials(
|
|
|
|
impersonate_service_account: str,
|
|
|
|
quoted_project_id: Optional[str] = None,
|
|
|
|
scopes: Optional[List[str]] = None,
|
2023-08-21 14:52:15 +05:30
|
|
|
lifetime: Optional[int] = 3600,
|
2023-08-20 22:50:02 +09:00
|
|
|
) -> impersonated_credentials.Credentials:
|
|
|
|
"""Get the credentials to impersonate"""
|
2023-08-21 14:52:15 +05:30
|
|
|
scopes = scopes or GOOGLE_CLOUD_SCOPES
|
2023-08-20 22:50:02 +09:00
|
|
|
source_credentials, _ = auth.default()
|
2023-08-21 14:52:15 +05:30
|
|
|
if quoted_project_id:
|
2023-08-20 22:50:02 +09:00
|
|
|
source_credentials, quoted_project_id = auth.default(
|
|
|
|
quota_project_id=quoted_project_id
|
|
|
|
)
|
2023-08-21 14:52:15 +05:30
|
|
|
return impersonated_credentials.Credentials(
|
2023-08-20 22:50:02 +09:00
|
|
|
source_credentials=source_credentials,
|
|
|
|
target_principal=impersonate_service_account,
|
|
|
|
target_scopes=scopes,
|
|
|
|
lifetime=lifetime,
|
|
|
|
)
|