open-metadata--openmetadata
bf2343b7e4
Integration Tests - MySQL + Elasticsearch / Detect Changes (push) Has been cancelled
Integration Tests - MySQL + Elasticsearch / integration-tests-mysql-elasticsearch (push) Has been cancelled
Integration Tests - PostgreSQL + Elasticsearch + Redis / Detect Changes (push) Has been cancelled
Integration Tests - PostgreSQL + Elasticsearch + Redis / integration-tests-postgres-elasticsearch-redis (push) Has been cancelled
Integration Tests - PostgreSQL + OpenSearch / Detect Changes (push) Has been cancelled
Integration Tests - PostgreSQL + OpenSearch / integration-tests-postgres-opensearch (push) Has been cancelled
Java Checkstyle / java-checkstyle (push) Has been cancelled
Maven Collate Tests / maven-collate-ci (push) Has been cancelled
OpenMetadata Service Unit Tests / openmetadata-service-unit-tests-status (push) Has been cancelled
Publish Package to Maven Central Repository / publish-maven-packages (push) Has been cancelled
OpenMetadata Service Unit Tests / Detect Changes (push) Has been cancelled
OpenMetadata Service Unit Tests / openmetadata-service-unit-tests (push) Has been cancelled
OpenMetadata Service Unit Tests / k8s_operator-unit-tests (push) Has been cancelled
128 行
5.4 KiB
Python
128 行
5.4 KiB
Python
# Copyright 2025 Collate
|
|
# Licensed under the Collate Community License, Version 1.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
# https://github.com/open-metadata/OpenMetadata/blob/main/ingestion/LICENSE
|
|
# 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.
|
|
"""
|
|
Characterization tests for the SHARED RDS IAM auth path in ``builders.py``.
|
|
|
|
RDS IAM auth tokens expire after ~15 minutes. ``get_connection_url_common`` mints
|
|
the token once and bakes it into the connection URL string, so a single engine
|
|
reuses one frozen token for every pooled connection and cannot refresh it.
|
|
|
|
The MySQL connector now works around this at the connector level (it builds the
|
|
engine without a token in the URL and attaches a ``do_connect`` listener that
|
|
injects a fresh token per connection) — see
|
|
``tests/unit/source/database/test_mysql_iam.py``.
|
|
|
|
These tests pin the behaviour of the SHARED helper, which is still token-frozen
|
|
and is the remaining gap for the other RDS connectors that go through it
|
|
(Postgres, Redshift, Greenplum, Timescale). They are expected to FAIL — and
|
|
should be flipped to assert refreshed behaviour — once the shared path is fixed
|
|
too. ``test_iam_token_is_used_for_authentication`` is the invariant that holds
|
|
either way.
|
|
"""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from metadata.generated.schema.entity.services.connections.database.common.iamAuthConfig import (
|
|
IamAuthConfigurationSource,
|
|
)
|
|
from metadata.generated.schema.entity.services.connections.database.mysqlConnection import (
|
|
MysqlConnection,
|
|
)
|
|
from metadata.generated.schema.security.credentials.awsCredentials import (
|
|
AWSCredentials,
|
|
)
|
|
from metadata.ingestion.connections import builders
|
|
|
|
IAM_TOKEN = "iam-token-v1"
|
|
HOST = "myrds.abc.us-east-1.rds.amazonaws.com"
|
|
PORT = "3306"
|
|
USERNAME = "iam_user"
|
|
REGION = "us-east-1"
|
|
|
|
|
|
def _iam_connection() -> MysqlConnection:
|
|
return MysqlConnection(
|
|
username=USERNAME,
|
|
hostPort=f"{HOST}:{PORT}",
|
|
authType=IamAuthConfigurationSource(awsConfig=AWSCredentials(awsRegion=REGION)),
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_rds_token():
|
|
"""Patch AWSClient so generate_db_auth_token returns a fixed token and is counted."""
|
|
with patch.object(builders, "AWSClient") as mock_aws_client:
|
|
rds_client = MagicMock()
|
|
rds_client.generate_db_auth_token.return_value = IAM_TOKEN
|
|
mock_aws_client.return_value.get_rds_client.return_value = rds_client
|
|
yield rds_client
|
|
|
|
|
|
class TestRdsIamTokenRefresh:
|
|
def test_iam_token_is_used_for_authentication(self, mock_rds_token):
|
|
"""Invariant: the generated IAM token authenticates the connection.
|
|
|
|
Holds before and after the B1 fix.
|
|
"""
|
|
url = builders.get_connection_url_common(_iam_connection())
|
|
|
|
assert IAM_TOKEN in url
|
|
assert mock_rds_token.generate_db_auth_token.call_args.kwargs == {
|
|
"DBHostname": HOST,
|
|
"Port": PORT,
|
|
"DBUsername": USERNAME,
|
|
"Region": REGION,
|
|
}
|
|
|
|
def test_iam_token_is_generated_once_and_frozen_in_url(self, mock_rds_token):
|
|
"""Documents the remaining shared-path gap: token baked into the URL string.
|
|
|
|
``get_connection_url_common`` embeds the token in the URL, so every engine
|
|
built from it reuses that single token for its whole lifetime — it is never
|
|
refreshed and goes stale after the ~15 min RDS IAM token TTL. The MySQL
|
|
connector bypasses this path; the connectors that still rely on it do not.
|
|
|
|
EXPECTED TO FAIL once the shared path is fixed to mint a token per
|
|
connection rather than embedding it in the URL.
|
|
"""
|
|
conn = _iam_connection()
|
|
|
|
first_url = builders.get_connection_url_common(conn)
|
|
second_url = builders.get_connection_url_common(conn)
|
|
|
|
assert IAM_TOKEN in first_url, "token is embedded directly in the URL (the bug)"
|
|
assert first_url == second_url, "same frozen token is reused, never refreshed"
|
|
|
|
def test_no_do_connect_listener_is_registered_for_iam(self, mock_rds_token):
|
|
"""Documents the shared-path gap: no per-connection token injection hook.
|
|
|
|
The shared ``create_generic_db_connection`` does not attach a ``do_connect``
|
|
listener for IAM, so an engine built purely through it cannot refresh the
|
|
baked-in token once it expires. (The MySQL connector adds this listener
|
|
itself; see test_mysql_iam.py.)
|
|
|
|
EXPECTED TO FAIL once the shared path grows its own IAM listener.
|
|
"""
|
|
with patch.object(builders, "listen") as mock_listen:
|
|
builders.create_generic_db_connection(
|
|
connection=_iam_connection(),
|
|
get_connection_url_fn=builders.get_connection_url_common,
|
|
get_connection_args_fn=builders.get_connection_args_common,
|
|
)
|
|
|
|
do_connect_registrations = [call for call in mock_listen.call_args_list if "do_connect" in call.args]
|
|
assert not do_connect_registrations, (
|
|
"no do_connect listener is wired for IAM today — this is the gap B1 "
|
|
"describes; once fixed, invert this assertion"
|
|
)
|