Fix #6787 - GCS credentials marked as required + validate privateKey (#6804)

Fix #6787 - GCS credentials marked as required + validate privateKey (#6804)
This commit is contained in:
Pere Miquel Brull 2022-08-19 11:19:20 +02:00 committed by GitHub
parent 6f3cd4cfed
commit ed0a01edea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 240 additions and 35 deletions

View File

@ -70,7 +70,16 @@
"format": "uri" "format": "uri"
} }
}, },
"additionalProperties": false "additionalProperties": false,
"required": [
"type",
"projectId",
"privateKeyId",
"privateKey",
"clientEmail",
"clientId",
"clientX509CertUrl"
]
}, },
"GCSCredentialsPath": { "GCSCredentialsPath": {
"title": "GCS Credentials Path", "title": "GCS Credentials Path",

View File

@ -202,6 +202,25 @@ def _unsafe_parse_config(config: dict, cls: T, message: str) -> None:
raise err raise err
def _parse_inner_connection(config_dict: dict, source_type: str) -> None:
"""
Parse the inner connection of the flagged connectors
:param config_dict: JSON configuration
:param source_type: source type name, e.g., Airflow.
"""
inner_source_type = config_dict["source"]["serviceConnection"]["config"][
"connection"
]["type"]
inner_service_type = get_service_type(inner_source_type)
inner_connection_class = get_connection_class(inner_source_type, inner_service_type)
_unsafe_parse_config(
config=config_dict["source"]["serviceConnection"]["config"]["connection"],
cls=inner_connection_class,
message=f"Error parsing the inner service connection for {source_type}",
)
def parse_service_connection(config_dict: dict) -> None: def parse_service_connection(config_dict: dict) -> None:
""" """
Parse the service connection and raise any scoped Parse the service connection and raise any scoped
@ -221,18 +240,7 @@ def parse_service_connection(config_dict: dict) -> None:
if source_type in HAS_INNER_CONNECTION: if source_type in HAS_INNER_CONNECTION:
# We will first parse the inner `connection` configuration # We will first parse the inner `connection` configuration
inner_source_type = config_dict["source"]["serviceConnection"]["config"][ _parse_inner_connection(config_dict, source_type)
"connection"
]["type"]
inner_service_type = get_service_type(inner_source_type)
inner_connection_class = get_connection_class(
inner_source_type, inner_service_type
)
_unsafe_parse_config(
config=config_dict["source"]["serviceConnection"]["config"]["connection"],
cls=inner_connection_class,
message=f"Error parsing the inner service connection for {source_type}",
)
# Parse the service connection dictionary with the scoped class # Parse the service connection dictionary with the scoped class
_unsafe_parse_config( _unsafe_parse_config(

View File

@ -14,6 +14,9 @@ Credentials helper module
import json import json
import os import os
import tempfile import tempfile
from typing import Dict
from cryptography.hazmat.primitives import serialization
from metadata.generated.schema.security.credentials.gcsCredentials import ( from metadata.generated.schema.security.credentials.gcsCredentials import (
GCSCredentials, GCSCredentials,
@ -33,6 +36,24 @@ class InvalidGcsConfigException(Exception):
""" """
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:
raise InvalidPrivateKeyException(f"Cannot serialise key - {err}")
def create_credential_tmp_file(credentials: dict) -> str: def create_credential_tmp_file(credentials: dict) -> str:
""" """
Given a credentials' dict, store it in a tmp file Given a credentials' dict, store it in a tmp file
@ -46,6 +67,31 @@ def create_credential_tmp_file(credentials: dict) -> str:
return fp.name return fp.name
def build_google_credentials_dict(gcs_values: GCSValues) -> Dict[str, str]:
"""
Given GCSValues, build a dictionary as the JSON file
downloaded from GCS with the service_account
:param gcs_values: GCS credentials
:return: Dictionary with credentials
"""
private_key_str = gcs_values.privateKey.get_secret_value()
validate_private_key(private_key_str)
return {
"type": gcs_values.type,
"project_id": gcs_values.projectId,
"private_key_id": gcs_values.privateKeyId,
"private_key": private_key_str,
"client_email": gcs_values.clientEmail,
"client_id": gcs_values.clientId,
"auth_uri": str(gcs_values.authUri),
"token_uri": str(gcs_values.tokenUri),
"auth_provider_x509_cert_url": str(gcs_values.authProviderX509CertUrl),
"client_x509_cert_url": str(gcs_values.clientX509CertUrl),
}
def set_google_credentials(gcs_credentials: GCSCredentials) -> None: def set_google_credentials(gcs_credentials: GCSCredentials) -> None:
""" """
Set GCS credentials environment variable Set GCS credentials environment variable
@ -57,26 +103,15 @@ def set_google_credentials(gcs_credentials: GCSCredentials) -> None:
if isinstance(gcs_credentials.gcsConfig, GCSCredentialsPath): if isinstance(gcs_credentials.gcsConfig, GCSCredentialsPath):
os.environ[GOOGLE_CREDENTIALS] = str(gcs_credentials.gcsConfig.__root__) os.environ[GOOGLE_CREDENTIALS] = str(gcs_credentials.gcsConfig.__root__)
return return
if gcs_credentials.gcsConfig.projectId is None: if gcs_credentials.gcsConfig.projectId is None:
logger.info( logger.info(
"No credentials available, using the current environment permissions authenticated via gcloud SDK ." "No credentials available, using the current environment permissions authenticated via gcloud SDK."
) )
return return
if isinstance(gcs_credentials.gcsConfig, GCSValues): if isinstance(gcs_credentials.gcsConfig, GCSValues):
credentials_dict = { credentials_dict = build_google_credentials_dict(gcs_credentials.gcsConfig)
"type": gcs_credentials.gcsConfig.type,
"project_id": gcs_credentials.gcsConfig.projectId,
"private_key_id": gcs_credentials.gcsConfig.privateKeyId,
"private_key": gcs_credentials.gcsConfig.privateKey.get_secret_value(),
"client_email": gcs_credentials.gcsConfig.clientEmail,
"client_id": gcs_credentials.gcsConfig.clientId,
"auth_uri": str(gcs_credentials.gcsConfig.authUri),
"token_uri": str(gcs_credentials.gcsConfig.tokenUri),
"auth_provider_x509_cert_url": str(
gcs_credentials.gcsConfig.authProviderX509CertUrl
),
"client_x509_cert_url": str(gcs_credentials.gcsConfig.clientX509CertUrl),
}
tmp_credentials_file = create_credential_tmp_file(credentials=credentials_dict) tmp_credentials_file = create_credential_tmp_file(credentials=credentials_dict)
os.environ[GOOGLE_CREDENTIALS] = tmp_credentials_file os.environ[GOOGLE_CREDENTIALS] = tmp_credentials_file

View File

@ -0,0 +1,82 @@
# 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.
"""
Test Credentials helper module
"""
from unittest import TestCase
from pydantic import SecretStr
from metadata.generated.schema.security.credentials.gcsCredentials import GCSValues
from metadata.utils.credentials import (
InvalidPrivateKeyException,
build_google_credentials_dict,
)
class TestCredentials(TestCase):
"""
Validate credentials handling
"""
def test_build_google_credentials_dict(self):
"""
Check how we can validate GCS values
"""
# Key mocked online
private_key = """-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDMGwM93kIt3D4r4+dWAGdoTboSaZcFLhsG1lvnZlYEpnZoFo1M
ek7laRKDUW3CkdTlSid9p4/RTs9SYKuuXvNKNSLApHUeR2zgKBIHYTGGv1t1bEWc
ohVeqr7w8HkFr9LV4qxgFEWBBd3QYncY/Y1iZgTtbmMiUxJN9vj/kuH0xQIDAQAB
AoGAPDqAY2JRrwy9v9/ZpPQrj4jYLpS//sRTL1pT9l2pZmfkquR0v6ub2nB+CQgf
VnoIE70lGBw5AS+7V/i00JiuO6GP/MWWqxKdc5McjBGYDIb+9gQ/DrryVDHsqgGX
iZrWr7rIrpGsbCB2xt2HPpKR7D9IpI8FA+EEU9fIPfETM6ECQQDv69L78zdijSNk
CYx70dVHqCiDZT5RbkJqDmQwKabIGXBqZLTM+7ZAHotq0EXGc5BvQGyIMso/qIOs
Wq3imi3dAkEA2ci4xEzj5guQcGxoVcxfGm+M/VqXLuw/eW1sYdOp52OwdDywxG+I
6tpm5ByVowhqT8PHDJVOy8GEV9QNw0Y4CQJBAJiyn/rJJlPr/j1aMnZP642KwhY2
pr4PDegQNsXMjKDISBr+82+POMSAbD1UR0RyItgbybe5k62GZB+bKxaRCGUCQEVj
l8MrwH0eeCHp2IBlwnN40VIz1/GiYkL9I0g0GXFZKPKQF74uz1AM0DWkCeVNHBpY
BYaz18xB1znonY33RIkCQQDE3wAWxFrvr582J12qJkE4enmNhRJFdcSREDX54d/5
VEhPQF0i0tUU7Fl071hcYaiQoZx4nIjN+NG6p5QKbl6k
-----END RSA PRIVATE KEY-----"""
gcs_values = GCSValues(
type="my_type",
projectId="project_id",
privateKeyId="private_key_id",
privateKey=private_key,
clientEmail="email@mail.com",
clientId="client_id",
clientX509CertUrl="http://localhost:1234",
)
expected_dict = {
"type": "my_type",
"project_id": "project_id",
"private_key_id": "private_key_id",
"private_key": private_key,
"client_email": "email@mail.com",
"client_id": "client_id",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "http://localhost:1234",
}
build_google_credentials_dict(gcs_values)
self.assertEqual(expected_dict, build_google_credentials_dict(gcs_values))
gcs_values.privateKey = SecretStr("I don't think I am a proper Private Key")
with self.assertRaises(InvalidPrivateKeyException):
build_google_credentials_dict(gcs_values)

View File

@ -10,6 +10,8 @@ This page list all the supported helm values for OpenMetadata Helm Charts.
## Global Chart Values ## Global Chart Values
<Table>
| Key | Type | Default | | Key | Type | Default |
| :---------- | :---------- | :---------- | | :---------- | :---------- | :---------- |
| global.authentication.provider | string | `no-auth` | | global.authentication.provider | string | `no-auth` |
@ -75,6 +77,7 @@ This page list all the supported helm values for OpenMetadata Helm Charts.
| global.elasticsearch.trustStore.password.secretRef | string | `elasticsearch-truststore-secrets` | | global.elasticsearch.trustStore.password.secretRef | string | `elasticsearch-truststore-secrets` |
| global.elasticsearch.trustStore.password.secretKey | string | `openmetadata-elasticsearch-truststore-password` | | global.elasticsearch.trustStore.password.secretKey | string | `openmetadata-elasticsearch-truststore-password` |
| global.jwtTokenConfiguration.enabled | bool | `false` | | global.jwtTokenConfiguration.enabled | bool | `false` |
| global.fernetKey | string | `jJ/9sz0g0OHxsfxOoSfdFdmk3ysNmPRnH3TUAbz3IHA=` |
| global.jwtTokenConfiguration.rsapublicKeyFilePath | string | `Empty String` | | global.jwtTokenConfiguration.rsapublicKeyFilePath | string | `Empty String` |
| global.jwtTokenConfiguration.rsaprivateKeyFilePath | string | `Empty String` | | global.jwtTokenConfiguration.rsaprivateKeyFilePath | string | `Empty String` |
| global.jwtTokenConfiguration.jwtissuer | string | `open-metadata.org` | | global.jwtTokenConfiguration.jwtissuer | string | `open-metadata.org` |
@ -84,9 +87,11 @@ This page list all the supported helm values for OpenMetadata Helm Charts.
| global.openmetadata.host | string | `openmetadata` | | global.openmetadata.host | string | `openmetadata` |
| global.openmetadata.port | int | 8585 | | global.openmetadata.port | int | 8585 |
</Table>
## Chart Values ## Chart Values
<Table>
| Key | Type | Default | | Key | Type | Default |
| :---------- | :---------- | :---------- | | :---------- | :---------- | :---------- |
@ -125,3 +130,5 @@ This page list all the supported helm values for OpenMetadata Helm Charts.
| serviceAccount.name | string | `nil` | | serviceAccount.name | string | `nil` |
| sidecars | list | `[]` | | sidecars | list | `[]` |
| tolerations | list | `[]` | | tolerations | list | `[]` |
</Table>

View File

@ -14,6 +14,10 @@ Recovery practices.
While there are cloud services that feature automatic snapshots and replication, the metadata CLI While there are cloud services that feature automatic snapshots and replication, the metadata CLI
now allows all users to perform backups regardless of the underlying infrastructure. now allows all users to perform backups regardless of the underlying infrastructure.
## Requirements
The backup CLI needs to be used with `openmetadata-ingestion` version 0.12 or higher.
## Installation ## Installation
The CLI comes bundled in the base `openmetadata-ingestion` Python package. You can install it with: The CLI comes bundled in the base `openmetadata-ingestion` Python package. You can install it with:

View File

@ -11,6 +11,8 @@ slug: /openmetadata/connectors/database/bigquery
<p> To execute metadata extraction and usage workflow successfully the user or the service account should have enough access to fetch required data. Following table describes the minimum required permissions </p> <p> To execute metadata extraction and usage workflow successfully the user or the service account should have enough access to fetch required data. Following table describes the minimum required permissions </p>
<Table>
| # | GCP Permission | GCP Role | Required For | | # | GCP Permission | GCP Role | Required For |
| :---------- | :---------- | :---------- | :---------- | | :---------- | :---------- | :---------- | :---------- |
| 1 | bigquery.datasets.get | BigQuery Data Viewer | Metadata Ingestion | | 1 | bigquery.datasets.get | BigQuery Data Viewer | Metadata Ingestion |
@ -25,6 +27,8 @@ slug: /openmetadata/connectors/database/bigquery
| 10 | bigquery.readsessions.create | BigQuery Admin | Bigquery Usage Workflow | | 10 | bigquery.readsessions.create | BigQuery Admin | Bigquery Usage Workflow |
| 11 | bigquery.readsessions.getData | BigQuery Admin | Bigquery Usage Workflow | | 11 | bigquery.readsessions.getData | BigQuery Admin | Bigquery Usage Workflow |
</Table>
<MetadataIngestionService connector="BigQuery"/> <MetadataIngestionService connector="BigQuery"/>
<h4>Connection Options</h4> <h4>Connection Options</h4>

View File

@ -25,7 +25,27 @@ const mockSecurityConfigS3 = {
}; };
const mockSecurityConfigGCSValue = { const mockSecurityConfigGCSValue = {
gcsConfig: {}, gcsConfig: {
authProviderX509CertUrl: 'url',
authUri: 'uri',
clientEmail: 'email',
clientId: 'id',
clientX509CertUrl: 'certUrl',
privateKey: 'privateKey',
privateKeyId: 'keyId',
projectId: 'projectId',
tokenUri: 'tokenUri',
type: 'type',
},
}; };
const mockPrefixConfig = { const mockPrefixConfig = {

View File

@ -21,6 +21,28 @@ const mockSubmit = jest.fn();
const mockPrefixConfigChange = jest.fn(); const mockPrefixConfigChange = jest.fn();
const mockSecurityConfigChange = jest.fn(); const mockSecurityConfigChange = jest.fn();
const gsConfig = {
authProviderX509CertUrl: 'url',
authUri: 'uri',
clientEmail: 'email',
clientId: 'id',
clientX509CertUrl: 'certUrl',
privateKey: 'privateKey',
privateKeyId: 'keyId',
projectId: 'projectId',
tokenUri: 'tokenUri',
type: 'type',
};
const mockProps = { const mockProps = {
okText: 'Next', okText: 'Next',
cancelText: 'Back', cancelText: 'Back',
@ -86,6 +108,7 @@ describe('Test DBT GCS Config Form', () => {
{...mockProps} {...mockProps}
dbtSecurityConfig={{ dbtSecurityConfig={{
gcsConfig: { gcsConfig: {
...gsConfig,
type: 'CredsType', type: 'CredsType',
}, },
}} }}
@ -102,6 +125,7 @@ describe('Test DBT GCS Config Form', () => {
{...mockProps} {...mockProps}
dbtSecurityConfig={{ dbtSecurityConfig={{
gcsConfig: { gcsConfig: {
...gsConfig,
projectId: 'ProjectId', projectId: 'ProjectId',
}, },
}} }}
@ -118,6 +142,7 @@ describe('Test DBT GCS Config Form', () => {
{...mockProps} {...mockProps}
dbtSecurityConfig={{ dbtSecurityConfig={{
gcsConfig: { gcsConfig: {
...gsConfig,
privateKeyId: 'PrivateKeyId', privateKeyId: 'PrivateKeyId',
}, },
}} }}
@ -134,6 +159,7 @@ describe('Test DBT GCS Config Form', () => {
{...mockProps} {...mockProps}
dbtSecurityConfig={{ dbtSecurityConfig={{
gcsConfig: { gcsConfig: {
...gsConfig,
privateKey: 'PrivateKey', privateKey: 'PrivateKey',
}, },
}} }}
@ -148,7 +174,9 @@ describe('Test DBT GCS Config Form', () => {
const { container } = render( const { container } = render(
<DBTGCSConfig <DBTGCSConfig
{...mockProps} {...mockProps}
dbtSecurityConfig={{ gcsConfig: { clientEmail: 'ClientEmail' } }} dbtSecurityConfig={{
gcsConfig: { ...gsConfig, clientEmail: 'ClientEmail' },
}}
/> />
); );
const inputClientEmail = getByTestId(container, 'client-email'); const inputClientEmail = getByTestId(container, 'client-email');
@ -160,7 +188,7 @@ describe('Test DBT GCS Config Form', () => {
const { container } = render( const { container } = render(
<DBTGCSConfig <DBTGCSConfig
{...mockProps} {...mockProps}
dbtSecurityConfig={{ gcsConfig: { clientId: 'ClientId' } }} dbtSecurityConfig={{ gcsConfig: { ...gsConfig, clientId: 'ClientId' } }}
/> />
); );
const inputClientId = getByTestId(container, 'client-id'); const inputClientId = getByTestId(container, 'client-id');
@ -172,7 +200,9 @@ describe('Test DBT GCS Config Form', () => {
const { container } = render( const { container } = render(
<DBTGCSConfig <DBTGCSConfig
{...mockProps} {...mockProps}
dbtSecurityConfig={{ gcsConfig: { authUri: 'http://www.AuthUri.com' } }} dbtSecurityConfig={{
gcsConfig: { ...gsConfig, authUri: 'http://www.AuthUri.com' },
}}
/> />
); );
const inputAuthUri = getByTestId(container, 'auth-uri'); const inputAuthUri = getByTestId(container, 'auth-uri');
@ -185,7 +215,7 @@ describe('Test DBT GCS Config Form', () => {
<DBTGCSConfig <DBTGCSConfig
{...mockProps} {...mockProps}
dbtSecurityConfig={{ dbtSecurityConfig={{
gcsConfig: { tokenUri: 'http://www.TokenUri.com' }, gcsConfig: { ...gsConfig, tokenUri: 'http://www.TokenUri.com' },
}} }}
/> />
); );
@ -199,7 +229,10 @@ describe('Test DBT GCS Config Form', () => {
<DBTGCSConfig <DBTGCSConfig
{...mockProps} {...mockProps}
dbtSecurityConfig={{ dbtSecurityConfig={{
gcsConfig: { authProviderX509CertUrl: 'http://www.AuthCertUri.com' }, gcsConfig: {
...gsConfig,
authProviderX509CertUrl: 'http://www.AuthCertUri.com',
},
}} }}
/> />
); );
@ -216,7 +249,10 @@ describe('Test DBT GCS Config Form', () => {
<DBTGCSConfig <DBTGCSConfig
{...mockProps} {...mockProps}
dbtSecurityConfig={{ dbtSecurityConfig={{
gcsConfig: { clientX509CertUrl: 'http://www.ClientCertUri.com' }, gcsConfig: {
...gsConfig,
clientX509CertUrl: 'http://www.ClientCertUri.com',
},
}} }}
/> />
); );