Skip to content

Commit e7ff3a6

Browse files
committed
* Adding oauth2 and 2.1 support to trino-python-client
* moved set_session to a base class. * added default keyring backend keyrings.cryptfile.cryptfile.CryptFileKeyring to securely store JWT tokens.
1 parent 2108c38 commit e7ff3a6

7 files changed

Lines changed: 414 additions & 52 deletions

File tree

README.md

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,10 @@ the [`JWT` authentication type](https://trino.io/docs/current/security/jwt.html)
217217

218218
### OAuth2 authentication
219219

220+
Make sure that the OAuth2 support is installed using `pip install trino[oauth]`.
221+
222+
#### Interactive Browser authentication
223+
220224
The `OAuth2Authentication` class can be used to connect to a Trino cluster configured with
221225
the [OAuth2 authentication type](https://trino.io/docs/current/security/oauth2.html).
222226

@@ -248,14 +252,127 @@ The OAuth2 token will be cached either per `trino.auth.OAuth2Authentication` ins
248252
from trino.auth import OAuth2Authentication
249253

250254
engine = create_engine(
251-
"trino://<username>@<host>:<port>/<catalog>",
255+
"trino://<username>@<host>:<port>/<catalog>",
252256
connect_args={
253257
"auth": OAuth2Authentication(),
254258
"http_scheme": "https",
255259
}
256260
)
257261
```
258262

263+
#### Client Credentials authentication
264+
265+
```python
266+
from trino.dbapi import connect
267+
from trino.auth import ClientCredentials
268+
from trino.oauth2.models import OidcConfig
269+
270+
auth = ClientCredentials(
271+
client_id="<client_id>",
272+
client_secret="<client_secret>",
273+
url_config=OidcConfig(
274+
token_endpoint="<token_endpoint>",
275+
# other endpoints if needed
276+
),
277+
scope="<number of scopes>", # optional
278+
audience="<audience>", # optional
279+
)
280+
281+
conn = connect(
282+
user="<username>",
283+
auth=auth,
284+
http_scheme="https",
285+
...
286+
)
287+
```
288+
289+
#### Device Code authentication
290+
291+
```python
292+
from trino.dbapi import connect
293+
from trino.auth import DeviceCode
294+
from trino.oauth2.models import OidcConfig
295+
296+
auth = DeviceCode(
297+
client_id="<client_id>",
298+
url_config=OidcConfig(
299+
token_endpoint="<token_endpoint>",
300+
device_authorization_endpoint="<device_authorization_endpoint>",
301+
),
302+
scope="<scope>", # optional
303+
audience="<audience>", # optional
304+
)
305+
306+
conn = connect(
307+
user="<username>",
308+
auth=auth,
309+
http_scheme="https",
310+
...
311+
)
312+
```
313+
314+
#### Authorization Code authentication
315+
316+
```python
317+
from trino.dbapi import connect
318+
from trino.auth import AuthorizationCode
319+
from trino.oauth2.models import OidcConfig
320+
321+
auth = AuthorizationCode(
322+
client_id="<client_id>",
323+
client_secret="<client_secret>", # optional
324+
url_config=OidcConfig(
325+
token_endpoint="<token_endpoint>",
326+
authorization_endpoint="<authorization_endpoint>",
327+
),
328+
scope="<scope>", # optional
329+
audience="<audience>", # optional
330+
)
331+
332+
conn = connect(
333+
user="<username>",
334+
auth=auth,
335+
http_scheme="https",
336+
...
337+
)
338+
```
339+
340+
### Reference
341+
342+
For further details, please consult [Trino documentation](https://trino.io/docs/current).
343+
344+
### Secure Token Storage
345+
346+
By default all ClientCredentials, DeviceCode, AuthorizationCode JWT tokens are securely storaged
347+
using the keyrings.cryptfile feature of [keyring library](https://pypi.org/project/keyring/).
348+
349+
Tokens are stored encrypted at ~/.local/share/python_keyring/cryptfile_pass.cfg
350+
351+
You can optionally use different keyring backends by supplying the `PYTHON_KEYRING_BACKEND` environment variable.
352+
353+
To use an encrypted file backend for credentials:
354+
355+
```bash
356+
export KEYRING_CRYPTFILE_PASSWORD=your_secure_password
357+
```
358+
359+
Or you can pass the password directly (less secure):
360+
361+
```python
362+
conn = connect(
363+
host="trino.example.com",
364+
port=443,
365+
auth=DeviceCode(
366+
client_id="<CLIENT_ID>",
367+
client_secret="<CLIENT_SECRET>",
368+
url_config=OidcConfig(oidc_discovery_url="https://sso.example.com/.well-known/openid-configuration"),
369+
token_storage_password="your_secure_password" # less secure
370+
),
371+
http_scheme="https"
372+
)
373+
```
374+
375+
259376
### Certificate authentication
260377

261378
`CertificateAuthentication` class can be used to connect to Trino cluster configured with [certificate based authentication](https://trino.io/docs/current/security/certificate.html). `CertificateAuthentication` requires paths to a valid client certificate and private key.

setup.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@
3333
"krb5 == 0.5.1"]
3434
sqlalchemy_require = ["sqlalchemy >= 1.3"]
3535
external_authentication_token_cache_require = ["keyring"]
36+
oauth_require = ["trino.oauth2 @ git+https://github.com/dprophet/trino-python-oauth2"]
3637

3738
# We don't add localstorage_require to all_require as users must explicitly opt in to use keyring.
38-
all_require = kerberos_require + sqlalchemy_require
39+
all_require = kerberos_require + sqlalchemy_require + oauth_require
3940

4041
tests_require = all_require + gssapi_require + [
4142
# httpretty >= 1.1 duplicates requests in `httpretty.latest_requests`
@@ -96,6 +97,7 @@
9697
"all": all_require,
9798
"kerberos": kerberos_require,
9899
"gssapi": gssapi_require,
100+
"oauth": oauth_require,
99101
"sqlalchemy": sqlalchemy_require,
100102
"tests": tests_require,
101103
"external-authentication-token-cache": external_authentication_token_cache_require,

tests/unit/oauth_test_utils.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,54 @@ def get_token_callback(self, request, uri, response_headers):
150150
if challenge.attempts == 0:
151151
return [200, response_headers, f'{{"token": "{challenge.token}"}}']
152152
return [200, response_headers, f'{{"nextUri": "{uri}"}}']
153+
154+
155+
import keyring.backend
156+
157+
158+
class MockKeyring(keyring.backend.KeyringBackend):
159+
priority = 1
160+
161+
def __init__(self):
162+
self.file_location = self._generate_test_root_dir()
163+
164+
@staticmethod
165+
def _generate_test_root_dir():
166+
import tempfile
167+
168+
return tempfile.mkdtemp(prefix="trino-python-client-unit-test-")
169+
170+
def _get_file_path(self, servicename, username):
171+
from os.path import join
172+
173+
file_location = self.file_location
174+
file_name = f"{servicename}_{username}.txt"
175+
return join(file_location, file_name)
176+
177+
def set_password(self, servicename, username, password):
178+
file_path = self._get_file_path(servicename, username)
179+
180+
with open(file_path, "w") as file:
181+
file.write(password)
182+
183+
def get_password(self, servicename, username):
184+
import os
185+
186+
file_path = self._get_file_path(servicename, username)
187+
if not os.path.exists(file_path):
188+
return None
189+
190+
with open(file_path, "r") as file:
191+
password = file.read()
192+
193+
return password
194+
195+
def delete_password(self, servicename, username):
196+
import os
197+
198+
file_path = self._get_file_path(servicename, username)
199+
if not os.path.exists(file_path):
200+
return None
201+
202+
os.remove(file_path)
203+

tests/unit/test_client.py

Lines changed: 1 addition & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
from tests.unit.oauth_test_utils import RedirectHandlerWithException
4848
from tests.unit.oauth_test_utils import SERVER_ADDRESS
4949
from tests.unit.oauth_test_utils import TOKEN_RESOURCE
50+
from tests.unit.oauth_test_utils import MockKeyring
5051
from trino import __version__
5152
from trino import constants
5253
from trino.auth import _OAuth2KeyRingTokenCache
@@ -1406,47 +1407,3 @@ def test_store_long_password(self):
14061407
retrieved_password = cache.get_token_from_cache(host)
14071408
self.assertEqual(long_password, retrieved_password)
14081409

1409-
1410-
class MockKeyring(keyring.backend.KeyringBackend):
1411-
def __init__(self):
1412-
self.file_location = self._generate_test_root_dir()
1413-
1414-
@staticmethod
1415-
def _generate_test_root_dir():
1416-
import tempfile
1417-
1418-
return tempfile.mkdtemp(prefix="trino-python-client-unit-test-")
1419-
1420-
def file_path(self, servicename, username):
1421-
from os.path import join
1422-
1423-
file_location = self.file_location
1424-
file_name = f"{servicename}_{username}.txt"
1425-
return join(file_location, file_name)
1426-
1427-
def set_password(self, servicename, username, password):
1428-
file_path = self.file_path(servicename, username)
1429-
1430-
with open(file_path, "w") as file:
1431-
file.write(password)
1432-
1433-
def get_password(self, servicename, username):
1434-
import os
1435-
1436-
file_path = self.file_path(servicename, username)
1437-
if not os.path.exists(file_path):
1438-
return None
1439-
1440-
with open(file_path, "r") as file:
1441-
password = file.read()
1442-
1443-
return password
1444-
1445-
def delete_password(self, servicename, username):
1446-
import os
1447-
1448-
file_path = self.file_path(servicename, username)
1449-
if not os.path.exists(file_path):
1450-
return None
1451-
1452-
os.remove(file_path)

tests/unit/test_dbapi.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import httpretty
1717
import pytest
18+
import keyring
1819
from httpretty import httprettified
1920
from requests import Session
2021

@@ -26,6 +27,7 @@
2627
from tests.unit.oauth_test_utils import RedirectHandler
2728
from tests.unit.oauth_test_utils import SERVER_ADDRESS
2829
from tests.unit.oauth_test_utils import TOKEN_RESOURCE
30+
from tests.unit.oauth_test_utils import MockKeyring
2931
from trino import constants
3032
from trino.auth import OAuth2Authentication
3133
from trino.dbapi import connect
@@ -58,8 +60,15 @@ def test_http_session_is_defaulted_when_not_specified(mock_client):
5860
assert mock_client.TrinoRequest.http.Session.return_value in request_args
5961

6062

63+
@pytest.fixture
64+
def mock_keyring():
65+
mk = MockKeyring()
66+
keyring.set_keyring(mk)
67+
return mk
68+
69+
6170
@httprettified
62-
def test_token_retrieved_once_per_auth_instance(sample_post_response_data, sample_get_response_data):
71+
def test_token_retrieved_once_per_auth_instance(mock_keyring, sample_post_response_data, sample_get_response_data):
6372
token = str(uuid.uuid4())
6473
challenge_id = str(uuid.uuid4())
6574

@@ -123,7 +132,7 @@ def test_token_retrieved_once_per_auth_instance(sample_post_response_data, sampl
123132

124133

125134
@httprettified
126-
def test_token_retrieved_once_when_authentication_instance_is_shared(sample_post_response_data,
135+
def test_token_retrieved_once_when_authentication_instance_is_shared(mock_keyring, sample_post_response_data,
127136
sample_get_response_data):
128137
token = str(uuid.uuid4())
129138
challenge_id = str(uuid.uuid4())
@@ -189,7 +198,7 @@ def test_token_retrieved_once_when_authentication_instance_is_shared(sample_post
189198

190199

191200
@httprettified
192-
def test_token_retrieved_once_when_multithreaded(sample_post_response_data, sample_get_response_data):
201+
def test_token_retrieved_once_when_multithreaded(mock_keyring, sample_post_response_data, sample_get_response_data):
193202
token = str(uuid.uuid4())
194203
challenge_id = str(uuid.uuid4())
195204

trino/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1010
# See the License for the specific language governing permissions and
1111
# limitations under the License.
12+
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
13+
1214
from . import auth
1315
from . import client
1416
from . import constants

0 commit comments

Comments
 (0)