diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index f006f0303608..91b036f486f1 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -7891,7 +7891,7 @@ ## sso-admin
-27% implemented +34% implemented - [X] attach_customer_managed_policy_reference_to_permission_set - [X] attach_managed_policy_to_permission_set @@ -7936,7 +7936,7 @@ - [ ] list_account_assignment_deletion_status - [X] list_account_assignments - [X] list_account_assignments_for_principal -- [ ] list_accounts_for_provisioned_permission_set +- [X] list_accounts_for_provisioned_permission_set - [ ] list_application_access_scopes - [ ] list_application_assignments - [ ] list_application_assignments_for_principal @@ -7945,14 +7945,14 @@ - [ ] list_application_providers - [ ] list_applications - [X] list_customer_managed_policy_references_in_permission_set -- [ ] list_instances +- [X] list_instances - [X] list_managed_policies_in_permission_set - [ ] list_permission_set_provisioning_status - [X] list_permission_sets -- [ ] list_permission_sets_provisioned_to_account +- [X] list_permission_sets_provisioned_to_account - [ ] list_tags_for_resource - [ ] list_trusted_token_issuers -- [ ] provision_permission_set +- [X] provision_permission_set - [ ] put_application_access_scope - [ ] put_application_assignment_configuration - [ ] put_application_authentication_method @@ -7962,7 +7962,7 @@ - [ ] tag_resource - [ ] untag_resource - [ ] update_application -- [ ] update_instance +- [X] update_instance - [ ] update_instance_access_control_attribute_configuration - [X] update_permission_set - [ ] update_trusted_token_issuer diff --git a/docs/docs/services/sso-admin.rst b/docs/docs/services/sso-admin.rst index 600e7c99b83e..66b86e2d66a3 100644 --- a/docs/docs/services/sso-admin.rst +++ b/docs/docs/services/sso-admin.rst @@ -59,7 +59,11 @@ sso-admin - [ ] list_account_assignment_deletion_status - [X] list_account_assignments - [X] list_account_assignments_for_principal -- [ ] list_accounts_for_provisioned_permission_set +- [X] list_accounts_for_provisioned_permission_set + + The following parameters are not yet implemented: MaxResults, NextToken, ProvisioningStatus + + - [ ] list_application_access_scopes - [ ] list_application_assignments - [ ] list_application_assignments_for_principal @@ -68,14 +72,22 @@ sso-admin - [ ] list_application_providers - [ ] list_applications - [X] list_customer_managed_policy_references_in_permission_set -- [ ] list_instances +- [X] list_instances - [X] list_managed_policies_in_permission_set - [ ] list_permission_set_provisioning_status - [X] list_permission_sets -- [ ] list_permission_sets_provisioned_to_account +- [X] list_permission_sets_provisioned_to_account + + The following parameters are not yet implemented: AccountId, ProvisioningStatus, MaxResults, NextToken + + - [ ] list_tags_for_resource - [ ] list_trusted_token_issuers -- [ ] provision_permission_set +- [X] provision_permission_set + + The TargetType/TargetId parameters are currently ignored - PermissionSets are simply provisioned to the caller's account + + - [ ] put_application_access_scope - [ ] put_application_assignment_configuration - [ ] put_application_authentication_method @@ -85,7 +97,7 @@ sso-admin - [ ] tag_resource - [ ] untag_resource - [ ] update_application -- [ ] update_instance +- [X] update_instance - [ ] update_instance_access_control_attribute_configuration - [X] update_permission_set - [ ] update_trusted_token_issuer diff --git a/moto/ssoadmin/models.py b/moto/ssoadmin/models.py index 8e75b0dcc08f..c28187d0397c 100644 --- a/moto/ssoadmin/models.py +++ b/moto/ssoadmin/models.py @@ -7,6 +7,7 @@ from moto.iam.aws_managed_policies import aws_managed_policies_data from moto.moto_api._internal import mock_random as random from moto.utilities.paginator import paginate +from moto.utilities.utils import get_partition from .exceptions import ( ConflictException, @@ -94,12 +95,7 @@ def to_json(self, include_creation_date: bool = False) -> Dict[str, Any]: @staticmethod def generate_id(instance_arn: str) -> str: - chars = list(range(10)) + ["a", "b", "c", "d", "e", "f"] - return ( - instance_arn - + "/ps-" - + "".join(str(random.choice(chars)) for _ in range(16)) - ) + return instance_arn + "/ps-" + random.get_random_string(length=16).lower() class ManagedPolicy(BaseModel): @@ -124,6 +120,30 @@ def __eq__(self, other: Any) -> bool: return f"{self.path}{self.name}" == f"{other.path}{other.name}" +class Instance: + def __init__(self, account_id: str, region: str): + self.created_date = unix_time() + self.identity_store_id = ( + f"d-{random.get_random_string(length=10, lower_case=True)}" + ) + self.instance_arn = f"arn:{get_partition(region)}:sso:::instance/ssoins-{random.get_random_string(length=16, lower_case=True)}" + self.account_id = account_id + self.status = "ACTIVE" + self.name: Optional[str] = None + + self.provisioned_permission_sets: List[PermissionSet] = [] + + def to_json(self) -> Dict[str, Any]: + return { + "CreatedDate": self.created_date, + "IdentityStoreId": self.identity_store_id, + "InstanceArn": self.instance_arn, + "Name": self.name, + "OwnerAccountId": self.account_id, + "Status": self.status, + } + + class SSOAdminBackend(BaseBackend): """Implementation of SSOAdmin APIs.""" @@ -133,6 +153,9 @@ def __init__(self, region_name: str, account_id: str): self.deleted_account_assignments: List[AccountAssignment] = list() self.permission_sets: List[PermissionSet] = list() self.aws_managed_policies: Optional[Dict[str, Any]] = None + self.instances: List[Instance] = [] + + self.instances.append(Instance(self.account_id, self.region_name)) def create_account_assignment( self, @@ -327,6 +350,13 @@ def delete_permission_set( permission_set_arn, ) self.permission_sets.remove(permission_set) + + for instance in self.instances: + try: + instance.provisioned_permission_sets.remove(permission_set) + except ValueError: + pass + return permission_set.to_json(include_creation_date=True) def _find_permission_set( @@ -542,5 +572,47 @@ def describe_account_assignment_deletion_status( raise ResourceNotFoundException + def list_instances(self) -> List[Instance]: + return self.instances + + def update_instance(self, instance_arn: str, name: str) -> None: + for instance in self.instances: + if instance.instance_arn == instance_arn: + instance.name = name + + def provision_permission_set( + self, instance_arn: str, permission_set_arn: str + ) -> None: + """ + The TargetType/TargetId parameters are currently ignored - PermissionSets are simply provisioned to the caller's account + """ + permission_set = self._find_permission_set(instance_arn, permission_set_arn) + instance = [i for i in self.instances if i.instance_arn == instance_arn][0] + instance.provisioned_permission_sets.append(permission_set) + + def list_permission_sets_provisioned_to_account( + self, instance_arn: str + ) -> List[PermissionSet]: + """ + The following parameters are not yet implemented: AccountId, ProvisioningStatus, MaxResults, NextToken + """ + for instance in self.instances: + if instance.instance_arn == instance_arn: + return instance.provisioned_permission_sets + return [] + + def list_accounts_for_provisioned_permission_set( + self, instance_arn: str, permission_set_arn: str + ) -> List[str]: + """ + The following parameters are not yet implemented: MaxResults, NextToken, ProvisioningStatus + """ + for instance in self.instances: + if instance.instance_arn == instance_arn: + for ps in instance.provisioned_permission_sets: + if ps.permission_set_arn == permission_set_arn: + return [self.account_id] + return [] + ssoadmin_backends = BackendDict(SSOAdminBackend, "sso-admin") diff --git a/moto/ssoadmin/responses.py b/moto/ssoadmin/responses.py index fab5b64d2928..533e23c5cbb1 100644 --- a/moto/ssoadmin/responses.py +++ b/moto/ssoadmin/responses.py @@ -1,6 +1,8 @@ import json +from uuid import uuid4 from moto.core.responses import BaseResponse +from moto.core.utils import unix_time from .models import SSOAdminBackend, ssoadmin_backends @@ -325,3 +327,58 @@ def describe_account_assignment_deletion_status(self) -> str: return json.dumps( dict(AccountAssignmentDeletionStatus=account_assignment_deletion_status) ) + + def list_instances(self) -> str: + instances = self.ssoadmin_backend.list_instances() + + return json.dumps({"Instances": [i.to_json() for i in instances]}) + + def update_instance(self) -> str: + instance_arn = self._get_param("InstanceArn") + name = self._get_param("Name") + + self.ssoadmin_backend.update_instance(instance_arn=instance_arn, name=name) + + return "{}" + + def provision_permission_set(self) -> str: + instance_arn = self._get_param("InstanceArn") + permission_set_arn = self._get_param("PermissionSetArn") + + self.ssoadmin_backend.provision_permission_set( + instance_arn=instance_arn, + permission_set_arn=permission_set_arn, + ) + return json.dumps( + { + "PermissionSetProvisioningStatus": { + "AccountId": self.current_account, + "CreatedDate": unix_time(), + "PermissionSetArn": permission_set_arn, + "RequestId": str(uuid4()), + "Status": "SUCCEEDED", + } + } + ) + + def list_permission_sets_provisioned_to_account(self) -> str: + instance_arn = self._get_param("InstanceArn") + + permission_sets = ( + self.ssoadmin_backend.list_permission_sets_provisioned_to_account( + instance_arn + ) + ) + arns = [p.permission_set_arn for p in permission_sets] + return json.dumps({"PermissionSets": arns}) + + def list_accounts_for_provisioned_permission_set(self) -> str: + instance_arn = self._get_param("InstanceArn") + permission_set_arn = self._get_param("PermissionSetArn") + + account_ids = ( + self.ssoadmin_backend.list_accounts_for_provisioned_permission_set( + instance_arn=instance_arn, permission_set_arn=permission_set_arn + ) + ) + return json.dumps({"AccountIds": account_ids}) diff --git a/tests/test_ssoadmin/test_ssoadmin_instances.py b/tests/test_ssoadmin/test_ssoadmin_instances.py new file mode 100644 index 000000000000..ab18c140f207 --- /dev/null +++ b/tests/test_ssoadmin/test_ssoadmin_instances.py @@ -0,0 +1,38 @@ +import boto3 + +from moto import mock_aws +from tests import DEFAULT_ACCOUNT_ID + + +@mock_aws +def test_list_instances(): + ssoadmin = boto3.client("sso-admin", "us-east-1") + + # We automatically create an instance on startup + # In AWS, this would involve some manual steps on the dashboard + instances = ssoadmin.list_instances()["Instances"] + assert len(instances) == 1 + + assert instances[0]["CreatedDate"] + assert instances[0]["IdentityStoreId"].startswith("d-") + assert instances[0]["InstanceArn"].startswith("arn:aws:sso:::instance/ssoins-") + assert instances[0]["OwnerAccountId"] == DEFAULT_ACCOUNT_ID + assert instances[0]["Status"] == "ACTIVE" + + assert "Name" not in instances[0] + + +@mock_aws +def test_update_instance(): + ssoadmin = boto3.client("sso-admin", "us-east-1") + + # We automatically create an instance on startup + # In AWS, this would involve some manual steps on the dashboard + initial = ssoadmin.list_instances()["Instances"][0] + + ssoadmin.update_instance(InstanceArn=initial["InstanceArn"], Name="instancename") + + updated = ssoadmin.list_instances()["Instances"][0] + assert updated["Name"] == "instancename" + assert initial["IdentityStoreId"] == updated["IdentityStoreId"] + assert initial["InstanceArn"] == updated["InstanceArn"] diff --git a/tests/test_ssoadmin/test_ssoadmin_permission_sets.py b/tests/test_ssoadmin/test_ssoadmin_permission_sets.py new file mode 100644 index 000000000000..e1b2579441a4 --- /dev/null +++ b/tests/test_ssoadmin/test_ssoadmin_permission_sets.py @@ -0,0 +1,63 @@ +import boto3 + +from moto import mock_aws +from tests import DEFAULT_ACCOUNT_ID + + +@mock_aws +def test_provision_permission_set(): + ssoadmin = boto3.client("sso-admin", "us-east-1") + + instance_arn = ssoadmin.list_instances()["Instances"][0]["InstanceArn"] + + p_set_arn = ssoadmin.create_permission_set(InstanceArn=instance_arn, Name="pset1")[ + "PermissionSet" + ]["PermissionSetArn"] + + status = ssoadmin.provision_permission_set( + InstanceArn=instance_arn, + PermissionSetArn=p_set_arn, + TargetType="AWS_ACCOUNT", + )["PermissionSetProvisioningStatus"] + + assert status["AccountId"] == DEFAULT_ACCOUNT_ID + assert status["CreatedDate"] + assert status["PermissionSetArn"] == p_set_arn + assert status["Status"] == "SUCCEEDED" + + +@mock_aws +def test_list_permission_sets_provisioned_to_account(): + ssoadmin = boto3.client("sso-admin", "us-east-1") + + instance_arn = ssoadmin.list_instances()["Instances"][0]["InstanceArn"] + + p_set_arn = ssoadmin.create_permission_set(InstanceArn=instance_arn, Name="pset1")[ + "PermissionSet" + ]["PermissionSetArn"] + + provisioned = ssoadmin.list_permission_sets_provisioned_to_account( + AccountId=DEFAULT_ACCOUNT_ID, InstanceArn=instance_arn + )["PermissionSets"] + assert len(provisioned) == 0 + + accounts = ssoadmin.list_accounts_for_provisioned_permission_set( + InstanceArn=instance_arn, PermissionSetArn=p_set_arn + )["AccountIds"] + assert accounts == [] + + ssoadmin.provision_permission_set( + InstanceArn=instance_arn, + PermissionSetArn=p_set_arn, + TargetType="AWS_ACCOUNT", + ) + + provisioned = ssoadmin.list_permission_sets_provisioned_to_account( + AccountId=DEFAULT_ACCOUNT_ID, InstanceArn=instance_arn + )["PermissionSets"] + assert provisioned == [p_set_arn] + + accounts = ssoadmin.list_accounts_for_provisioned_permission_set( + InstanceArn=instance_arn, PermissionSetArn=p_set_arn + )["AccountIds"] + assert accounts == [DEFAULT_ACCOUNT_ID]