Repository: getmoto/moto
Pull request #7727 (2024-05-28 19:46:30)

SSO-Admin instances
New methods:
 - list_accounts_for_provisioned_permission_set()
 - list_instances()
 - list_permission_sets_provisioned_to_account()
 - update_instance()

The `list_instances`-call returns a default SSO instance that is created on start-up.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/7727?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
Attention: Patch coverage is `98.50746%` with `1 lines` in your changes are missing coverage. Please review.
> Project coverage is 94.33%. Comparing base [(`3300d99`)](https://app.codecov.io/gh/getmoto/moto/commit/3300d9998e7ae96f754ea1361e9039c46d75b6ef?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`c31ea5c`)](https://app.codecov.io/gh/getmoto/moto/commit/c31ea5cc73818763d3075a29dd591d4571369971?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).
> Report is 1 commits behind head on master.

| [Files](https://app.codecov.io/gh/getmoto/moto/pull/7727?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Patch % | Lines |
|---|---|---|
| [moto/ssoadmin/models.py](https://app.codecov.io/gh/getmoto/moto/pull/7727?src=pr&el=tree&filepath=moto%2Fssoadmin%2Fmodels.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#diff-bW90by9zc29hZG1pbi9tb2RlbHMucHk=) | 97.61% | [1 Missing :warning: ](https://app.codecov.io/gh/getmoto/moto/pull/7727?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) |

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master    #7727   +/-   ##
=======================================
  Coverage   94.33%   94.33%           
=======================================
  Files        1092     1092           
  Lines       93039    93104   +65     
=======================================
+ Hits        87770    87834   +64     
- Misses       5269     5270    +1     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/7727/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [servertests](https://app.codecov.io/gh/getmoto/moto/pull/7727/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `29.54% <37.31%> (+<0.01%)` | :arrow_up: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/7727/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `94.30% <98.50%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#carryforward-flags-in-the-pull-request-comment) to find out more.


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/getmoto/moto/pull/7727?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).


Patch:
diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index f006f0303..91b036f48 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -7891,7 +7891,7 @@
 
 ## sso-admin
 <details>
-<summary>27% implemented</summary>
+<summary>34% implemented</summary>
 
 - [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 600e7c99b..66b86e2d6 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/applicationautoscaling/models.py b/moto/applicationautoscaling/models.py
index d3d2a1c64..426069f8c 100644
--- a/moto/applicationautoscaling/models.py
+++ b/moto/applicationautoscaling/models.py
@@ -673,4 +673,6 @@ class FakeScheduledAction(BaseModel):
         self.end_time = end_time
 
 
-applicationautoscaling_backends = BackendDict(ApplicationAutoscalingBackend, "ec2")
+applicationautoscaling_backends = BackendDict(
+    ApplicationAutoscalingBackend, "application-autoscaling"
+)
diff --git a/moto/elb/models.py b/moto/elb/models.py
index 330bcc6b5..83dc01c3a 100644
--- a/moto/elb/models.py
+++ b/moto/elb/models.py
@@ -665,5 +665,4 @@ class ELBBackend(BaseBackend):
         return load_balancer.subnets
 
 
-# Use the same regions as EC2
-elb_backends = BackendDict(ELBBackend, "ec2")
+elb_backends = BackendDict(ELBBackend, "elb")
diff --git a/moto/elbv2/models.py b/moto/elbv2/models.py
index e7081d726..7dd4d4d75 100644
--- a/moto/elbv2/models.py
+++ b/moto/elbv2/models.py
@@ -1979,4 +1979,4 @@ Member must satisfy regular expression pattern: {expression}"
         return resource
 
 
-elbv2_backends = BackendDict(ELBv2Backend, "ec2")
+elbv2_backends = BackendDict(ELBv2Backend, "elbv2")
diff --git a/moto/iotdata/models.py b/moto/iotdata/models.py
index 8f25f13f0..880466538 100644
--- a/moto/iotdata/models.py
+++ b/moto/iotdata/models.py
@@ -221,4 +221,4 @@ class IoTDataPlaneBackend(BaseBackend):
         ]
 
 
-iotdata_backends = BackendDict(IoTDataPlaneBackend, "iot")
+iotdata_backends = BackendDict(IoTDataPlaneBackend, "iot-data")
diff --git a/moto/opsworks/models.py b/moto/opsworks/models.py
index 1add8b275..a23ade1d3 100644
--- a/moto/opsworks/models.py
+++ b/moto/opsworks/models.py
@@ -630,4 +630,4 @@ class OpsWorksBackend(BaseBackend):
         self.instances[instance_id].start()
 
 
-opsworks_backends = BackendDict(OpsWorksBackend, "ec2")
+opsworks_backends = BackendDict(OpsWorksBackend, "opsworks")
diff --git a/moto/signer/models.py b/moto/signer/models.py
index 2c46b07bd..dced5f59e 100644
--- a/moto/signer/models.py
+++ b/moto/signer/models.py
@@ -212,6 +212,4 @@ class SignerBackend(BaseBackend):
         self.tagger.untag_resource_using_names(resource_arn, tag_keys)
 
 
-# Using the lambda-regions
-# boto3.Session().get_available_regions("signer") still returns an empty list
-signer_backends = BackendDict(SignerBackend, "lambda")
+signer_backends = BackendDict(SignerBackend, "signer")
diff --git a/moto/ssoadmin/models.py b/moto/ssoadmin/models.py
index 8e75b0dcc..c28187d03 100644
--- a/moto/ssoadmin/models.py
+++ b/moto/ssoadmin/models.py
@@ -7,6 +7,7 @@ from moto.core.utils import unix_time
 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 @@ class PermissionSet(BaseModel):
 
     @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 @@ class CustomerManagedPolicy(BaseModel):
         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 @@ class SSOAdminBackend(BaseBackend):
         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 @@ class SSOAdminBackend(BaseBackend):
             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 @@ class SSOAdminBackend(BaseBackend):
 
         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 fab5b64d2..533e23c5c 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 @@ class SSOAdminResponse(BaseResponse):
         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})


Test patch:
diff --git a/tests/test_ssoadmin/test_ssoadmin_instances.py b/tests/test_ssoadmin/test_ssoadmin_instances.py
new file mode 100644
index 000000000..ab18c140f
--- /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 000000000..e1b257944
--- /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]