Skip to content

Commit 293a318

Browse files
feat: M2M (client id+secret) auth for Databricks
JIRA: LX-617 risk: low
1 parent 4879db7 commit 293a318

File tree

5 files changed

+58
-3
lines changed

5 files changed

+58
-3
lines changed

docs/content/en/latest/data/data-source/_index.md

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ CatalogDataSourceMsSql(
183183

184184
### Databricks
185185

186+
Using Machine-to-Machine (M2M) authentication (client_id + client_secret):
186187
```python
187188
CatalogDataSourceDatabricks(
188189
id=data_source_id,
@@ -193,9 +194,26 @@ CatalogDataSourceDatabricks(
193194
),
194195
schema=xyz,
195196
parameters=[{"name":"catalog", "value": os.environ["DATABRICKS_CATALOG"]}],
196-
credentials=BasicCredentials(
197-
username=os.environ["DATABRICKS_USER"],
198-
password=os.environ["DATABRICKS_PASSWORD"],
197+
credentials=ClientSecretCredentials(
198+
client_id=os.environ["DATABRICKS_CLIENT_ID"],
199+
client_secret=os.environ["DATABRICKS_CLIENT_SECRET"],
200+
),
201+
)
202+
```
203+
204+
Using personal access token authentication:
205+
```python
206+
CatalogDataSourceDatabricks(
207+
id=data_source_id,
208+
name=data_source_name,
209+
db_specific_attributes=DatabricksAttributes(
210+
host=os.environ["DATABRICKS_HOST"],
211+
http_path=os.environ["DATABRICKS_HTTP_PATH"]
212+
),
213+
schema=xyz,
214+
parameters=[{"name":"catalog", "value": os.environ["DATABRICKS_CATALOG"]}],
215+
credentials=TokenCredentials(
216+
token=os.environ["DATABRICKS_PERSONAL_ACCESS_TOKEN"]
199217
),
200218
)
201219
```

gooddata-sdk/gooddata_sdk/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
from gooddata_sdk.catalog.entity import (
5656
AttrCatalogEntity,
5757
BasicCredentials,
58+
ClientSecretCredentials,
5859
KeyPairCredentials,
5960
TokenCredentialsFromEnvVar,
6061
TokenCredentialsFromFile,

gooddata-sdk/gooddata_sdk/catalog/data_source/declarative_model/data_source.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ def to_test_request(
111111
token: Optional[str] = None,
112112
private_key: Optional[str] = None,
113113
private_key_passphrase: Optional[str] = None,
114+
client_id: Optional[str] = None,
115+
client_secret: Optional[str] = None,
114116
) -> TestDefinitionRequest:
115117
kwargs: dict[str, Any] = {"schema": self.schema}
116118
if password is not None:
@@ -123,6 +125,10 @@ def to_test_request(
123125
kwargs["private_key"] = private_key
124126
if private_key_passphrase is not None:
125127
kwargs["private_key_passphrase"] = private_key
128+
if client_id is not None:
129+
kwargs["client_id"] = client_id
130+
if client_secret is not None:
131+
kwargs["client_secret"] = client_secret
126132
return TestDefinitionRequest(type=self.type, url=self.url, **kwargs)
127133

128134
@staticmethod
@@ -141,6 +147,8 @@ def to_api(
141147
token: Optional[str] = None,
142148
private_key: Optional[str] = None,
143149
private_key_passphrase: Optional[str] = None,
150+
client_id: Optional[str] = None,
151+
client_secret: Optional[str] = None,
144152
) -> DeclarativeDataSource:
145153
dictionary = self._get_snake_dict()
146154
if password is not None:
@@ -151,6 +159,10 @@ def to_api(
151159
dictionary["private_key"] = private_key
152160
if private_key_passphrase is not None:
153161
dictionary["private_key_passphrase"] = private_key_passphrase
162+
if client_id is not None:
163+
dictionary["client_id"] = client_id
164+
if client_secret is not None:
165+
dictionary["client_secret"] = client_secret
154166
return self.client_class().from_dict(dictionary)
155167

156168
def store_to_disk(self, data_sources_folder: Path) -> None:

gooddata-sdk/gooddata_sdk/catalog/data_source/entity_model/data_source.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from gooddata_sdk.catalog.base import Base, value_in_allowed
1717
from gooddata_sdk.catalog.entity import (
1818
BasicCredentials,
19+
ClientSecretCredentials,
1920
Credentials,
2021
KeyPairCredentials,
2122
TokenCredentials,
@@ -34,6 +35,7 @@ def db_attrs_with_template(instance: CatalogDataSource, *args: Any) -> None:
3435
class CatalogDataSourceBase(Base):
3536
_SUPPORTED_CREDENTIALS: ClassVar[list[type[Credentials]]] = [
3637
BasicCredentials,
38+
ClientSecretCredentials,
3739
TokenCredentials,
3840
TokenCredentialsFromFile,
3941
KeyPairCredentials,

gooddata-sdk/gooddata_sdk/catalog/entity.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ class Credentials(Base):
121121
PASSWORD_KEY: ClassVar[str] = "password"
122122
PRIVATE_KEY: ClassVar[str] = "private_key"
123123
PRIVATE_KEY_PASSPHRASE: ClassVar[str] = "private_key_passphrase"
124+
CLIENT_ID: ClassVar[str] = "client_id"
125+
CLIENT_SECRET: ClassVar[str] = "client_secret"
124126

125127
def to_api_args(self) -> dict[str, Any]:
126128
return attr.asdict(self)
@@ -252,3 +254,23 @@ def from_api(cls, attributes: dict[str, Any]) -> KeyPairCredentials:
252254
# You have to fill it to keep it or update it
253255
private_key="",
254256
)
257+
258+
259+
@attr.s(auto_attribs=True, kw_only=True)
260+
class ClientSecretCredentials(Credentials):
261+
client_id: str
262+
client_secret: str = attr.field(repr=lambda value: "***")
263+
264+
@classmethod
265+
def is_part_of_api(cls, entity: dict[str, Any]) -> bool:
266+
return cls.CLIENT_ID in entity and cls.CLIENT_SECRET in entity
267+
268+
@classmethod
269+
def from_api(cls, attributes: dict[str, Any]) -> ClientSecretCredentials:
270+
# Credentials are not returned for security reasons
271+
return cls(
272+
client_id=attributes[cls.CLIENT_ID],
273+
# Client secret is not returned from API (security)
274+
# You have to fill it to keep it or update it
275+
client_secret="",
276+
)

0 commit comments

Comments
 (0)