diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index f143d52dd..cff668bba 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -6721,7 +6721,7 @@ ## redshift
-28% implemented +29% implemented - [ ] accept_reserved_node_exchange - [ ] authorize_cluster_security_group_ingress @@ -6789,7 +6789,7 @@ - [X] disable_snapshot_copy - [ ] enable_logging - [X] enable_snapshot_copy -- [ ] get_cluster_credentials +- [X] get_cluster_credentials - [ ] get_reserved_node_exchange_offerings - [X] modify_cluster - [ ] modify_cluster_db_revision diff --git a/moto/redshift/models.py b/moto/redshift/models.py index 15fb24ae2..f31de9c96 100644 --- a/moto/redshift/models.py +++ b/moto/redshift/models.py @@ -8,6 +8,7 @@ from boto3 import Session from moto.compat import OrderedDict from moto.core import BaseBackend, BaseModel, CloudFormationModel from moto.core.utils import iso_8601_datetime_with_milliseconds +from moto.utilities.utils import random_string from moto.ec2 import ec2_backends from .exceptions import ( ClusterAlreadyExistsFaultError, @@ -941,6 +942,25 @@ class RedshiftBackend(BaseBackend): resource = self._get_resource_from_arn(resource_name) resource.delete_tags(tag_keys) + def get_cluster_credentials( + self, cluster_identifier, db_user, auto_create, duration_seconds + ): + if duration_seconds < 900 or duration_seconds > 3600: + raise InvalidParameterValueError( + "Token duration must be between 900 and 3600 seconds" + ) + expiration = datetime.datetime.now() + datetime.timedelta(0, duration_seconds) + if cluster_identifier in self.clusters: + user_prefix = "IAM:" if auto_create is False else "IAMA:" + db_user = user_prefix + db_user + return { + "DbUser": db_user, + "DbPassword": random_string(32), + "Expiration": expiration, + } + else: + raise ClusterNotFoundError(cluster_identifier) + redshift_backends = {} for region in Session().get_available_regions("redshift"): diff --git a/moto/redshift/responses.py b/moto/redshift/responses.py index a4094949f..1159eb93f 100644 --- a/moto/redshift/responses.py +++ b/moto/redshift/responses.py @@ -694,3 +694,24 @@ class RedshiftResponse(BaseResponse): } } ) + + def get_cluster_credentials(self): + cluster_identifier = self._get_param("ClusterIdentifier") + db_user = self._get_param("DbUser") + auto_create = self._get_bool_param("AutoCreate", False) + duration_seconds = self._get_int_param("DurationSeconds", 900) + + cluster_credentials = self.redshift_backend.get_cluster_credentials( + cluster_identifier, db_user, auto_create, duration_seconds + ) + + return self.get_response( + { + "GetClusterCredentialsResponse": { + "GetClusterCredentialsResult": cluster_credentials, + "ResponseMetadata": { + "RequestId": "384ac68d-3775-11df-8963-01868b7c937a" + }, + } + } + ) diff --git a/tests/test_redshift/test_redshift.py b/tests/test_redshift/test_redshift.py index 3ac099b40..1c42c00bd 100644 --- a/tests/test_redshift/test_redshift.py +++ b/tests/test_redshift/test_redshift.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import time import datetime import boto @@ -1487,3 +1488,107 @@ def test_resize_cluster(): ) ex.value.response["Error"]["Code"].should.equal("InvalidParameterValue") ex.value.response["Error"]["Message"].should.contain("Invalid cluster type") + + +@mock_redshift +def test_get_cluster_credentials_non_existent_cluster(): + client = boto3.client("redshift", region_name="us-east-1") + + with pytest.raises(ClientError) as ex: + client.get_cluster_credentials(ClusterIdentifier="non-existent") + ex.value.response["Error"]["Code"].should.equal("ClusterNotFound") + ex.value.response["Error"]["Message"].should.match(r"Cluster .+ not found.") + + +@mock_redshift +def test_get_cluster_credentials_non_existent_cluster(): + client = boto3.client("redshift", region_name="us-east-1") + + with pytest.raises(ClientError) as ex: + client.get_cluster_credentials( + ClusterIdentifier="non-existent", DbUser="some_user" + ) + ex.value.response["Error"]["Code"].should.equal("ClusterNotFound") + ex.value.response["Error"]["Message"].should.match(r"Cluster .+ not found.") + + +@mock_redshift +def test_get_cluster_credentials_invalid_duration(): + client = boto3.client("redshift", region_name="us-east-1") + + cluster_identifier = "my_cluster" + client.create_cluster( + ClusterIdentifier=cluster_identifier, + ClusterType="single-node", + DBName="test", + MasterUsername="user", + MasterUserPassword="password", + NodeType="ds2.xlarge", + ) + + db_user = "some_user" + with pytest.raises(ClientError) as ex: + client.get_cluster_credentials( + ClusterIdentifier=cluster_identifier, DbUser=db_user, DurationSeconds=899 + ) + ex.value.response["Error"]["Code"].should.equal("InvalidParameterValue") + ex.value.response["Error"]["Message"].should.contain( + "Token duration must be between 900 and 3600 seconds" + ) + + with pytest.raises(ClientError) as ex: + client.get_cluster_credentials( + ClusterIdentifier=cluster_identifier, DbUser=db_user, DurationSeconds=3601 + ) + ex.value.response["Error"]["Code"].should.equal("InvalidParameterValue") + ex.value.response["Error"]["Message"].should.contain( + "Token duration must be between 900 and 3600 seconds" + ) + + +@mock_redshift +def test_get_cluster_credentials(): + client = boto3.client("redshift", region_name="us-east-1") + + cluster_identifier = "my_cluster" + client.create_cluster( + ClusterIdentifier=cluster_identifier, + ClusterType="single-node", + DBName="test", + MasterUsername="user", + MasterUserPassword="password", + NodeType="ds2.xlarge", + ) + + expected_expiration = time.mktime( + (datetime.datetime.now() + datetime.timedelta(0, 900)).timetuple() + ) + db_user = "some_user" + response = client.get_cluster_credentials( + ClusterIdentifier=cluster_identifier, DbUser=db_user, + ) + response["DbUser"].should.equal("IAM:%s" % db_user) + assert time.mktime((response["Expiration"]).timetuple()) == pytest.approx( + expected_expiration + ) + response["DbPassword"].should.have.length_of(32) + + response = client.get_cluster_credentials( + ClusterIdentifier=cluster_identifier, DbUser=db_user, AutoCreate=True + ) + response["DbUser"].should.equal("IAMA:%s" % db_user) + + response = client.get_cluster_credentials( + ClusterIdentifier=cluster_identifier, DbUser="some_other_user", AutoCreate=False + ) + response["DbUser"].should.equal("IAM:%s" % "some_other_user") + + expected_expiration = time.mktime( + (datetime.datetime.now() + datetime.timedelta(0, 3000)).timetuple() + ) + response = client.get_cluster_credentials( + ClusterIdentifier=cluster_identifier, DbUser=db_user, DurationSeconds=3000, + ) + assert time.mktime(response["Expiration"].timetuple()) == pytest.approx( + expected_expiration + )