diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 295b06a9d..1934b69fe 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -5662,15 +5662,15 @@ - [ ] attach_managed_policy_to_permission_set - [X] create_account_assignment - [ ] create_instance_access_control_attribute_configuration -- [ ] create_permission_set +- [X] create_permission_set - [X] delete_account_assignment - [ ] delete_inline_policy_from_permission_set - [ ] delete_instance_access_control_attribute_configuration -- [ ] delete_permission_set +- [X] delete_permission_set - [ ] describe_account_assignment_creation_status - [ ] describe_account_assignment_deletion_status - [ ] describe_instance_access_control_attribute_configuration -- [ ] describe_permission_set +- [X] describe_permission_set - [ ] describe_permission_set_provisioning_status - [ ] detach_managed_policy_from_permission_set - [ ] get_inline_policy_for_permission_set @@ -5681,7 +5681,7 @@ - [ ] list_instances - [ ] list_managed_policies_in_permission_set - [ ] list_permission_set_provisioning_status -- [ ] list_permission_sets +- [X] list_permission_sets - [ ] list_permission_sets_provisioned_to_account - [ ] list_tags_for_resource - [ ] provision_permission_set @@ -5689,7 +5689,7 @@ - [ ] tag_resource - [ ] untag_resource - [ ] update_instance_access_control_attribute_configuration -- [ ] update_permission_set +- [X] update_permission_set ## stepfunctions diff --git a/moto/ssoadmin/models.py b/moto/ssoadmin/models.py index 68b4854f0..26a743c34 100644 --- a/moto/ssoadmin/models.py +++ b/moto/ssoadmin/models.py @@ -3,6 +3,9 @@ from .exceptions import ResourceNotFound from moto.core import BaseBackend, BaseModel from moto.core.utils import BackendDict, unix_time from uuid import uuid4 +import random +from moto.utilities.paginator import paginate +from .utils import PAGINATION_MODEL class AccountAssignment(BaseModel): @@ -37,12 +40,54 @@ class AccountAssignment(BaseModel): return summary +class PermissionSet(BaseModel): + def __init__( + self, + name, + description, + instance_arn, + session_duration, + relay_state, + tags, + ): + self.name = name + self.description = description + self.instance_arn = instance_arn + self.permission_set_arn = PermissionSet.generate_id(instance_arn) + self.session_duration = session_duration + self.relay_state = relay_state + self.tags = tags + self.created_date = unix_time() + + def to_json(self, include_creation_date=False): + summary = { + "Name": self.name, + "Description": self.description, + "PermissionSetArn": self.permission_set_arn, + "SessionDuration": self.session_duration, + "RelayState": self.relay_state, + } + if include_creation_date: + summary["CreatedDate"] = self.created_date + return summary + + @staticmethod + def generate_id(instance_arn): + chars = list(range(10)) + ["a", "b", "c", "d", "e", "f"] + return ( + instance_arn + + "/ps-" + + "".join(str(random.choice(chars)) for _ in range(16)) + ) + + class SSOAdminBackend(BaseBackend): """Implementation of SSOAdmin APIs.""" def __init__(self, region_name, account_id): super().__init__(region_name, account_id) self.account_assignments = list() + self.permission_sets = list() def create_account_assignment( self, @@ -132,5 +177,89 @@ class SSOAdminBackend(BaseBackend): ) return account_assignments + def create_permission_set( + self, + name, + description, + instance_arn, + session_duration, + relay_state, + tags, + ): + permission_set = PermissionSet( + name, + description, + instance_arn, + session_duration, + relay_state, + tags, + ) + self.permission_sets.append(permission_set) + return permission_set.to_json(True) + + def update_permission_set( + self, + instance_arn, + permission_set_arn, + description, + session_duration, + relay_state, + ): + permission_set = self._find_permission_set( + instance_arn, + permission_set_arn, + ) + self.permission_sets.remove(permission_set) + permission_set.description = description + permission_set.session_duration = session_duration + permission_set.relay_state = relay_state + self.permission_sets.append(permission_set) + return permission_set.to_json(True) + + def describe_permission_set( + self, + instance_arn, + permission_set_arn, + ): + permission_set = self._find_permission_set( + instance_arn, + permission_set_arn, + ) + return permission_set.to_json(True) + + def delete_permission_set( + self, + instance_arn, + permission_set_arn, + ): + permission_set = self._find_permission_set( + instance_arn, + permission_set_arn, + ) + self.permission_sets.remove(permission_set) + return permission_set.to_json(include_creation_date=True) + + def _find_permission_set( + self, + instance_arn, + permission_set_arn, + ): + for permission_set in self.permission_sets: + instance_arn_match = permission_set.instance_arn == instance_arn + permission_set_match = ( + permission_set.permission_set_arn == permission_set_arn + ) + if instance_arn_match and permission_set_match: + return permission_set + raise ResourceNotFound + + @paginate(pagination_model=PAGINATION_MODEL) + def list_permission_sets(self, instance_arn): + permission_sets = [] + for permission_set in self.permission_sets: + if permission_set.instance_arn == instance_arn: + permission_sets.append(permission_set) + return permission_sets + ssoadmin_backends = BackendDict(SSOAdminBackend, "sso") diff --git a/moto/ssoadmin/responses.py b/moto/ssoadmin/responses.py index 43b8091ab..8988b9a7f 100644 --- a/moto/ssoadmin/responses.py +++ b/moto/ssoadmin/responses.py @@ -65,3 +65,71 @@ class SSOAdminResponse(BaseResponse): permission_set_arn=permission_set_arn, ) return json.dumps({"AccountAssignments": assignments}) + + def create_permission_set(self): + name = self._get_param("Name") + description = self._get_param("Description") + instance_arn = self._get_param("InstanceArn") + session_duration = self._get_param("SessionDuration", 3600) + relay_state = self._get_param("RelayState") + tags = self._get_param("Tags") + + permission_set = self.ssoadmin_backend.create_permission_set( + name=name, + description=description, + instance_arn=instance_arn, + session_duration=session_duration, + relay_state=relay_state, + tags=tags, + ) + + return json.dumps({"PermissionSet": permission_set}) + + def delete_permission_set(self): + params = json.loads(self.body) + instance_arn = params.get("InstanceArn") + permission_set_arn = params.get("PermissionSetArn") + self.ssoadmin_backend.delete_permission_set( + instance_arn=instance_arn, + permission_set_arn=permission_set_arn, + ) + + def update_permission_set(self): + instance_arn = self._get_param("InstanceArn") + permission_set_arn = self._get_param("PermissionSetArn") + description = self._get_param("Description") + session_duration = self._get_param("SessionDuration", 3600) + relay_state = self._get_param("RelayState") + + self.ssoadmin_backend.update_permission_set( + instance_arn=instance_arn, + permission_set_arn=permission_set_arn, + description=description, + session_duration=session_duration, + relay_state=relay_state, + ) + + def describe_permission_set(self): + instance_arn = self._get_param("InstanceArn") + permission_set_arn = self._get_param("PermissionSetArn") + + permission_set = self.ssoadmin_backend.describe_permission_set( + instance_arn=instance_arn, + permission_set_arn=permission_set_arn, + ) + return json.dumps({"PermissionSet": permission_set}) + + def list_permission_sets(self): + instance_arn = self._get_param("InstanceArn") + max_results = self._get_int_param("MaxResults") + next_token = self._get_param("NextToken") + permission_sets, next_token = self.ssoadmin_backend.list_permission_sets( + instance_arn=instance_arn, max_results=max_results, next_token=next_token + ) + permission_set_ids = [] + for permission_set in permission_sets: + permission_set_ids.append(permission_set.permission_set_arn) + response = {"PermissionSets": permission_set_ids} + if next_token: + response["NextToken"] = next_token + return json.dumps(response) diff --git a/moto/ssoadmin/utils.py b/moto/ssoadmin/utils.py new file mode 100644 index 000000000..1b684206c --- /dev/null +++ b/moto/ssoadmin/utils.py @@ -0,0 +1,9 @@ +PAGINATION_MODEL = { + "list_permission_sets": { + "input_token": "next_token", + "limit_key": "max_results", + "limit_default": 100, + "result_key": "PermissionSets", + "unique_attribute": "permission_set_arn", + }, +} diff --git a/tests/test_ssoadmin/test_ssoadmin.py b/tests/test_ssoadmin/test_ssoadmin.py index 64091ad33..fd9fc505b 100644 --- a/tests/test_ssoadmin/test_ssoadmin.py +++ b/tests/test_ssoadmin/test_ssoadmin.py @@ -188,3 +188,203 @@ def test_list_account_assignments(): } ] ) + + +@mock_ssoadmin +def test_create_permission_set(): + client = boto3.client("sso-admin", region_name="ap-southeast-1") + resp = client.create_permission_set( + Name="test", + Description="Test permission set", + InstanceArn="arn:aws:sso:::instance/ins-aaaabbbbccccdddd", + SessionDuration="PT1H", + RelayState="https://console.aws.amazon.com/ec2", + ) + resp.should.have.key("PermissionSet") + permissionSet = resp["PermissionSet"] + permissionSet.should.have.key("Name").equals("test") + permissionSet.should.have.key("PermissionSetArn") + permissionSet.should.have.key("Description") + permissionSet.should.have.key("CreatedDate") + permissionSet.should.have.key("SessionDuration") + permissionSet.should.have.key("RelayState") + + +@mock_ssoadmin +def test_update_permission_set(): + client = boto3.client("sso-admin", region_name="ap-southeast-1") + resp = client.create_permission_set( + Name="test", + Description="Test permission set", + InstanceArn="arn:aws:sso:::instance/ins-aaaabbbbccccdddd", + SessionDuration="PT1H", + ) + permissionSet = resp["PermissionSet"] + + resp = client.update_permission_set( + InstanceArn="arn:aws:sso:::instance/ins-aaaabbbbccccdddd", + PermissionSetArn=permissionSet["PermissionSetArn"], + Description="New description", + SessionDuration="PT2H", + RelayState="https://console.aws.amazon.com/s3", + ) + resp = client.describe_permission_set( + InstanceArn="arn:aws:sso:::instance/ins-aaaabbbbccccdddd", + PermissionSetArn=permissionSet["PermissionSetArn"], + ) + resp.should.have.key("PermissionSet") + permissionSet = resp["PermissionSet"] + permissionSet.should.have.key("Name").equals("test") + permissionSet.should.have.key("Description").equals("New description") + permissionSet.should.have.key("CreatedDate") + permissionSet.should.have.key("SessionDuration").equals("PT2H") + permissionSet.should.have.key("RelayState").equals( + "https://console.aws.amazon.com/s3" + ) + + +@mock_ssoadmin +def test_update_permission_set_unknown(): + client = boto3.client("sso-admin", region_name="ap-southeast-1") + + with pytest.raises(ClientError) as exc: + client.update_permission_set( + InstanceArn="arn:aws:sso:::instance/ins-aaaabbbbccccdddd", + PermissionSetArn="arn:aws:sso:::permissionSet/ins-eeeeffffgggghhhh/ps-hhhhkkkkppppoooo", + Description="New description", + SessionDuration="PT2H", + RelayState="https://console.aws.amazon.com/s3", + ) + err = exc.value.response["Error"] + err["Code"].should.equal("ResourceNotFound") + + +@mock_ssoadmin +def test_describe_permission_set(): + client = boto3.client("sso-admin", region_name="ap-southeast-1") + resp = client.create_permission_set( + Name="test", + Description="Test permission set", + InstanceArn="arn:aws:sso:::instance/ins-aaaabbbbccccdddd", + SessionDuration="PT1H", + ) + permissionSet = resp["PermissionSet"] + + resp = client.describe_permission_set( + InstanceArn="arn:aws:sso:::instance/ins-aaaabbbbccccdddd", + PermissionSetArn=permissionSet["PermissionSetArn"], + ) + resp.should.have.key("PermissionSet") + permissionSet = resp["PermissionSet"] + permissionSet.should.have.key("Name").equals("test") + permissionSet.should.have.key("PermissionSetArn") + permissionSet.should.have.key("Description") + permissionSet.should.have.key("CreatedDate") + permissionSet.should.have.key("SessionDuration") + + +@mock_ssoadmin +def test_describe_permission_set_unknown(): + client = boto3.client("sso-admin", region_name="ap-southeast-1") + + with pytest.raises(ClientError) as exc: + client.describe_permission_set( + InstanceArn="arn:aws:sso:::instance/ins-aaaabbbbccccdddd", + PermissionSetArn="arn:aws:sso:::permissionSet/ins-eeeeffffgggghhhh/ps-hhhhkkkkppppoooo", + ) + err = exc.value.response["Error"] + err["Code"].should.equal("ResourceNotFound") + + +@mock_ssoadmin +def test_delete_permission_set(): + client = boto3.client("sso-admin", region_name="ap-southeast-1") + resp = client.create_permission_set( + Name="test", + Description="Test permission set", + InstanceArn="arn:aws:sso:::instance/ins-aaaabbbbccccdddd", + SessionDuration="PT1H", + ) + permissionSet = resp["PermissionSet"] + resp = client.delete_permission_set( + InstanceArn="arn:aws:sso:::instance/ins-aaaabbbbccccdddd", + PermissionSetArn=permissionSet["PermissionSetArn"], + ) + with pytest.raises(ClientError) as exc: + client.describe_permission_set( + InstanceArn="arn:aws:sso:::instance/ins-aaaabbbbccccdddd", + PermissionSetArn=permissionSet["PermissionSetArn"], + ) + err = exc.value.response["Error"] + err["Code"].should.equal("ResourceNotFound") + + +@mock_ssoadmin +def test_delete_permission_set_unknown(): + client = boto3.client("sso-admin", region_name="ap-southeast-1") + + with pytest.raises(ClientError) as exc: + client.delete_permission_set( + InstanceArn="arn:aws:sso:::instance/ins-aaaabbbbccccdddd", + PermissionSetArn="arn:aws:sso:::permissionSet/ins-eeeeffffgggghhhh/ps-hhhhkkkkppppoooo", + ) + err = exc.value.response["Error"] + err["Code"].should.equal("ResourceNotFound") + + +@mock_ssoadmin +def test_list_permission_sets(): + client = boto3.client("sso-admin", region_name="ap-southeast-1") + + response = client.list_permission_sets( + InstanceArn="arn:aws:sso:::instance/ins-aaaabbbbccccdddd", + ) + response.should.have.key("PermissionSets") + permissionSets = response["PermissionSets"] + len(permissionSets).should.equal(0) + + for i in range(5): + client.create_permission_set( + Name="test" + str(i), + Description="Test permission set " + str(i), + InstanceArn="arn:aws:sso:::instance/ins-aaaabbbbccccdddd", + SessionDuration="PT1H", + ) + response = client.list_permission_sets( + InstanceArn="arn:aws:sso:::instance/ins-aaaabbbbccccdddd", + ) + response.should.have.key("PermissionSets") + permissionSets = response["PermissionSets"] + len(permissionSets).should.equal(5) + + +@mock_ssoadmin +def test_list_permission_sets_pagination(): + client = boto3.client("sso-admin", region_name="ap-southeast-1") + + response = client.list_permission_sets( + InstanceArn="arn:aws:sso:::instance/ins-aaaabbbbccccdddd", + ) + response.should.have.key("PermissionSets") + permissionSets = response["PermissionSets"] + len(permissionSets).should.equal(0) + + for i in range(25): + client.create_permission_set( + Name="test" + str(i), + Description="Test permission set " + str(i), + InstanceArn="arn:aws:sso:::instance/ins-aaaabbbbccccdddd", + SessionDuration="PT1H", + ) + response = client.list_permission_sets( + InstanceArn="arn:aws:sso:::instance/ins-aaaabbbbccccdddd", + ) + response.should.have.key("PermissionSets") + response.should_not.have.key("NextToken") + + paginator = client.get_paginator("list_permission_sets") + page_iterator = paginator.paginate( + InstanceArn="arn:aws:sso:::instance/ins-aaaabbbbccccdddd", MaxResults=5 + ) + for page in page_iterator: + len(page["PermissionSets"]).should.be.lower_than_or_equal_to(5)