Skip to content

Commit fb87367

Browse files
authored
Merge pull request #417 from UgnineSirdis/oauth2-token-exchange Implement OAuth 2.0 Token Exchange credentials provider in Python SDK
2 parents f9a03ac + 9163675 commit fb87367

File tree

15 files changed

+791
-43
lines changed

15 files changed

+791
-43
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import argparse
2+
import ydb
3+
import ydb.oauth2_token_exchange
4+
5+
6+
def parse_args():
7+
parser = argparse.ArgumentParser(
8+
usage="%(prog)s options", description="Oauth 2.0 token exchange credentials example"
9+
)
10+
parser.add_argument("-d", "--database", required=True, help="Name of the database to use")
11+
parser.add_argument("-e", "--endpoint", required=True, help="Endpoint url to use")
12+
parser.add_argument("--token-endpoint", required=True, help="Token endpoint url to use")
13+
parser.add_argument("--private-key-file", required=True, help="Private key file path in pem format")
14+
parser.add_argument("--key-id", help="Key id")
15+
parser.add_argument("--audience", help="Audience")
16+
parser.add_argument("--issuer", help="Jwt token issuer")
17+
parser.add_argument("--subject", help="Jwt token subject")
18+
19+
return parser.parse_args()
20+
21+
22+
def execute_query(session):
23+
# Create the transaction and execute the `select 1` query.
24+
# All transactions must be committed using the `commit_tx` flag in the last
25+
# statement. The either way to commit transaction is using `commit` method of `TxContext` object, which is
26+
# not recommended.
27+
return session.transaction().execute(
28+
"select 1 as cnt;",
29+
commit_tx=True,
30+
settings=ydb.BaseRequestSettings().with_timeout(3).with_operation_timeout(2),
31+
)
32+
33+
34+
def main():
35+
args = parse_args()
36+
37+
# Example demonstrates how to initializate driver instance
38+
# using the oauth 2.0 token exchange credentials provider.
39+
driver = ydb.Driver(
40+
endpoint=args.endpoint,
41+
database=args.database,
42+
root_certificates=ydb.load_ydb_root_certificate(),
43+
credentials=ydb.oauth2_token_exchange.Oauth2TokenExchangeCredentials(
44+
token_endpoint=args.token_endpoint,
45+
audience=args.audience,
46+
subject_token_source=ydb.oauth2_token_exchange.JwtTokenSource(
47+
signing_method="RS256",
48+
private_key_file=args.private_key_file,
49+
key_id=args.key_id,
50+
issuer=args.issuer,
51+
subject=args.subject,
52+
audience=args.audience,
53+
),
54+
),
55+
)
56+
57+
# Start driver context manager.
58+
# The recommended way of using Driver object is using `with`
59+
# clause, because the context manager automatically stops the driver.
60+
with driver:
61+
# wait until driver become initialized
62+
driver.wait(fail_fast=True, timeout=5)
63+
64+
# Initialize the session pool instance and enter the context manager.
65+
# The context manager automatically stops the session pool.
66+
# On the session pool termination all YDB sessions are closed.
67+
with ydb.SessionPool(driver) as pool:
68+
69+
# Execute the query with the `retry_operation_helper` the.
70+
# The `retry_operation_sync` helper used to help developers
71+
# to retry YDB specific errors like locks invalidation.
72+
# The first argument of the `retry_operation_sync` is a function to retry.
73+
# This function must have session as the first argument.
74+
result = pool.retry_operation_sync(execute_query)
75+
assert result[0].rows[0].cnt == 1
76+
77+
78+
main()

test-requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ pytest-asyncio==0.21.0
3030
pytest-docker-compose==3.2.1
3131
python-dotenv==0.18.0
3232
PyYAML==5.3.1
33+
pyjwt==2.0.0
3334
requests==2.31.0
3435
texttable==1.6.4
3536
toml==0.10.2
@@ -46,4 +47,5 @@ pylint-protobuf
4647
cython
4748
freezegun==1.2.2
4849
pytest-cov
50+
yandexcloud
4951
-e .

tests/aio/test_credentials.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import pytest
2+
import time
3+
import grpc
4+
import threading
5+
6+
import tests.auth.test_credentials
7+
import tests.oauth2_token_exchange
8+
import tests.oauth2_token_exchange.test_token_exchange
9+
import ydb.aio.iam
10+
import ydb.aio.oauth2_token_exchange
11+
import ydb.oauth2_token_exchange.token_source
12+
13+
14+
class TestServiceAccountCredentials(ydb.aio.iam.ServiceAccountCredentials):
15+
def _channel_factory(self):
16+
return grpc.aio.insecure_channel(self._iam_endpoint)
17+
18+
def get_expire_time(self):
19+
return self._expires_in - time.time()
20+
21+
22+
class TestOauth2TokenExchangeCredentials(ydb.aio.oauth2_token_exchange.Oauth2TokenExchangeCredentials):
23+
def get_expire_time(self):
24+
return self._expires_in - time.time()
25+
26+
27+
@pytest.mark.asyncio
28+
async def test_yandex_service_account_credentials():
29+
server = tests.auth.test_credentials.IamTokenServiceTestServer()
30+
credentials = TestServiceAccountCredentials(
31+
tests.auth.test_credentials.SERVICE_ACCOUNT_ID,
32+
tests.auth.test_credentials.ACCESS_KEY_ID,
33+
tests.auth.test_credentials.PRIVATE_KEY,
34+
server.get_endpoint(),
35+
)
36+
t = (await credentials.auth_metadata())[0][1]
37+
assert t == "test_token"
38+
assert credentials.get_expire_time() <= 42
39+
server.stop()
40+
41+
42+
@pytest.mark.asyncio
43+
async def test_oauth2_token_exchange_credentials():
44+
server = tests.oauth2_token_exchange.test_token_exchange.Oauth2TokenExchangeServiceForTest(40124)
45+
46+
def serve(s):
47+
s.handle_request()
48+
49+
serve_thread = threading.Thread(target=serve, args=(server,))
50+
serve_thread.start()
51+
52+
credentials = TestOauth2TokenExchangeCredentials(
53+
server.endpoint(),
54+
ydb.oauth2_token_exchange.token_source.FixedTokenSource("test_src_token", "test_token_type"),
55+
audience=["a1", "a2"],
56+
scope=["s1", "s2"],
57+
)
58+
t = (await credentials.auth_metadata())[0][1]
59+
assert t == "Bearer test_dst_token"
60+
assert credentials.get_expire_time() <= 42
61+
62+
serve_thread.join()

tests/auth/__init__.py

Whitespace-only changes.

tests/auth/test_credentials.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import jwt
2+
import concurrent.futures
3+
import grpc
4+
import time
5+
6+
import ydb.iam
7+
8+
from yandex.cloud.iam.v1 import iam_token_service_pb2_grpc
9+
from yandex.cloud.iam.v1 import iam_token_service_pb2
10+
11+
SERVICE_ACCOUNT_ID = "sa_id"
12+
ACCESS_KEY_ID = "key_id"
13+
PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC75/JS3rMcLJxv\nFgpOzF5+2gH+Yig3RE2MTl9uwC0BZKAv6foYr7xywQyWIK+W1cBhz8R4LfFmZo2j\nM0aCvdRmNBdW0EDSTnHLxCsFhoQWLVq+bI5f5jzkcoiioUtaEpADPqwgVULVtN/n\nnPJiZ6/dU30C3jmR6+LUgEntUtWt3eq3xQIn5lG3zC1klBY/HxtfH5Hu8xBvwRQT\nJnh3UpPLj8XwSmriDgdrhR7o6umWyVuGrMKlLHmeivlfzjYtfzO1MOIMG8t2/zxG\nR+xb4Vwks73sH1KruH/0/JMXU97npwpe+Um+uXhpldPygGErEia7abyZB2gMpXqr\nWYKMo02NAgMBAAECggEAO0BpC5OYw/4XN/optu4/r91bupTGHKNHlsIR2rDzoBhU\nYLd1evpTQJY6O07EP5pYZx9mUwUdtU4KRJeDGO/1/WJYp7HUdtxwirHpZP0lQn77\nuccuX/QQaHLrPekBgz4ONk+5ZBqukAfQgM7fKYOLk41jgpeDbM2Ggb6QUSsJISEp\nzrwpI/nNT/wn+Hvx4DxrzWU6wF+P8kl77UwPYlTA7GsT+T7eKGVH8xsxmK8pt6lg\nsvlBA5XosWBWUCGLgcBkAY5e4ZWbkdd183o+oMo78id6C+PQPE66PLDtHWfpRRmN\nm6XC03x6NVhnfvfozoWnmS4+e4qj4F/emCHvn0GMywKBgQDLXlj7YPFVXxZpUvg/\nrheVcCTGbNmQJ+4cZXx87huqwqKgkmtOyeWsRc7zYInYgraDrtCuDBCfP//ZzOh0\nLxepYLTPk5eNn/GT+VVrqsy35Ccr60g7Lp/bzb1WxyhcLbo0KX7/6jl0lP+VKtdv\nmto+4mbSBXSM1Y5BVVoVgJ3T/wKBgQDsiSvPRzVi5TTj13x67PFymTMx3HCe2WzH\nJUyepCmVhTm482zW95pv6raDr5CTO6OYpHtc5sTTRhVYEZoEYFTM9Vw8faBtluWG\nBjkRh4cIpoIARMn74YZKj0C/0vdX7SHdyBOU3bgRPHg08Hwu3xReqT1kEPSI/B2V\n4pe5fVrucwKBgQCNFgUxUA3dJjyMES18MDDYUZaRug4tfiYouRdmLGIxUxozv6CG\nZnbZzwxFt+GpvPUV4f+P33rgoCvFU+yoPctyjE6j+0aW0DFucPmb2kBwCu5J/856\nkFwCx3blbwFHAco+SdN7g2kcwgmV2MTg/lMOcU7XwUUcN0Obe7UlWbckzQKBgQDQ\nnXaXHL24GGFaZe4y2JFmujmNy1dEsoye44W9ERpf9h1fwsoGmmCKPp90az5+rIXw\nFXl8CUgk8lXW08db/r4r+ma8Lyx0GzcZyplAnaB5/6j+pazjSxfO4KOBy4Y89Tb+\nTP0AOcCi6ws13bgY+sUTa/5qKA4UVw+c5zlb7nRpgwKBgGXAXhenFw1666482iiN\ncHSgwc4ZHa1oL6aNJR1XWH+aboBSwR+feKHUPeT4jHgzRGo/aCNHD2FE5I8eBv33\nof1kWYjAO0YdzeKrW0rTwfvt9gGg+CS397aWu4cy+mTI+MNfBgeDAIVBeJOJXLlX\nhL8bFAuNNVrCOp79TNnNIsh7\n-----END PRIVATE KEY-----\n" # noqa: E501
14+
PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu+fyUt6zHCycbxYKTsxe\nftoB/mIoN0RNjE5fbsAtAWSgL+n6GK+8csEMliCvltXAYc/EeC3xZmaNozNGgr3U\nZjQXVtBA0k5xy8QrBYaEFi1avmyOX+Y85HKIoqFLWhKQAz6sIFVC1bTf55zyYmev\n3VN9At45kevi1IBJ7VLVrd3qt8UCJ+ZRt8wtZJQWPx8bXx+R7vMQb8EUEyZ4d1KT\ny4/F8Epq4g4Ha4Ue6OrplslbhqzCpSx5nor5X842LX8ztTDiDBvLdv88RkfsW+Fc\nJLO97B9Sq7h/9PyTF1Pe56cKXvlJvrl4aZXT8oBhKxImu2m8mQdoDKV6q1mCjKNN\njQIDAQAB\n-----END PUBLIC KEY-----\n" # noqa: E501
15+
16+
17+
def test_metadata_credentials():
18+
credentials = ydb.iam.MetadataUrlCredentials()
19+
raised = False
20+
try:
21+
credentials.auth_metadata()
22+
except Exception:
23+
raised = True
24+
25+
assert raised
26+
27+
28+
class IamTokenServiceForTest(iam_token_service_pb2_grpc.IamTokenServiceServicer):
29+
def Create(self, request, context):
30+
print("IAM token service request: {}".format(request))
31+
# Validate jwt:
32+
decoded = jwt.decode(
33+
request.jwt, key=PUBLIC_KEY, algorithms=["PS256"], audience="https://iam.api.cloud.yandex.net/iam/v1/tokens"
34+
)
35+
assert decoded["iss"] == SERVICE_ACCOUNT_ID
36+
assert decoded["aud"] == "https://iam.api.cloud.yandex.net/iam/v1/tokens"
37+
assert abs(decoded["iat"] - time.time()) <= 60
38+
assert abs(decoded["exp"] - time.time()) <= 3600
39+
40+
response = iam_token_service_pb2.CreateIamTokenResponse(iam_token="test_token")
41+
response.expires_at.seconds = int(time.time() + 42)
42+
return response
43+
44+
45+
class IamTokenServiceTestServer(object):
46+
def __init__(self):
47+
self.server = grpc.server(concurrent.futures.ThreadPoolExecutor(max_workers=2))
48+
iam_token_service_pb2_grpc.add_IamTokenServiceServicer_to_server(IamTokenServiceForTest(), self.server)
49+
self.server.add_insecure_port(self.get_endpoint())
50+
self.server.start()
51+
52+
def stop(self):
53+
self.server.stop(1)
54+
self.server.wait_for_termination()
55+
56+
def get_endpoint(self):
57+
return "localhost:54321"
58+
59+
60+
class TestServiceAccountCredentials(ydb.iam.ServiceAccountCredentials):
61+
def _channel_factory(self):
62+
return grpc.insecure_channel(self._iam_endpoint)
63+
64+
def get_expire_time(self):
65+
return self._expires_in - time.time()
66+
67+
68+
def test_yandex_service_account_credentials():
69+
server = IamTokenServiceTestServer()
70+
credentials = TestServiceAccountCredentials(SERVICE_ACCOUNT_ID, ACCESS_KEY_ID, PRIVATE_KEY, server.get_endpoint())
71+
t = credentials.get_auth_token()
72+
assert t == "test_token"
73+
assert credentials.get_expire_time() <= 42
74+
server.stop()

tests/oauth2_token_exchange/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)