Skip to content

Commit

Permalink
Allow PKC#12 certificate content and password handling
Browse files Browse the repository at this point in the history
  • Loading branch information
alexis.flipo committed Dec 1, 2023
1 parent 77e37b0 commit e77b921
Show file tree
Hide file tree
Showing 4 changed files with 29 additions and 3 deletions.
17 changes: 15 additions & 2 deletions dbtmetabase/metabase.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
NullValue,
)

from requests_pkcs12 import Pkcs12Adapter


class MetabaseClient:
"""Metabase API client."""
Expand Down Expand Up @@ -122,13 +124,14 @@ def __init__(
password: Optional[str],
verify: Optional[Union[str, bool]] = None,
cert: Optional[Union[str, Tuple[str, str]]] = None,
pkcs12_data: Optional[Tuple[str, str]] = None,
session_id: Optional[str] = None,
use_http: bool = False,
sync: Optional[bool] = True,
sync_timeout: Optional[int] = None,
exclude_sources: bool = False,
http_extra_headers: Optional[dict] = None,
http_timeout: int = 15,
http_timeout: Optional[int] = 15,
):
"""Constructor.
Expand All @@ -141,20 +144,31 @@ def __init__(
use_http {bool} -- Use HTTP instead of HTTPS. (default: {False})
verify {Union[str, bool]} -- Path to certificate or disable verification. (default: {None})
cert {Union[str, Tuple[str, str]]} -- Path to a custom certificate to be used by the Metabase client. (default: {None})
pkcs12_data (Optional[Tuple[str, str]], optional): PKCS#12 Certificate content with its password. If the certificate is not physically present on the running host. (default: {None})
session_id {str} -- Metabase session ID. (default: {None})
sync (bool, optional): Attempt to synchronize Metabase schema with local models. Defaults to True.
sync_timeout (Optional[int], optional): Synchronization timeout (in secs). Defaults to None.
http_extra_headers {dict} -- HTTP headers to be used by the Metabase client. (default: {None})
exclude_sources {bool} -- Exclude exporting sources. (default: {False})
"""

self.base_url = f"{'http' if use_http else 'https'}://{host}"
self.session = requests.Session()
self.session.verify = verify
self.session.cert = cert

if http_extra_headers is not None:
self.session.headers.update(http_extra_headers)

adaptor = HTTPAdapter(max_retries=Retry(total=3, backoff_factor=0.5))

if pkcs12_data is not None:
adaptor = Pkcs12Adapter(
pkcs12_data=pkcs12_data[0],
pkcs12_password=pkcs12_data[1],
)
self.session.mount(self.base_url, adaptor)
self.http_timeout = http_timeout
session_header = session_id or self.get_session_id(user, password)
self.session.headers["X-Metabase-Session"] = session_header

Expand All @@ -166,7 +180,6 @@ def __init__(
self.table_map: MutableMapping = {}
self.models_exposed: List = []
self.native_query: str = ""
self.http_timeout = http_timeout
# This regex is looking for from and join clauses, and extracting the table part.
# It won't recognize some valid sql table references, such as `from "table with spaces"`.
self.exposure_parser = re.compile(r"[FfJj][RrOo][OoIi][MmNn]\s+([\w.\"]+)")
Expand Down
4 changes: 4 additions & 0 deletions dbtmetabase/models/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ class MetabaseUnableToSync(Exception):

class MetabaseRuntimeError(Exception):
"""Thrown when Metabase execution failed."""


class MetabaseCertificateImplementationError(Exception):
"""Thrown when cert argument and cert_data argument are both defined"""
10 changes: 9 additions & 1 deletion dbtmetabase/models/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
NoDbtPathSupplied,
NoDbtSchemaSupplied,
NoMetabaseCredentialsSupplied,
MetabaseCertificateImplementationError,
)
from ..parsers.dbt import DbtReader
from ..parsers.dbt_folder import DbtFolderReader
Expand All @@ -29,6 +30,7 @@ def __init__(
use_http: bool = False,
verify: Optional[Union[str, bool]] = True,
cert: Optional[Union[str, Tuple[str, str]]] = None,
pkcs12_data: Optional[Union[str, Tuple[str, str]]] = None,
sync: bool = True,
sync_timeout: Optional[int] = None,
exclude_sources: bool = False,
Expand All @@ -46,6 +48,7 @@ def __init__(
use_http (bool, optional): Use HTTP to connect to Metabase.. Defaults to False.
verify (Optional[Union[str, bool]], optional): Path to custom certificate bundle to be used by Metabase client. Defaults to True.
cert (Optional[Union[str, Tuple[str, str]]], optional): Path to a custom certificate to be used by the Metabase client, or a tuple containing the path to the certificate and key. Defaults to None.
pkcs12_data (Optional[Tuple[str, str]], optional): PKCS#12 Certificate content with its password. If the certificate is not physically present on the running host. (default: {None})
sync (bool, optional): Attempt to synchronize Metabase schema with local models. Defaults to True.
sync_timeout (Optional[int], optional): Synchronization timeout (in secs). Defaults to None.
exclude_sources (bool, optional): Exclude exporting sources. Defaults to False.
Expand All @@ -62,6 +65,7 @@ def __init__(
self.use_http = use_http
self.verify = verify
self.cert = cert
self.pkcs12_data = pkcs12_data
self.http_extra_headers = dict(http_extra_headers) if http_extra_headers else {}
self.http_timeout = http_timeout
# Metabase Sync
Expand Down Expand Up @@ -98,6 +102,10 @@ def prepare_metabase_client(self, dbt_models: Optional[List[MetabaseModel]] = No
raise NoMetabaseCredentialsSupplied(
"Credentials or session ID not supplied"
)
if self.cert and self.pkcs12_data:
raise MetabaseCertificateImplementationError(
"cert and pkcs12_data arguments can't be both defined"
)

self._client = MetabaseClient(
host=self.host,
Expand All @@ -106,14 +114,14 @@ def prepare_metabase_client(self, dbt_models: Optional[List[MetabaseModel]] = No
use_http=self.use_http,
verify=self.verify,
cert=self.cert,
pkcs12_data=self.pkcs12_data,
http_extra_headers=self.http_extra_headers,
session_id=self.session_id,
sync=self.sync,
sync_timeout=self.sync_timeout,
exclude_sources=self.exclude_sources,
http_timeout=self.http_timeout,
)

# Sync and attempt schema alignment prior to execution; if timeout is not explicitly set, proceed regardless of success
if self.sync:
self._client.sync_and_wait(self.database, dbt_models)
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ PyYAML>=5.4.1
requests>=2.26.0
click>=8.0.0
rich>=12.0.0
requests-pkcs12==1.22

0 comments on commit e77b921

Please sign in to comment.