noqdev__iambic METASEP

docs/schemas/variable.md METASEP
# Variable

## Properties

- **`key`** *(string)*
- **`value`** *(string)*

docs/schemas/role_template.md METASEP
# RoleTemplate

## Properties

- **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
- **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
  - **Any of**
    - *boolean*
    - *array*
      - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
- **`template_type`** *(string)*: Default: `"NOQ::AWS::IAM::Role"`.
- **`file_path`** *(string)*
- **`read_only`** *(boolean)*: If set to True, iambic will only log drift instead of apply changes when drift is detected. Default: `false`.
- **`role_name`** *(string)*: Name of the role.
- **`description`**: Description of the role. Default: `""`.
  - **Any of**
    - *string*
    - *array*
      - **Items**: Refer to *[#/definitions/Description](#definitions/Description)*.
- **`owner`** *(string)*
- **`max_session_duration`**: Default: `3600`.
  - **Any of**
    - *integer*
    - *array*
      - **Items**: Refer to *[#/definitions/MaxSessionDuration](#definitions/MaxSessionDuration)*.
- **`path`**: Default: `"/"`.
  - **Any of**
    - *string*
    - *array*
      - **Items**: Refer to *[#/definitions/Path](#definitions/Path)*.
- **`permissions_boundary`**
  - **Any of**
    - : Refer to *[#/definitions/PermissionBoundary](#definitions/PermissionBoundary)*.
    - *array*
      - **Items**: Refer to *[#/definitions/PermissionBoundary](#definitions/PermissionBoundary)*.
- **`role_access`** *(array)*: List of users and groups who can assume into the role. Default: `[]`.
  - **Items**: Refer to *[#/definitions/RoleAccess](#definitions/RoleAccess)*.
- **`assume_role_policy_document`**
  - **Any of**
    - : Refer to *[#/definitions/AssumeRolePolicyDocument](#definitions/AssumeRolePolicyDocument)*.
    - *array*
      - **Items**: Refer to *[#/definitions/AssumeRolePolicyDocument](#definitions/AssumeRolePolicyDocument)*.
- **`tags`** *(array)*: List of tags attached to the role. Default: `[]`.
  - **Items**: Refer to *[#/definitions/Tag](#definitions/Tag)*.
- **`managed_policies`** *(array)*: Managed policy arns attached to the role. Default: `[]`.
  - **Items**: Refer to *[#/definitions/ManagedPolicyRef](#definitions/ManagedPolicyRef)*.
- **`inline_policies`** *(array)*: List of the role's inline policies. Default: `[]`.
  - **Items**: Refer to *[#/definitions/PolicyDocument](#definitions/PolicyDocument)*.
## Definitions

- <a id="definitions/Deleted"></a>**`Deleted`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`deleted`** *(boolean)*: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran.
- <a id="definitions/Description"></a>**`Description`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`description`** *(string)*: Default: `""`.
- <a id="definitions/MaxSessionDuration"></a>**`MaxSessionDuration`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`max_session_duration`** *(integer)*
- <a id="definitions/Path"></a>**`Path`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`file_path`** *(string)*
- <a id="definitions/PermissionBoundary"></a>**`PermissionBoundary`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
  - **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
    - **Any of**
      - *boolean*
      - *array*
        - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
  - **`permissions_boundary_type`** *(string)*
  - **`permissions_boundary_arn`** *(string)*
- <a id="definitions/RoleAccess"></a>**`RoleAccess`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
  - **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
    - **Any of**
      - *boolean*
      - *array*
        - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
  - **`users`** *(array)*: List of users who can assume into the role. Default: `[]`.
    - **Items** *(string)*
  - **`groups`** *(array)*: List of groups. Users in one or more of the groups can assume into the role. Default: `[]`.
    - **Items** *(string)*
- <a id="definitions/Principal"></a>**`Principal`** *(object)*
  - **`aws`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
  - **`service`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
  - **`canonical_user`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
  - **`federated`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
- <a id="definitions/PolicyStatement"></a>**`PolicyStatement`** *(object)*
  - **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
  - **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
    - **Any of**
      - *boolean*
      - *array*
        - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`effect`** *(string)*: Allow | Deny.
  - **`principal`**
    - **Any of**
      - : Refer to *[#/definitions/Principal](#definitions/Principal)*.
      - *string*
  - **`not_principal`**
    - **Any of**
      - : Refer to *[#/definitions/Principal](#definitions/Principal)*.
      - *string*
  - **`action`**: A single regex or list of regexes. Values are the actions that can be performed on the resources in the policy statement.
    - **Any of**
      - *array*
        - **Items** *(string)*
      - *string*
  - **`not_action`**: An advanced policy element that explicitly matches everything except the specified list of actions.DON'T use this with effect: allow in the same statement OR policy.
    - **Any of**
      - *array*
        - **Items** *(string)*
      - *string*
  - **`resource`**: A single regex or list of regexes. Values specified are the resources the statement applies to.
    - **Any of**
      - *array*
        - **Items** *(string)*
      - *string*
  - **`not_resource`**: An advanced policy element that explicitly matches every resource except those specified.DON'T use this with effect: allow and action: '*'.
    - **Any of**
      - *array*
        - **Items** *(string)*
      - *string*
  - **`condition`** *(object)*: An optional set of conditions to determine of the policy applies to a resource.
  - **`sid`** *(string)*: The Policy Statement ID.
- <a id="definitions/AssumeRolePolicyDocument"></a>**`AssumeRolePolicyDocument`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`version`** *(string)*
  - **`statement`** *(array)*
    - **Items**: Refer to *[#/definitions/PolicyStatement](#definitions/PolicyStatement)*.
- <a id="definitions/Tag"></a>**`Tag`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
  - **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
    - **Any of**
      - *boolean*
      - *array*
        - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
  - **`key`** *(string)*
  - **`value`** *(string)*
- <a id="definitions/ManagedPolicyRef"></a>**`ManagedPolicyRef`** *(object)*
  - **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
  - **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
    - **Any of**
      - *boolean*
      - *array*
        - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`policy_arn`** *(string)*
- <a id="definitions/PolicyDocument"></a>**`PolicyDocument`** *(object)*
  - **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
  - **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
    - **Any of**
      - *boolean*
      - *array*
        - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`policy_name`** *(string)*: The name of the policy.
  - **`version`** *(string)*
  - **`statement`** *(array)*: List of policy statements.
    - **Items**: Refer to *[#/definitions/PolicyStatement](#definitions/PolicyStatement)*.

docs/schemas/role_access.md METASEP
# RoleAccess

## Properties

- **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
- **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
  - **Any of**
    - *boolean*
    - *array*
      - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
- **`users`** *(array)*: List of users who can assume into the role. Default: `[]`.
  - **Items** *(string)*
- **`groups`** *(array)*: List of groups. Users in one or more of the groups can assume into the role. Default: `[]`.
  - **Items** *(string)*
## Definitions

- <a id="definitions/Deleted"></a>**`Deleted`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`deleted`** *(boolean)*: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran.

docs/schemas/policy_statement.md METASEP
# PolicyStatement

## Properties

- **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
- **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
  - **Any of**
    - *boolean*
    - *array*
      - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
- **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`effect`** *(string)*: Allow | Deny.
- **`principal`**
  - **Any of**
    - : Refer to *[#/definitions/Principal](#definitions/Principal)*.
    - *string*
- **`not_principal`**
  - **Any of**
    - : Refer to *[#/definitions/Principal](#definitions/Principal)*.
    - *string*
- **`action`**: A single regex or list of regexes. Values are the actions that can be performed on the resources in the policy statement.
  - **Any of**
    - *array*
      - **Items** *(string)*
    - *string*
- **`not_action`**: An advanced policy element that explicitly matches everything except the specified list of actions.DON'T use this with effect: allow in the same statement OR policy.
  - **Any of**
    - *array*
      - **Items** *(string)*
    - *string*
- **`resource`**: A single regex or list of regexes. Values specified are the resources the statement applies to.
  - **Any of**
    - *array*
      - **Items** *(string)*
    - *string*
- **`not_resource`**: An advanced policy element that explicitly matches every resource except those specified.DON'T use this with effect: allow and action: '*'.
  - **Any of**
    - *array*
      - **Items** *(string)*
    - *string*
- **`condition`** *(object)*: An optional set of conditions to determine of the policy applies to a resource.
- **`sid`** *(string)*: The Policy Statement ID.
## Definitions

- <a id="definitions/Deleted"></a>**`Deleted`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`deleted`** *(boolean)*: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran.
- <a id="definitions/Principal"></a>**`Principal`** *(object)*
  - **`aws`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
  - **`service`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
  - **`canonical_user`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
  - **`federated`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*

docs/schemas/policy_document.md METASEP
# PolicyDocument

## Properties

- **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
- **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
  - **Any of**
    - *boolean*
    - *array*
      - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
- **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`policy_name`** *(string)*: The name of the policy.
- **`version`** *(string)*
- **`statement`** *(array)*: List of policy statements.
  - **Items**: Refer to *[#/definitions/PolicyStatement](#definitions/PolicyStatement)*.
## Definitions

- <a id="definitions/Deleted"></a>**`Deleted`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`deleted`** *(boolean)*: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran.
- <a id="definitions/Principal"></a>**`Principal`** *(object)*
  - **`aws`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
  - **`service`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
  - **`canonical_user`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
  - **`federated`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
- <a id="definitions/PolicyStatement"></a>**`PolicyStatement`** *(object)*
  - **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
  - **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
    - **Any of**
      - *boolean*
      - *array*
        - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`effect`** *(string)*: Allow | Deny.
  - **`principal`**
    - **Any of**
      - : Refer to *[#/definitions/Principal](#definitions/Principal)*.
      - *string*
  - **`not_principal`**
    - **Any of**
      - : Refer to *[#/definitions/Principal](#definitions/Principal)*.
      - *string*
  - **`action`**: A single regex or list of regexes. Values are the actions that can be performed on the resources in the policy statement.
    - **Any of**
      - *array*
        - **Items** *(string)*
      - *string*
  - **`not_action`**: An advanced policy element that explicitly matches everything except the specified list of actions.DON'T use this with effect: allow in the same statement OR policy.
    - **Any of**
      - *array*
        - **Items** *(string)*
      - *string*
  - **`resource`**: A single regex or list of regexes. Values specified are the resources the statement applies to.
    - **Any of**
      - *array*
        - **Items** *(string)*
      - *string*
  - **`not_resource`**: An advanced policy element that explicitly matches every resource except those specified.DON'T use this with effect: allow and action: '*'.
    - **Any of**
      - *array*
        - **Items** *(string)*
      - *string*
  - **`condition`** *(object)*: An optional set of conditions to determine of the policy applies to a resource.
  - **`sid`** *(string)*: The Policy Statement ID.

docs/schemas/permission_boundary.md METASEP
# PermissionBoundary

## Properties

- **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
- **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
  - **Any of**
    - *boolean*
    - *array*
      - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
- **`permissions_boundary_type`** *(string)*
- **`permissions_boundary_arn`** *(string)*
## Definitions

- <a id="definitions/Deleted"></a>**`Deleted`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`deleted`** *(boolean)*: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran.

docs/schemas/path.md METASEP
# Path

## Properties

- **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`file_path`** *(string)*

docs/schemas/max_session_duration.md METASEP
# MaxSessionDuration

## Properties

- **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`max_session_duration`** *(integer)*

docs/schemas/managed_policy_ref.md METASEP
# ManagedPolicyRef

## Properties

- **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
- **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
  - **Any of**
    - *boolean*
    - *array*
      - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
- **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`policy_arn`** *(string)*
## Definitions

- <a id="definitions/Deleted"></a>**`Deleted`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`deleted`** *(boolean)*: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran.

docs/schemas/group_template.md METASEP
# GroupTemplate

## Properties

- **`template_type`** *(string)*: Default: `"NOQ::Google::GroupTemplate"`.
- **`file_path`** *(string)*
- **`read_only`** *(boolean)*: If set to True, iambic will only log drift instead of apply changes when drift is detected. Default: `false`.
- **`name`** *(string)*
- **`email`** *(string)*
- **`description`** *(string)*
- **`welcome_message`** *(string)*
- **`members`** *(array)*
  - **Items**: Refer to *[#/definitions/GroupMember](#definitions/GroupMember)*.
- **`who_can_invite`**: Default: `"ALL_MANAGERS_CAN_INVITE"`.
  - **All of**
    - : Refer to *[#/definitions/WhoCanInvite](#definitions/WhoCanInvite)*.
- **`who_can_join`**: Default: `"CAN_REQUEST_TO_JOIN"`.
  - **All of**
    - : Refer to *[#/definitions/WhoCanJoin](#definitions/WhoCanJoin)*.
- **`who_can_post_message`**: Default: `"NONE_CAN_POST"`.
  - **All of**
    - : Refer to *[#/definitions/WhoCanPostMessage](#definitions/WhoCanPostMessage)*.
- **`who_can_view_group`**: Default: `"ALL_MANAGERS_CAN_VIEW"`.
  - **All of**
    - : Refer to *[#/definitions/WhoCanViewGroup](#definitions/WhoCanViewGroup)*.
- **`who_can_view_membership`**: Default: `"ALL_MANAGERS_CAN_VIEW"`.
  - **All of**
    - : Refer to *[#/definitions/WhoCanViewMembership](#definitions/WhoCanViewMembership)*.
## Definitions

- <a id="definitions/Deleted"></a>**`Deleted`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`deleted`** *(boolean)*: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran.
- <a id="definitions/GroupMemberRole"></a>**`GroupMemberRole`**: An enumeration. Must be one of: `["OWNER", "MANAGER", "MEMBER"]`.
- <a id="definitions/GroupMemberType"></a>**`GroupMemberType`**: An enumeration. Must be one of: `["USER", "GROUP", "EXTERNAL"]`.
- <a id="definitions/GroupMemberStatus"></a>**`GroupMemberStatus`**: An enumeration. Must be one of: `["ACTIVE", "INACTIVE", "PENDING"]`.
- <a id="definitions/GroupMemberSubscription"></a>**`GroupMemberSubscription`**: An enumeration. Must be one of: `["EACH_EMAIL", "DIGEST", "ABRIDGED", "NO_EMAIL"]`.
- <a id="definitions/GroupMember"></a>**`GroupMember`** *(object)*
  - **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
  - **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
    - **Any of**
      - *boolean*
      - *array*
        - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
  - **`email`** *(string)*
  - **`expand`** *(boolean)*: Expand the group into the members of the group. This is useful for nested groups. Default: `false`.
  - **`role`**: Default: `"MEMBER"`.
    - **All of**
      - : Refer to *[#/definitions/GroupMemberRole](#definitions/GroupMemberRole)*.
  - **`type`**: Default: `"USER"`.
    - **All of**
      - : Refer to *[#/definitions/GroupMemberType](#definitions/GroupMemberType)*.
  - **`status`**: Default: `"ACTIVE"`.
    - **All of**
      - : Refer to *[#/definitions/GroupMemberStatus](#definitions/GroupMemberStatus)*.
  - **`subscription`**: Default: `"EACH_EMAIL"`.
    - **All of**
      - : Refer to *[#/definitions/GroupMemberSubscription](#definitions/GroupMemberSubscription)*.
- <a id="definitions/WhoCanInvite"></a>**`WhoCanInvite`**: An enumeration. Must be one of: `["ALL_MANAGERS_CAN_INVITE", "ALL_MEMBERS_CAN_INVITE"]`.
- <a id="definitions/WhoCanJoin"></a>**`WhoCanJoin`**: An enumeration. Must be one of: `["ALL_IN_DOMAIN_CAN_JOIN", "ANYONE_CAN_JOIN", "CAN_REQUEST_TO_JOIN"]`.
- <a id="definitions/WhoCanPostMessage"></a>**`WhoCanPostMessage`**: An enumeration. Must be one of: `["ALL_IN_DOMAIN_CAN_POST", "ALL_MANAGERS_CAN_POST", "ALL_MEMBERS_CAN_POST", "ANYONE_CAN_POST", "NONE_CAN_POST"]`.
- <a id="definitions/WhoCanViewGroup"></a>**`WhoCanViewGroup`**: An enumeration. Must be one of: `["ALL_IN_DOMAIN_CAN_VIEW", "ALL_MANAGERS_CAN_VIEW", "ALL_MEMBERS_CAN_VIEW", "ANYONE_CAN_VIEW"]`.
- <a id="definitions/WhoCanViewMembership"></a>**`WhoCanViewMembership`**: An enumeration. Must be one of: `["ALL_IN_DOMAIN_CAN_VIEW", "ALL_MANAGERS_CAN_VIEW", "ALL_MEMBERS_CAN_VIEW", "ANYONE_CAN_VIEW"]`.

docs/schemas/group_member.md METASEP
# GroupMember

## Properties

- **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
- **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
  - **Any of**
    - *boolean*
    - *array*
      - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
- **`email`** *(string)*
- **`expand`** *(boolean)*: Expand the group into the members of the group. This is useful for nested groups. Default: `false`.
- **`role`**: Default: `"MEMBER"`.
  - **All of**
    - : Refer to *[#/definitions/GroupMemberRole](#definitions/GroupMemberRole)*.
- **`type`**: Default: `"USER"`.
  - **All of**
    - : Refer to *[#/definitions/GroupMemberType](#definitions/GroupMemberType)*.
- **`status`**: Default: `"ACTIVE"`.
  - **All of**
    - : Refer to *[#/definitions/GroupMemberStatus](#definitions/GroupMemberStatus)*.
- **`subscription`**: Default: `"EACH_EMAIL"`.
  - **All of**
    - : Refer to *[#/definitions/GroupMemberSubscription](#definitions/GroupMemberSubscription)*.
## Definitions

- <a id="definitions/Deleted"></a>**`Deleted`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`deleted`** *(boolean)*: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran.
- <a id="definitions/GroupMemberRole"></a>**`GroupMemberRole`**: An enumeration. Must be one of: `["OWNER", "MANAGER", "MEMBER"]`.
- <a id="definitions/GroupMemberType"></a>**`GroupMemberType`**: An enumeration. Must be one of: `["USER", "GROUP", "EXTERNAL"]`.
- <a id="definitions/GroupMemberStatus"></a>**`GroupMemberStatus`**: An enumeration. Must be one of: `["ACTIVE", "INACTIVE", "PENDING"]`.
- <a id="definitions/GroupMemberSubscription"></a>**`GroupMemberSubscription`**: An enumeration. Must be one of: `["EACH_EMAIL", "DIGEST", "ABRIDGED", "NO_EMAIL"]`.

docs/schemas/group.md METASEP
# Group

## Properties

- **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
- **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
  - **Any of**
    - *boolean*
    - *array*
      - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
- **`template_type`** *(string)*: Default: `"NOQ::Google::Group"`.
- **`file_path`** *(string)*
- **`read_only`** *(boolean)*: If set to True, iambic will only log drift instead of apply changes when drift is detected. Default: `false`.
- **`name`** *(string)*
- **`email`** *(string)*
- **`description`** *(string)*
- **`welcome_message`** *(string)*
- **`members`** *(array)*
  - **Items**: Refer to *[#/definitions/GroupMember](#definitions/GroupMember)*.
- **`who_can_invite`**: Default: `"ALL_MANAGERS_CAN_INVITE"`.
  - **All of**
    - : Refer to *[#/definitions/WhoCanInvite](#definitions/WhoCanInvite)*.
- **`who_can_join`**: Default: `"CAN_REQUEST_TO_JOIN"`.
  - **All of**
    - : Refer to *[#/definitions/WhoCanJoin](#definitions/WhoCanJoin)*.
- **`who_can_post_message`**: Default: `"NONE_CAN_POST"`.
  - **All of**
    - : Refer to *[#/definitions/WhoCanPostMessage](#definitions/WhoCanPostMessage)*.
- **`who_can_view_group`**: Default: `"ALL_MANAGERS_CAN_VIEW"`.
  - **All of**
    - : Refer to *[#/definitions/WhoCanViewGroup](#definitions/WhoCanViewGroup)*.
- **`who_can_view_membership`**: Default: `"ALL_MANAGERS_CAN_VIEW"`.
  - **All of**
    - : Refer to *[#/definitions/WhoCanViewMembership](#definitions/WhoCanViewMembership)*.
## Definitions

- <a id="definitions/Deleted"></a>**`Deleted`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`deleted`** *(boolean)*: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran.
- <a id="definitions/GroupMemberRole"></a>**`GroupMemberRole`**: An enumeration. Must be one of: `["OWNER", "MANAGER", "MEMBER"]`.
- <a id="definitions/GroupMemberType"></a>**`GroupMemberType`**: An enumeration. Must be one of: `["USER", "GROUP", "EXTERNAL"]`.
- <a id="definitions/GroupMemberStatus"></a>**`GroupMemberStatus`**: An enumeration. Must be one of: `["ACTIVE", "INACTIVE", "PENDING"]`.
- <a id="definitions/GroupMemberSubscription"></a>**`GroupMemberSubscription`**: An enumeration. Must be one of: `["EACH_EMAIL", "DIGEST", "ABRIDGED", "NO_EMAIL"]`.
- <a id="definitions/GroupMember"></a>**`GroupMember`** *(object)*
  - **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
  - **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
    - **Any of**
      - *boolean*
      - *array*
        - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
  - **`email`** *(string)*
  - **`expand`** *(boolean)*: Expand the group into the members of the group. This is useful for nested groups. Default: `false`.
  - **`role`**: Default: `"MEMBER"`.
    - **All of**
      - : Refer to *[#/definitions/GroupMemberRole](#definitions/GroupMemberRole)*.
  - **`type`**: Default: `"USER"`.
    - **All of**
      - : Refer to *[#/definitions/GroupMemberType](#definitions/GroupMemberType)*.
  - **`status`**: Default: `"ACTIVE"`.
    - **All of**
      - : Refer to *[#/definitions/GroupMemberStatus](#definitions/GroupMemberStatus)*.
  - **`subscription`**: Default: `"EACH_EMAIL"`.
    - **All of**
      - : Refer to *[#/definitions/GroupMemberSubscription](#definitions/GroupMemberSubscription)*.
- <a id="definitions/WhoCanInvite"></a>**`WhoCanInvite`**: An enumeration. Must be one of: `["ALL_MANAGERS_CAN_INVITE", "ALL_MEMBERS_CAN_INVITE"]`.
- <a id="definitions/WhoCanJoin"></a>**`WhoCanJoin`**: An enumeration. Must be one of: `["ALL_IN_DOMAIN_CAN_JOIN", "ANYONE_CAN_JOIN", "CAN_REQUEST_TO_JOIN"]`.
- <a id="definitions/WhoCanPostMessage"></a>**`WhoCanPostMessage`**: An enumeration. Must be one of: `["ALL_IN_DOMAIN_CAN_POST", "ALL_MANAGERS_CAN_POST", "ALL_MEMBERS_CAN_POST", "ANYONE_CAN_POST", "NONE_CAN_POST"]`.
- <a id="definitions/WhoCanViewGroup"></a>**`WhoCanViewGroup`**: An enumeration. Must be one of: `["ALL_IN_DOMAIN_CAN_VIEW", "ALL_MANAGERS_CAN_VIEW", "ALL_MEMBERS_CAN_VIEW", "ANYONE_CAN_VIEW"]`.
- <a id="definitions/WhoCanViewMembership"></a>**`WhoCanViewMembership`**: An enumeration. Must be one of: `["ALL_IN_DOMAIN_CAN_VIEW", "ALL_MANAGERS_CAN_VIEW", "ALL_MEMBERS_CAN_VIEW", "ANYONE_CAN_VIEW"]`.

docs/schemas/extends_config.md METASEP
# ExtendsConfig

## Properties

- **`key`**: Refer to *[#/definitions/ExtendsConfigKey](#definitions/ExtendsConfigKey)*.
- **`value`** *(string)*
## Definitions

- <a id="definitions/ExtendsConfigKey"></a>**`ExtendsConfigKey`**: An enumeration. Must be one of: `["AWS_SECRETS_MANAGER"]`.

docs/schemas/config.md METASEP
# Config

## Properties

- **`aws_accounts`** *(array)*
  - **Items**: Refer to *[#/definitions/AWSAccount](#definitions/AWSAccount)*.
- **`google`**: Refer to *[#/definitions/GoogleConfig](#definitions/GoogleConfig)*.
- **`extends`** *(array)*: Default: `[]`.
  - **Items**: Refer to *[#/definitions/ExtendsConfig](#definitions/ExtendsConfig)*.
- **`secrets`** *(object)*
- **`role_access_tag`** *(string)*: The key of the tag used to store users and groups that can assume into the role the tag is on. Default: `"noq-authorized"`.
- **`variables`** *(array)*: A list of variables to be used when creating templates. These apply to all aws_accounts but can be overwritten by an account. Default: `[]`.
  - **Items**: Refer to *[#/definitions/Variable](#definitions/Variable)*.
- **`slack_app`** *(string)*
- **`sqs`** *(object)*: Default: `{}`.
- **`slack`** *(object)*: Default: `{}`.
## Definitions

- <a id="definitions/RegionName"></a>**`RegionName`**: An enumeration. Must be one of: `["us-east-1", "us-west-1", "us-west-2", "eu-west-1", "eu-west-2", "eu-central-1", "ap-southeast-1", "ap-southeast-2", "ap-northeast-1", "ap-northeast-2", "sa-east-1", "cn-north-1"]`.
- <a id="definitions/Variable"></a>**`Variable`** *(object)*
  - **`key`** *(string)*
  - **`value`** *(string)*
- <a id="definitions/AWSAccount"></a>**`AWSAccount`** *(object)*
  - **`account_id`** *(string)*: The AWS Account ID.
  - **`org_id`** *(string)*: A unique identifier designating the identity of the organization.
  - **`account_name`** *(string)*
  - **`default_region`**: Default region to use when making AWS requests. Default: `"us-east-1"`.
    - **All of**
      - : Refer to *[#/definitions/RegionName](#definitions/RegionName)*.
  - **`aws_profile`** *(string)*: The AWS profile used when making calls to the account.
  - **`assume_role_arn`** *(string)*: The role arn to assume into when making calls to the account.
  - **`external_id`** *(string)*: The external id to use for assuming into a role when making calls to the account.
  - **`role_access_tag`** *(string)*: The key of the tag used to store users and groups that can assume into the role the tag is on.
  - **`variables`** *(array)*: A list of variables to be used when creating templates. Default: `[]`.
    - **Items**: Refer to *[#/definitions/Variable](#definitions/Variable)*.
  - **`boto3_session_map`** *(object)*
  - **`read_only`** *(boolean)*: If set to True, iambic will only log drift instead of apply changes when drift is detected. Default: `false`.
- <a id="definitions/GoogleGroupsConfig"></a>**`GoogleGroupsConfig`** *(object)*
  - **`enabled`** *(boolean)*: Default: `false`.
- <a id="definitions/GoogleConfig"></a>**`GoogleConfig`** *(object)*
  - **`groups`**: Refer to *[#/definitions/GoogleGroupsConfig](#definitions/GoogleGroupsConfig)*.
- <a id="definitions/ExtendsConfigKey"></a>**`ExtendsConfigKey`**: An enumeration. Must be one of: `["AWS_SECRETS_MANAGER"]`.
- <a id="definitions/ExtendsConfig"></a>**`ExtendsConfig`** *(object)*
  - **`key`**: Refer to *[#/definitions/ExtendsConfigKey](#definitions/ExtendsConfigKey)*.
  - **`value`** *(string)*

docs/schemas/awsaccount.md METASEP
# AWSAccount

## Properties

- **`account_id`** *(string)*: The AWS Account ID.
- **`org_id`** *(string)*: A unique identifier designating the identity of the organization.
- **`account_name`** *(string)*
- **`default_region`**: Default region to use when making AWS requests. Default: `"us-east-1"`.
  - **All of**
    - : Refer to *[#/definitions/RegionName](#definitions/RegionName)*.
- **`aws_profile`** *(string)*: The AWS profile used when making calls to the account.
- **`assume_role_arn`** *(string)*: The role arn to assume into when making calls to the account.
- **`external_id`** *(string)*: The external id to use for assuming into a role when making calls to the account.
- **`role_access_tag`** *(string)*: The key of the tag used to store users and groups that can assume into the role the tag is on.
- **`variables`** *(array)*: A list of variables to be used when creating templates. Default: `[]`.
  - **Items**: Refer to *[#/definitions/Variable](#definitions/Variable)*.
- **`boto3_session_map`** *(object)*
- **`read_only`** *(boolean)*: If set to True, iambic will only log drift instead of apply changes when drift is detected. Default: `false`.
## Definitions

- <a id="definitions/RegionName"></a>**`RegionName`**: An enumeration. Must be one of: `["us-east-1", "us-west-1", "us-west-2", "eu-west-1", "eu-west-2", "eu-central-1", "ap-southeast-1", "ap-southeast-2", "ap-northeast-1", "ap-northeast-2", "sa-east-1", "cn-north-1"]`.
- <a id="definitions/Variable"></a>**`Variable`** *(object)*
  - **`key`** *(string)*
  - **`value`** *(string)*

docs/schemas/assume_role_policy_document.md METASEP
# AssumeRolePolicyDocument

## Properties

- **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`version`** *(string)*
- **`statement`** *(array)*
  - **Items**: Refer to *[#/definitions/PolicyStatement](#definitions/PolicyStatement)*.
## Definitions

- <a id="definitions/Deleted"></a>**`Deleted`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`deleted`** *(boolean)*: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran.
- <a id="definitions/Principal"></a>**`Principal`** *(object)*
  - **`aws`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
  - **`service`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
  - **`canonical_user`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
  - **`federated`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
- <a id="definitions/PolicyStatement"></a>**`PolicyStatement`** *(object)*
  - **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
  - **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
    - **Any of**
      - *boolean*
      - *array*
        - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`effect`** *(string)*: Allow | Deny.
  - **`principal`**
    - **Any of**
      - : Refer to *[#/definitions/Principal](#definitions/Principal)*.
      - *string*
  - **`not_principal`**
    - **Any of**
      - : Refer to *[#/definitions/Principal](#definitions/Principal)*.
      - *string*
  - **`action`**: A single regex or list of regexes. Values are the actions that can be performed on the resources in the policy statement.
    - **Any of**
      - *array*
        - **Items** *(string)*
      - *string*
  - **`not_action`**: An advanced policy element that explicitly matches everything except the specified list of actions.DON'T use this with effect: allow in the same statement OR policy.
    - **Any of**
      - *array*
        - **Items** *(string)*
      - *string*
  - **`resource`**: A single regex or list of regexes. Values specified are the resources the statement applies to.
    - **Any of**
      - *array*
        - **Items** *(string)*
      - *string*
  - **`not_resource`**: An advanced policy element that explicitly matches every resource except those specified.DON'T use this with effect: allow and action: '*'.
    - **Any of**
      - *array*
        - **Items** *(string)*
      - *string*
  - **`condition`** *(object)*: An optional set of conditions to determine of the policy applies to a resource.
  - **`sid`** *(string)*: The Policy Statement ID.

test/aws/test_iam_roles.py METASEP

test/aws/factories.py METASEP
from pydantic_factories import Ignore, ModelFactory

from iambic.aws.iam.policy import models as policy_models
from iambic.aws.iam.role.models import RoleTemplate


class AssumeRolePolicyDocumentFactory(ModelFactory):
    __model__ = policy_models.AssumeRolePolicyDocument


class PolicyDocumentFactory(ModelFactory):
    __model__ = policy_models.PolicyDocument


class RoleFactory(ModelFactory):
    __model__ = RoleTemplate

    deleted = Ignore()
    included_orgs = Ignore()
    excluded_orgs = Ignore()

test/aws/__init__.py METASEP

docs/SCHEMA.md METASEP
# AWS Template Models
* [RoleTemplate](schemas/role_template.md)
* [AssumeRolePolicyDocument](schemas/assume_role_policy_document.md)
* [ManagedPolicyRef](schemas/managed_policy_ref.md)
* [PolicyDocument](schemas/policy_document.md)
* [Path](schemas/path.md)
* [PermissionBoundary](schemas/permission_boundary.md)
* [MaxSessionDuration](schemas/max_session_duration.md)
* [RoleAccess](schemas/role_access.md)
* [PolicyStatement](schemas/policy_statement.md)

# Google Template Models
* [GroupTemplate](schemas/group_template.md)
* [GroupMember](schemas/group_member.md)

# Config Models
* [AWSAccount](schemas/awsaccount.md)
* [Config](schemas/config.md)
* [ExtendsConfig](schemas/extends_config.md)
* [Variable](schemas/variable.md)

docs/DOCKER.md METASEP
# Docker


Iambic ships with a docker container and a docker-compose.yaml file that can be ran either locally, in AWS Lambda, GitHub Actions, and potentially other places.

The container is purpose built to support Python 3.10 in AWS Lambda because at the current time, AWS Lambda does not support Python 3.10 (See: <https://github.com/aws/aws-lambda-base-images/issues/31>). See the Dockerfile for more information.

The Dockerfile ships with [AWSLambdaric](https://docs.aws.amazon.com/lambda/latest/dg/images-test.html), which allows us to test Lambda functions locally. The actual Iambic Lambda Python code is in iambic/lambda/app.py.

This code can import your AWS and Google environment, apply the environment, and perform other actions.

## Next Steps

- We support cloning git repos. We need to add support for accessing these repos for importing, planning, and applying templates. Check out the `clone_git_repos` function for the base functionality.
- Get this container running as a GitHub action on 1) main branch push (apply the changes), 2) schedule (Every 5 minutes) to detect/correct drift
- Support automatically detecting accounts via AWS organizations, and populating roles on these accounts.
- Create a specific role for iambic (And don't re-use Noq SaaS SpokeRole)
- Update Iambic container to be accessible on dockerhub or public ECR.

## Running Locally

### Running without Docker

To test the Iambic code without Docker, simply use the VSCode run configurations that start with `Iambic: Lambda`.

### Running in Docker

To run the container locally, run the following commands:

```bash
# Refresh AWS Credentials with a specific profile
noq file arn:aws:iam::940552945933:role/NoqCentralRoleCorpNoqDev -p arn:aws:iam::940552945933:role/NoqCentralRoleCorpNoqDev -f
# Build the container
docker-compose build

# Start the container
docker-compose up
```

This will run the container and mount the current directory into the container. This allows you to make changes to the code and see the changes reflected in the container.

When it is operational, you can invoke Lambda locally with the following:

```bash
# Import
curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"command": "import"}'
# Detect
curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"command": "detect"}'
```

For a full list of commands, see the `LambdaCommand` enum in iambic/lambda/app.py.

## Running in GitHub Actions

There is a GitHub action for testing the import functionality in `noq-templates/.github/workflows`. This action runs when changes are pushed to the main branch, and attempts to simply import the cloud environment. Right now, this is a no-op operation.

This Github action is rather inefficient because it is building the docker images manually during every run. In reality, it should utilize an image in Public DockerHub or ECR.

## Running in AWS Lambda

TBD


## Deploying to ECR

```bash
# Retrieve development_admin credentials
export AWS_PROFILE=noq_dev
# Login to public ECR
aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws/s2p9s3r8
# (From iambic root directory) build the container
docker build -t iambic .
# Tag and push
docker tag iambic:latest public.ecr.aws/s2p9s3r8/iambic:latest
docker push public.ecr.aws/s2p9s3r8/iambic:latest

docs/CONTRIBUTING.md METASEP
# Contribution Doc


## Writing new templates or models

Each attribute should support a single value or multiple values on the boundary of accounts.
An example of this would be `Role.path`. Notice how path can be a str or list of `Path` instances.
```python
class Path(AccessModel):
    file_path: str


class RoleTemplate(NoqTemplate, AccessModel):
    ...
    path: Optional[Union[str | list[Path]]] = "/"
```

NEVER use dict to represent an attribute unless absolutely necessary due to the attributes dynamic nature.
An example of this would be `PolicyStatement.condition` where key is the condition statement making representing the attribute too difficult. 
 

## Adding a new resource model
This section is applies to any type of model class include `NoqTemplate` classes. 

### Implement `resource_type(self) -> str:`
A unique representation of the resource's type. 
For AWS resources this would be `aws:${service}:${resource}` e.g. aws:iam:role.
There is no strict standard at the time this was written so use your best judgement.
Be sure that it is named in such a way that no model in iambic will ever collide.

### Implement `resource_id(self) -> str:`
The unique value for the resource.  
For a role this would be the role name. For a tag it would be the key. For an inline policy, the policy name.

### Optionally overwrite `exclude_keys(self) -> set` 
Used to specify attributes in a model that are not part of the API request to the related service e.g. AWS/boto3.

This method only extends the following set of attributes which are already ignored:
```python
exclude_keys = {
    "deleted",
    "expires_at",
    "included_accounts",
    "excluded_accounts",
    "included_orgs",
    "excluded_orgs",
    "owner",
    "template_type",
    "file_path",
}
```

## Adding a new Noq template
A Noq template defines 1 or more resources. 
A template will typically only have 1 resource attributed to it. 
For example, a template that defines a managed policy.
This managed policy will be applied to the accounts defined by included/exclude accounts and included/excluded orgs.

However, resources that have a 1-1 or 1-N relationship with another resource will also include those resources in its template.
For example, a role has tags and those tags cannot be attributed to any other resource making them bound to the role.
Therefore, the tags are defined as an attribute of the role. The same applies to inline policies and assume role policies.
Managed policies can be attached to many roles so the managed policy is not an attribute of the role template.

Below are some other guidelines for defining a template class and incorporating a new template into iambic.

### Define `template_type`
This is the string value used to identify a template's type.
The value is unique across all NoqTemplate classes in iambic.
This value MUST be prefixed with `NOQ::`.

### Implement `async def _apply_to_account(self, account_config: AccountConfig) -> bool:`  
This method is responsible for conditionally updating, creating, or deleting the resources the template represents.
For example, the AWS role template also carries with it tag resources, inline policy resources, instances profiles, etc.
This method will not only upsert or delete the role but also these related resources. 

### Add the class to `iambic.config.templates.TEMPLATES`
This list is used for validation and resolving the template type of a yaml file.

### Create an async function to import existing resources
* Add the function to a module called `template_generation.py` that will be in the same package as the template's model.
* Must group resources with identical `resource_id`
* Must group attributes of the resource
* Take into account the jinja representation and the raw value when grouping
* Check if resource is already a template and overwrite that file before creating a new file
* Follow the iambic preferred directory structure of `resources/${provider}/${resource}` e.g. resources/aws/roles
* Add the function to import existing resources to `iambic.request_handler.generate.generate_templates`

#### Use the helper functions
There are a number of functions in `iambic.core.template_generation.py` to simplify a lot of the import process. 

##### `group_int_or_str_attribute(account_config_map: dict[str, AccountConfig], number_of_accounts_resource_on: int, account_resources: Union[dict | list[dict]], key: Union[int | str]) -> Union[int | str | list[dict]]:`
Groups an attribute by accounts, formats the attribute and normalizes the included accounts.

:param account_config_map:
:param number_of_accounts_resource_on:
:param account_resources: list[dict(account_id:str, resources=list[dict])]
:param is_dict_attr: If false and only one hit, still return as a list. Useful for things like inline_policies.

##### `group_dict_attribute(account_config_map: dict[str, AccountConfig], number_of_accounts_resource_on: int, account_resources: list[dict], is_dict_attr: bool = True) -> Union[dict | list[dict]]:`
Groups an attribute by accounts, formats the attribute and normalizes the included accounts.

:param account_config_map: {account_id: account_config}
:param number_of_accounts_resource_on:
:param account_resources: list[dict(account_id:str, resources=list[dict])]
:param is_dict_attr: If false and only one hit, still return as a list. Useful for things like inline_policies.
:return:

##### `base_group_dict_attribute(account_config_map: dict[str, AccountConfig], account_resources: list[dict]) -> list[dict]:`
Groups an attribute that is a dict or list of dicts with matching accounts

Call group_dict_attribute instead unless you need to transform this response.
An example would be tags which also contain role_access.

:param account_config_map: dict(account_id:str = AccountConfig)
:param account_resources: list[dict(account_id:str, resources=list[dict])]
:return: list[dict(included_accounts: str, resource_val=list[dict]|dict)]

##### `base_group_str_attribute(account_config_map: dict[str, AccountConfig], account_resources: list[dict]) -> dict[str, list]:`
Groups a string attribute by a shared name across of accounts

The ability to pass in and maintain arbitrary keys is necessary for parsing resource names related to a boto3 response.
It's also useful when the str is a reference object like inline policies where the str is a policy name.

Call group_int_or_str_attribute instead unless you need to transform this response.
An example would be grouping role names for generating the template where you need to keep the file_path ref.

:param account_config_map: dict(account_id:str = AccountConfig)
:param account_resources: list[dict(account_id:str, resources=list[dict(resource_val: str, **)])]
:return: dict(attribute_val: str = list[dict(resource_val: str, account_id: str, **)])

##### `set_included_accounts_for_grouped_attribute(account_config_map: dict[str, AccountConfig], number_of_accounts_resource_on: int, grouped_attribute) -> Union[list | dict]:`
Takes a grouped attribute and formats its `included_accounts` to * or a list of account names

:param account_config_map: {account_id: account_config}
:param number_of_accounts_resource_on:
:param grouped_attribute:
:return:

#### Use `iambic.aws.iam.role.template_generation.generate_aws_role_templates` as an example
While `generate_aws_role_templates` is somewhat complex what it's doing can be broken out into a series of steps that can be used for any other template type.

1. Retrieve the primary resource and write it into a file and create a map where {resource_id: file_path}
2. Retrieve related resources and update the file created in step 1
3. Format the resource file map to the structure expected by `base_group_str_attribute`
4. Now that the resources have been grouped by their id group the attributes for the resource across accounts and write the template. 
   1. Use `create_templated_role` as an example. The way it works should translate for any other template.

## Testing

TODO
demo/demo_config.yaml METASEP
aws_accounts:
- account_id: '972417093400'
  account_name: 'demo-1'
  aws_profile: demo_1
- account_id: '694815895589'
  account_name: 'demo-2'
  aws_profile: demo_2
- account_id: '518317429440'
  account_name: 'demo-3'
  aws_profile: demo_3

demo/config.yaml METASEP
extends:
  - key: AWS_SECRETS_MANAGER
    value: arn:aws:secretsmanager:us-west-2:759357822767:secret:dev/google-MmAS1o
    assume_role_arn: arn:aws:iam::759357822767:role/development_admin
aws_accounts:
  - account_id: '759357822767'
    account_name: dev
    assume_role_arn: 'arn:aws:iam::759357822767:role/NoqSpokeRoleCorpNoqDev'
  - account_id: '259868150464'
    account_name: staging
    assume_role_arn: 'arn:aws:iam::259868150464:role/NoqSpokeRoleCorpNoqDev'
  - account_id: '940552945933'
    account_name: prod
    assume_role_arn: 'arn:aws:iam::940552945933:role/NoqSpokeRoleCorpNoqDev'

google:
  groups:
    enabled: false

slack:
  alert_channel_id: 'C046KESSMQ8'

sqs:
  queues:
    iam_mutation: 
      arn: arn:aws:sqs:us-east-1:759357822767:noqform-iam-mutation-events
iambic/google/group/utils.py METASEP
import asyncio

from googleapiclient.errors import HttpError

from iambic.config.models import GoogleProject
from iambic.core.context import ctx
from iambic.core.logger import log
from iambic.core.models import ProposedChange, ProposedChangeType
from iambic.core.utils import aio_wrapper


async def list_groups(domain, google_project: GoogleProject):
    from iambic.google.group.models import get_group_template

    groups = []
    try:
        service = await google_project.get_service_connection(
            "admin", "directory_v1", domain
        )
        if not service:
            return []
    except AttributeError as err:
        log.exception("Unable to process google groups.", error=err)
        return

    req = await aio_wrapper(service.groups().list, domain=domain)
    res = req.execute()
    if res and "groups" in res:
        for group in res["groups"]:
            group_template = await get_group_template(service, group, domain)
            groups.append(group_template)
    return groups


async def get_group(group_email: str, domain: str, google_project: GoogleProject):
    from iambic.google.group.models import get_group_template

    try:
        service = await google_project.get_service_connection(
            "admin", "directory_v1", domain
        )
        if not service:
            return []
    except AttributeError as err:
        log.exception("Unable to process google groups.", error=err)
        return

    # TODO: Error handling:
    req = service.groups().get(groupKey=group_email)
    try:
        if group := req.execute():
            return await get_group_template(service, group, domain)
    except HttpError as err:
        if err.reason == "Not Authorized to access this resource/api":
            log.error("Unable to get group. It may not exist", error=err.reason)
        else:
            raise


async def create_group(
    id: str,
    domain: str,
    email: str,
    name: str,
    description: str,
    google_project: GoogleProject,
):
    try:
        service = await google_project.get_service_connection(
            "admin", "directory_v1", domain
        )
        if not service:
            return []
    except AttributeError as err:
        log.exception("Unable to process google groups.", error=err)
        return
    req = await aio_wrapper(
        service.groups().insert,
        body={"id": id, "email": email, "name": name, "description": description},
    )
    return req.execute()


async def update_group_domain(current_domain, proposed_domain, log_params):
    response = []
    if current_domain != proposed_domain:
        log_str = "Modifying group domain"
        response.append(
            ProposedChange(
                change_type=ProposedChangeType.UPDATE,
                attribute="domain",
                change_summary={
                    "current_domain": current_domain,
                    "proposed_domain": proposed_domain,
                },
            )
        )
        log.info(
            log_str,
            current_domain=current_domain,
            proposed_domain=proposed_domain,
            **log_params,
        )
        raise NotImplementedError(
            f"Current Domain {current_domain} does not match "
            f"proposed domain {proposed_domain}. We are unable "
            "to update group domains at this point in time."
        )
    return response


async def update_group_description(
    group_email,
    current_description,
    proposed_description,
    domain,
    google_project,
    log_params,
):
    response = []
    if current_description == proposed_description:
        return response
    try:
        service = await google_project.get_service_connection(
            "admin", "directory_v1", domain
        )
        if not service:
            return False
    except AttributeError as err:
        log.exception("Unable to process google groups.", error=err)
        return response
    log_str = "Detected updated group description"
    response.append(
        ProposedChange(
            change_type=ProposedChangeType.UPDATE,
            attribute="description",
            change_summary={
                "current_description": current_description,
                "proposed_description": proposed_description,
            },
        )
    )
    if ctx.execute:
        log_str = "Updating group description"
        await aio_wrapper(
            service.groups()
            .patch(groupKey=group_email, body={"description": proposed_description})
            .execute
        )
    log.info(
        log_str,
        current_description=current_description,
        proposed_description=proposed_description,
        **log_params,
    )
    return response


async def update_group_name(
    group_email, current_name, proposed_name, domain, google_project, log_params
):
    response = []
    if current_name == proposed_name:
        return response
    log_str = "Detected group name update"
    response.append(
        ProposedChange(
            change_type=ProposedChangeType.UPDATE,
            attribute="group_name",
            new_value=proposed_name,
        )
    )

    try:
        service = await google_project.get_service_connection(
            "admin", "directory_v1", domain
        )
        if not service:
            return False
    except AttributeError as err:
        log.exception("Unable to process google groups.", error=err)
        return
    if ctx.execute:
        log_str = "Updating group name"
        await aio_wrapper(
            service.groups()
            .patch(groupKey=group_email, body={"name": proposed_name})
            .execute
        )
    log.info(
        log_str, current_name=current_name, proposed_name=proposed_name, **log_params
    )
    return response


async def update_group_email(
    current_email, proposed_email, domain, google_project, log_params
):
    # TODO: This won't work as-is, since we aren't really aware of the old e-mail
    response = []
    if current_email == proposed_email:
        return response
    log_str = "Detected group e-mail update"
    response.append(
        ProposedChange(
            change_type=ProposedChangeType.UPDATE,
            attribute="group_email",
            new_value=proposed_email,
        )
    )

    try:
        service = await google_project.get_service_connection(
            "admin", "directory_v1", domain
        )
        if not service:
            return False
    except AttributeError as err:
        log.exception("Unable to process google groups.", error=err)
        return
    if ctx.execute:
        log_str = "Updating group e-mail"
        await aio_wrapper(
            service.groups()
            .patch(groupKey=current_email, body={"email": proposed_email})
            .execute
        )
    log.info(
        log_str, current_name=current_email, proposed_name=proposed_email, **log_params
    )
    return response


async def delete_group(group_email, domain, google_project, log_params):
    # TODO: This isn't used yet
    response = []
    try:
        service = await google_project.get_service_connection(
            "admin", "directory_v1", domain
        )
        if not service:
            return False
    except AttributeError as err:
        log.exception("Unable to process google groups.", error=err)
        return response

    log_str = "Detected group deletion"
    response.append(
        ProposedChange(
            change_type=ProposedChangeType.DELETE,
            attribute="group_email",
        )
    )
    if ctx.execute:
        log_str = "Deleting Group"
        await aio_wrapper(service.groups().delete(groupKey=group_email).execute)
    log.info(log_str, group_email=group_email, **log_params)
    return response


async def update_group_members(
    group_email, current_members, proposed_members, domain, google_project, log_params
):
    # TODO: This will likely fail if I change the Role of a user, since we are doing all
    # of these operations with asyncio.gather. Should do the remove operations first, then the add ones.
    tasks = []
    response = []
    try:
        service = await google_project.get_service_connection(
            "admin", "directory_v1", domain
        )
        if not service:
            return False
    except AttributeError as err:
        log.exception("Unable to process google groups.", error=err)
        return

    if users_to_remove := [
        member for member in current_members if member not in proposed_members
    ]:
        log_str = "Detected users to remove from group"
        response.append(
            ProposedChange(
                change_type=ProposedChangeType.DETACH,
                attribute="users",
                change_summary={
                    "UsersToRemove": [user.email for user in users_to_remove]
                },
            )
        )
        if ctx.execute:
            log_str = "Removing users from group"
            for user in users_to_remove:
                tasks.append(
                    aio_wrapper(
                        service.members()
                        .delete(groupKey=group_email, memberKey=user)
                        .execute
                    )
                )
        log.info(log_str, users=[user.email for user in users_to_remove], **log_params)

    if users_to_add := [
        member for member in proposed_members if member not in current_members
    ]:
        log_str = "Detected new users to add to group"
        response.append(
            ProposedChange(
                change_type=ProposedChangeType.ATTACH,
                attribute="users",
                change_summary={"UsersToAdd": [user.email for user in users_to_add]},
            )
        )
        if ctx.execute:
            log_str = "Adding users to group"
            for user in users_to_add:
                tasks.append(
                    aio_wrapper(
                        service.members()
                        .insert(
                            groupKey=group_email,
                            body={
                                "email": user.email,
                                "role": user.role.value,
                                "type": user.type.value,
                                "status": user.status.value,
                            },
                        )
                        .execute
                    )
                )

        log.info(log_str, users=[user.email for user in users_to_add], **log_params)
    if tasks:
        await asyncio.gather(*tasks)
    return response

iambic/google/group/template_generation.py METASEP
import json
import os

from iambic.core.utils import yaml
from iambic.google.group.utils import list_groups


async def generate_group_templates(config, domain, output_dir, google_project):
    """List all groups in the domain, along with members and
    settings"""

    groups = await list_groups(domain, google_project)

    base_path = os.path.expanduser(output_dir)
    for group in groups:
        file_path = os.path.expanduser(group.file_path)
        path = os.path.join(base_path, file_path)
        os.makedirs(os.path.dirname(path), exist_ok=True)
        with open(path, "w") as f:
            f.write(
                yaml.dump(
                    {
                        "template_type": group.template_type,
                        **json.loads(
                            group.json(
                                exclude_unset=True,
                                exclude_defaults=True,
                                exclude={"file_path"},
                            )
                        ),
                    }
                )
            )
    return groups

iambic/google/group/models.py METASEP
import asyncio
from itertools import chain
from typing import List, Optional

from pydantic import Field

from iambic.aws.models import ExpiryModel
from iambic.config.models import GoogleProject
from iambic.core.context import ctx
from iambic.core.logger import log
from iambic.core.models import AccountChangeDetails, ProposedChange, ProposedChangeType
from iambic.google.group.utils import (
    create_group,
    get_group,
    update_group_description,
    update_group_domain,
    update_group_email,
    update_group_members,
    update_group_name,
)
from iambic.google.models import (
    GoogleTemplate,
    GroupMemberRole,
    GroupMemberStatus,
    GroupMemberSubscription,
    GroupMemberType,
    WhoCanInvite,
    WhoCanJoin,
    WhoCanPostMessage,
    WhoCanViewGroup,
    WhoCanViewMembership,
)


class GroupMember(ExpiryModel):
    email: str
    expand: bool = Field(
        False,
        description="Expand the group into the members of the group. This is useful for nested groups.",
    )
    role: GroupMemberRole = GroupMemberRole.MEMBER
    type: GroupMemberType = GroupMemberType.USER
    status: GroupMemberStatus = GroupMemberStatus.ACTIVE
    subscription: GroupMemberSubscription = GroupMemberSubscription.EACH_EMAIL

    @property
    def resource_type(self):
        return "google:group:member"

    @property
    def resource_id(self):
        return self.email


class GroupTemplate(GoogleTemplate):
    template_type = "NOQ::Google::Group"
    name: str
    domain: str
    email: str
    description: str
    welcome_message: Optional[str]
    members: List[GroupMember]
    who_can_invite: WhoCanInvite = "ALL_MANAGERS_CAN_INVITE"
    who_can_join: WhoCanJoin = "CAN_REQUEST_TO_JOIN"
    who_can_post_message: WhoCanPostMessage = "NONE_CAN_POST"
    who_can_view_group: WhoCanViewGroup = "ALL_MANAGERS_CAN_VIEW"
    who_can_view_membership: WhoCanViewMembership = "ALL_MANAGERS_CAN_VIEW"
    read_only: bool = False
    # TODO: who_can_contact_group_members
    # TODO: who_can_view_member_email_addresses
    # TODO: allow_email_posting
    # TODO: allow_web_posting
    # TODO: conversation_history
    # TODO: There is more. Check google group settings page

    def apply_resource_dict(self, google_project: GoogleProject):
        return {
            "name": self.name,
            "email": self.email,
            "description": self.description,
            "members": self.members,
        }

    async def _apply_to_account(
        self, google_project: GoogleProject
    ) -> AccountChangeDetails:
        proposed_group = self.apply_resource_dict(google_project)
        change_details = AccountChangeDetails(
            account=self.domain,
            resource_id=self.email,
            new_value=proposed_group,  # TODO fix
            proposed_changes=[],
        )

        log_params = dict(
            resource_type=self.resource_type,
            resource_id=self.email,
            account=str(self.domain),
        )
        # read_only = self._is_read_only(google_project)

        current_group = await get_group(self.email, self.domain, google_project)
        if current_group:
            change_details.current_value = current_group
        # TODO: Check if deleted
        # deleted = self.get_attribute_val_for_account(aws_account, "deleted", False)
        # if isinstance(deleted, list):
        #     deleted = deleted[0].deleted
        # if deleted:
        #     if current_group:
        #         # Delete me

        group_exists = bool(current_group)

        tasks = []

        if not group_exists:
            change_details.proposed_changes.append(
                ProposedChange(
                    change_type=ProposedChangeType.CREATE,
                    resource_id=self.email,
                    resource_type=self.resource_type,
                )
            )
            log_str = "New resource found in code."
            if not ctx.execute:
                log.info(log_str, **log_params)
                # Exit now because apply functions won't work if resource doesn't exist
                return change_details

            log_str = f"{log_str} Creating resource..."
            log.info(log_str, **log_params)

            await create_group(
                id=self.email,
                domain=self.domain,
                email=self.email,
                name=self.name,
                description=self.description,
                google_project=google_project,
            )
            current_group = await get_group(self.email, self.domain, google_project)
            if current_group:
                change_details.current_value = current_group

        # TODO: Support group expansion
        tasks.extend(
            [
                update_group_domain(current_group.domain, self.domain, log_params),
                update_group_email(
                    current_group.email,
                    self.email,
                    self.domain,
                    google_project,
                    log_params,
                ),
                update_group_name(
                    self.email,
                    current_group.name,
                    self.name,
                    self.domain,
                    google_project,
                    log_params,
                ),
                update_group_description(
                    self.email,
                    current_group.description,
                    self.description,
                    self.domain,
                    google_project,
                    log_params,
                ),
                update_group_members(
                    self.email,
                    current_group.members,
                    self.members,
                    self.domain,
                    google_project,
                    log_params,
                ),
            ]
        )

        changes_made = await asyncio.gather(*tasks)
        if any(changes_made):
            change_details.proposed_changes.extend(
                list(chain.from_iterable(changes_made))
            )

        if ctx.execute:
            log.debug(
                "Successfully finished execution for resource",
                changes_made=bool(change_details.proposed_changes),
                **log_params,
            )
        else:
            log.debug(
                "Successfully finished scanning for drift for resource",
                requires_changes=bool(change_details.proposed_changes),
                **log_params,
            )

        return change_details

    @property
    def resource_type(self):
        return "google:group"

    def _is_read_only(self, google_project: GoogleProject):
        return google_project.read_only or self.read_only


async def get_group_template(service, group, domain) -> GroupTemplate:
    member_req = service.members().list(groupKey=group["email"])
    member_res = member_req.execute() or {}
    members = [
        GroupMember(
            email=member["email"],
            role=GroupMemberRole(member["role"]),
            type=GroupMemberType(member["type"]),
            status=GroupMemberStatus(member["status"]),
        )
        for member in member_res.get("members", [])
    ]
    file_name = f"{group['email'].split('@')[0]}.yaml"
    return GroupTemplate(
        file_path=f"google_groups/{domain}/{file_name}",
        domain=domain,
        name=group["name"],
        email=group["email"],
        description=group["description"],
        members=members,
    )

iambic/google/group/__init__.py METASEP

test/test_base.py METASEP
from unittest import TestCase


class TemplateBaseTestCase(TestCase):
    """
    This class helps to clearly define expectations for testing an iambic template.
    It defines the methods for providing the minimum set of expected tests for contribution.
    If any test does not apply to the template being tested, simply override the method with "pass"
    """

    def create_base_resource(self):
        """
        Create the bare minimum resource.
        Check that it was created and the object values are all correctly set on the provider.

        Example:
            Create a basic AWS IAM Role
                Verify the role is now there
                Check that the tags, name, description, and assume role policy document are correct
        """
        raise NotImplementedError

    def base_resource_invalid_value(self):
        """
        Check how a non-standard response from the provider is handled after an invalid value is provided.
        """
        raise NotImplementedError

    def create_complex_resource(self):
        """
        If the template represents multiple resources for the provider
        use this to test if not only the primary resource is created but its related resources.

        Example:
            Create an AWS IAM Role with inline policies, tags, and managed policies (managed policy ref)
                These are only attached to a single role but are technically different resources.
                They are updated, deleted, and for some created via entirely different API calls.
                Verify the role is there and correct
                Verify each related resource was also created correctly
        """
        raise NotImplementedError

    def create_resource_multi_account_values(self):
        """
        If this template can be applied to multiple accounts on the provider
        use this to test a scenario where a value is different between accounts.

        Example:
            Create an AWS IAM Role in 2 accounts.
            Account 1 has a description of "I am for testing"
            Account 2 has a description of "I am for validating"
            Check the description is set correctly for both accounts.
        """
        raise NotImplementedError

    def update_base_resource(self):
        """
        Create a bare minimum resource and attempt to update one or more attributes.

        Example:
            Create an AWS IAM Role in 2 accounts.
            Account 1 has a description of "I am for testing"
            Account 2 has a description of "I am for validating"
            Check the description is set correctly for both accounts.
        """
        raise NotImplementedError

    def update_complex_resource(self):
        """
        If the template represents multiple resources for the provider
        use this to test if not only the primary resource is updated but its related resources.

        Example:
            Create an AWS IAM Role with inline policies, tags, and managed policies (managed policy ref)
            Update the 1 or more attribute of each related resource
                Verify each related resource was update correctly
        """
        raise NotImplementedError

    def delete_base_resource(self):
        """
        Create a bare minimum resource then attempt to delete it.

        Example:
            Create an AWS IAM Role.
            Attempt to delete it.
            Confirm the role was deleted.
        """
        raise NotImplementedError

    def delete_multi_account_resource(self):
        """
        If this template can be applied to multiple accounts on the provider
        use this to test a scenario where the resource was deleted across all accounts.

        Example:
            Create an AWS IAM Role in 2 accounts.
            Attempt to delete them.
            Confirm the role was deleted on both accounts.
        """
        raise NotImplementedError

    def delete_complex_resource(self):
        """
        If the template represents multiple resources for the provider
        use this to test if not only the primary resource is deleted but its related resources.

        Example:
            Create an AWS IAM Role with inline policies, tags, and managed policies (managed policy ref)
            Delete the role.
            Verify not only the role was deleted but all resources related to it.
        """
        raise NotImplementedError

test/__init__.py METASEP
# Load the yaml
# Set the supported accounts

docs/generate_schema.py METASEP
import os

import jsonschema2md2

from iambic.aws.iam.models import MaxSessionDuration, Path
from iambic.aws.iam.policy.models import (
    AssumeRolePolicyDocument,
    ManagedPolicyRef,
    PolicyDocument,
    PolicyStatement,
)
from iambic.aws.iam.role.models import PermissionBoundary, RoleAccess, RoleTemplate
from iambic.config.models import AWSAccount, Config, ExtendsConfig, Variable
from iambic.core.utils import camel_to_snake
from iambic.google.group.models import GroupMember, GroupTemplate


def create_model_schemas(
    parser: jsonschema2md2.Parser,
    schema_dir: str,
    schema_md_str: str,
    model_schemas: list,
) -> str:
    for model in model_schemas:
        class_name = str(model.__name__)
        file_name = camel_to_snake(class_name)
        model_schema_path = str(os.path.join(schema_dir, f"{file_name}.md"))
        schema_md_str += f"* [{class_name}]({model_schema_path.replace('docs/', '')})\n"
        with open(model_schema_path, "w") as f:
            f.write("".join(parser.parse_schema(model.schema())))

    return schema_md_str


def generate_docs():
    aws_template_models = [
        RoleTemplate,
        AssumeRolePolicyDocument,
        ManagedPolicyRef,
        PolicyDocument,
        Path,
        PermissionBoundary,
        MaxSessionDuration,
        RoleAccess,
        PolicyStatement,
    ]
    google_template_models = [GroupTemplate, GroupMember]
    config_models = [
        AWSAccount,
        Config,
        ExtendsConfig,
        Variable,
    ]

    schema_dir = os.path.join("docs", "schemas")
    os.makedirs(schema_dir, exist_ok=True)
    parser = jsonschema2md2.Parser(
        examples_as_yaml=False,
        show_examples="all",
    )
    schema_md_str = "# AWS Template Models\n"
    schema_md_str = create_model_schemas(
        parser, schema_dir, schema_md_str, aws_template_models
    )
    schema_md_str += "\n# Google Template Models\n"
    schema_md_str = create_model_schemas(
        parser, schema_dir, schema_md_str, google_template_models
    )
    schema_md_str += "\n# Config Models\n"
    schema_md_str = create_model_schemas(
        parser, schema_dir, schema_md_str, config_models
    )

    with open(os.path.join("docs", "SCHEMA.md"), "w") as f:
        f.write(schema_md_str)


if __name__ == "__main__":
    generate_docs()

docs/__init__.py METASEP

setup.cfg METASEP
[flake8]
ignore = E203, E266, E501, W503
max-line-length = 88
max-complexity = 18
select = B,C,E,F,W,T4

[isort]
multi_line_output=3
include_trailing_comma=True
force_grid_wrap=0
use_parentheses=True
line_length=88

[tool:pytest]
testpaths=test/

pyproject.toml METASEP
[tool.poetry]
name = "iambic"
version = "0.1.0"
description = "The python package used to generate, parse, and execute noqform yaml templates."
authors = ["Handsome Engineer <engineering@noq.dev>"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.10"
boto3 = "^1.24.88"
click = "^8.1.3"
"ruamel.yaml" = "^0.17.21"
asgiref = "^3.5.2"
structlog = "^22.1.0"
pydantic = "^1.10.2"
deepdiff = "^5.8.1"
Jinja2 = "^3.1.2"
black = "^22.10.0"
isort = "^5.10.1"
flake8 = "^5.0.4"
pytest = "^7.1.3"
pytest-cov = "^4.0.0"
pre-commit = "^2.20.0"
ujson = "^5.5.0"
aiofiles = "^22.1.0"
xxhash = "^3.0.0"
slack-bolt = "^1.15.1"
google-api-python-client = "^2.64.0"
google-auth = "^2.12.0"
jsonschema2md2 = "^0.6.0"
GitPython = "^3.1.29"
pydantic-factories = "^1.12.0"

[tool.poetry.scripts]
iambic = "iambic.main:cli"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"


poetry.lock METASEP
[[package]]
name = "aiofiles"
version = "22.1.0"
description = "File support for asyncio."
category = "main"
optional = false
python-versions = ">=3.7,<4.0"

[[package]]
name = "asgiref"
version = "3.5.2"
description = "ASGI specs, helper code, and adapters"
category = "main"
optional = false
python-versions = ">=3.7"

[package.extras]
tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"]

[[package]]
name = "attrs"
version = "22.1.0"
description = "Classes Without Boilerplate"
category = "main"
optional = false
python-versions = ">=3.5"

[package.extras]
dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"]
docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]

[[package]]
name = "black"
version = "22.10.0"
description = "The uncompromising code formatter."
category = "main"
optional = false
python-versions = ">=3.7"

[package.dependencies]
click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
pathspec = ">=0.9.0"
platformdirs = ">=2"
tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}

[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.7.4)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]

[[package]]
name = "boto3"
version = "1.24.88"
description = "The AWS SDK for Python"
category = "main"
optional = false
python-versions = ">= 3.7"

[package.dependencies]
botocore = ">=1.27.88,<1.28.0"
jmespath = ">=0.7.1,<2.0.0"
s3transfer = ">=0.6.0,<0.7.0"

[package.extras]
crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]

[[package]]
name = "botocore"
version = "1.27.88"
description = "Low-level, data-driven core of boto 3."
category = "main"
optional = false
python-versions = ">= 3.7"

[package.dependencies]
jmespath = ">=0.7.1,<2.0.0"
python-dateutil = ">=2.1,<3.0.0"
urllib3 = ">=1.25.4,<1.27"

[package.extras]
crt = ["awscrt (==0.14.0)"]

[[package]]
name = "cachetools"
version = "5.2.0"
description = "Extensible memoizing collections and decorators"
category = "main"
optional = false
python-versions = "~=3.7"

[[package]]
name = "certifi"
version = "2022.9.24"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
python-versions = ">=3.6"

[[package]]
name = "cfgv"
version = "3.3.1"
description = "Validate configuration and produce human readable error messages."
category = "main"
optional = false
python-versions = ">=3.6.1"

[[package]]
name = "charset-normalizer"
version = "2.1.1"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main"
optional = false
python-versions = ">=3.6.0"

[package.extras]
unicode_backport = ["unicodedata2"]

[[package]]
name = "click"
version = "8.1.3"
description = "Composable command line interface toolkit"
category = "main"
optional = false
python-versions = ">=3.7"

[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}

[[package]]
name = "colorama"
version = "0.4.5"
description = "Cross-platform colored terminal text."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"

[[package]]
name = "coverage"
version = "6.5.0"
description = "Code coverage measurement for Python"
category = "main"
optional = false
python-versions = ">=3.7"

[package.dependencies]
tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}

[package.extras]
toml = ["tomli"]

[[package]]
name = "deepdiff"
version = "5.8.1"
description = "Deep Difference and Search of any Python object/data."
category = "main"
optional = false
python-versions = ">=3.6"

[package.dependencies]
ordered-set = ">=4.1.0,<4.2.0"

[package.extras]
cli = ["clevercsv (==0.7.1)", "click (==8.0.3)", "pyyaml (==5.4.1)", "toml (==0.10.2)"]

[[package]]
name = "distlib"
version = "0.3.6"
description = "Distribution utilities"
category = "main"
optional = false
python-versions = "*"

[[package]]
name = "Faker"
version = "15.1.1"
description = "Faker is a Python package that generates fake data for you."
category = "main"
optional = false
python-versions = ">=3.7"

[package.dependencies]
python-dateutil = ">=2.4"

[[package]]
name = "filelock"
version = "3.8.0"
description = "A platform independent file lock."
category = "main"
optional = false
python-versions = ">=3.7"

[package.extras]
docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"]
testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"]

[[package]]
name = "flake8"
version = "5.0.4"
description = "the modular source code checker: pep8 pyflakes and co"
category = "main"
optional = false
python-versions = ">=3.6.1"

[package.dependencies]
mccabe = ">=0.7.0,<0.8.0"
pycodestyle = ">=2.9.0,<2.10.0"
pyflakes = ">=2.5.0,<2.6.0"

[[package]]
name = "gitdb"
version = "4.0.9"
description = "Git Object Database"
category = "main"
optional = false
python-versions = ">=3.6"

[package.dependencies]
smmap = ">=3.0.1,<6"

[[package]]
name = "GitPython"
version = "3.1.29"
description = "GitPython is a python library used to interact with Git repositories"
category = "main"
optional = false
python-versions = ">=3.7"

[package.dependencies]
gitdb = ">=4.0.1,<5"

[[package]]
name = "google-api-core"
version = "2.10.2"
description = "Google API client core library"
category = "main"
optional = false
python-versions = ">=3.7"

[package.dependencies]
google-auth = ">=1.25.0,<3.0dev"
googleapis-common-protos = ">=1.56.2,<2.0dev"
protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0dev"
requests = ">=2.18.0,<3.0.0dev"

[package.extras]
grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio-status (>=1.33.2,<2.0dev)"]
grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0dev)"]
grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0dev)"]

[[package]]
name = "google-api-python-client"
version = "2.64.0"
description = "Google API Client Library for Python"
category = "main"
optional = false
python-versions = ">=3.7"

[package.dependencies]
google-api-core = ">=1.31.5,<2.0.0 || >2.3.0,<3.0.0dev"
google-auth = ">=1.19.0,<3.0.0dev"
google-auth-httplib2 = ">=0.1.0"
httplib2 = ">=0.15.0,<1dev"
uritemplate = ">=3.0.1,<5"

[[package]]
name = "google-auth"
version = "2.12.0"
description = "Google Authentication Library"
category = "main"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*"

[package.dependencies]
cachetools = ">=2.0.0,<6.0"
pyasn1-modules = ">=0.2.1"
rsa = {version = ">=3.1.4,<5", markers = "python_version >= \"3.6\""}
six = ">=1.9.0"

[package.extras]
aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)", "requests (>=2.20.0,<3.0.0dev)"]
enterprise_cert = ["cryptography (==36.0.2)", "pyopenssl (==22.0.0)"]
pyopenssl = ["pyopenssl (>=20.0.0)"]
reauth = ["pyu2f (>=0.1.5)"]

[[package]]
name = "google-auth-httplib2"
version = "0.1.0"
description = "Google Authentication Library: httplib2 transport"
category = "main"
optional = false
python-versions = "*"

[package.dependencies]
google-auth = "*"
httplib2 = ">=0.15.0"
six = "*"

[[package]]
name = "googleapis-common-protos"
version = "1.56.4"
description = "Common protobufs used in Google APIs"
category = "main"
optional = false
python-versions = ">=3.7"

[package.dependencies]
protobuf = ">=3.15.0,<5.0.0dev"

[package.extras]
grpc = ["grpcio (>=1.0.0,<2.0.0dev)"]

[[package]]
name = "httplib2"
version = "0.20.4"
description = "A comprehensive HTTP client library."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"

[package.dependencies]
pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0.2,<3.0.3 || >3.0.3,<4", markers = "python_version > \"3.0\""}

[[package]]
name = "identify"
version = "2.5.6"
description = "File identification library for Python"
category = "main"
optional = false
python-versions = ">=3.7"

[package.extras]
license = ["ukkonen"]

[[package]]
name = "idna"
version = "3.4"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
python-versions = ">=3.5"

[[package]]
name = "iniconfig"
version = "1.1.1"
description = "iniconfig: brain-dead simple config-ini parsing"
category = "main"
optional = false
python-versions = "*"

[[package]]
name = "isort"
version = "5.10.1"
description = "A Python utility / library to sort Python imports."
category = "main"
optional = false
python-versions = ">=3.6.1,<4.0"

[package.extras]
colors = ["colorama (>=0.4.3,<0.5.0)"]
pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
plugins = ["setuptools"]
requirements_deprecated_finder = ["pip-api", "pipreqs"]

[[package]]
name = "jinja2"
version = "3.1.2"
description = "A very fast and expressive template engine."
category = "main"
optional = false
python-versions = ">=3.7"

[package.dependencies]
MarkupSafe = ">=2.0"

[package.extras]
i18n = ["Babel (>=2.7)"]

[[package]]
name = "jmespath"
version = "1.0.1"
description = "JSON Matching Expressions"
category = "main"
optional = false
python-versions = ">=3.7"

[[package]]
name = "jsonschema2md2"
version = "0.6.0"
description = "Convert JSON Schema to human-readable Markdown documentation"
category = "main"
optional = false
python-versions = ">=3.6,<4.0"

[package.dependencies]
click = ">=7"
PyYAML = ">=5.1"

[[package]]
name = "markupsafe"
version = "2.1.1"
description = "Safely add untrusted strings to HTML/XML markup."
category = "main"
optional = false
python-versions = ">=3.7"

[[package]]
name = "mccabe"
version = "0.7.0"
description = "McCabe checker, plugin for flake8"
category = "main"
optional = false
python-versions = ">=3.6"

[[package]]
name = "mypy-extensions"
version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "main"
optional = false
python-versions = "*"

[[package]]
name = "nodeenv"
version = "1.7.0"
description = "Node.js virtual environment builder"
category = "main"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"

[package.dependencies]
setuptools = "*"

[[package]]
name = "ordered-set"
version = "4.1.0"
description = "An OrderedSet is a custom MutableSet that remembers its order, so that every"
category = "main"
optional = false
python-versions = ">=3.7"

[package.extras]
dev = ["black", "mypy", "pytest"]

[[package]]
name = "packaging"
version = "21.3"
description = "Core utilities for Python packages"
category = "main"
optional = false
python-versions = ">=3.6"

[package.dependencies]
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"

[[package]]
name = "pathspec"
version = "0.10.1"
description = "Utility library for gitignore style pattern matching of file paths."
category = "main"
optional = false
python-versions = ">=3.7"

[[package]]
name = "platformdirs"
version = "2.5.2"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "main"
optional = false
python-versions = ">=3.7"

[package.extras]
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"]
test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]

[[package]]
name = "pluggy"
version = "1.0.0"
description = "plugin and hook calling mechanisms for python"
category = "main"
optional = false
python-versions = ">=3.6"

[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]

[[package]]
name = "pre-commit"
version = "2.20.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
category = "main"
optional = false
python-versions = ">=3.7"

[package.dependencies]
cfgv = ">=2.0.0"
identify = ">=1.0.0"
nodeenv = ">=0.11.1"
pyyaml = ">=5.1"
toml = "*"
virtualenv = ">=20.0.8"

[[package]]
name = "protobuf"
version = "4.21.7"
description = ""
category = "main"
optional = false
python-versions = ">=3.7"

[[package]]
name = "py"
version = "1.11.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"

[[package]]
name = "pyasn1"
version = "0.4.8"
description = "ASN.1 types and codecs"
category = "main"
optional = false
python-versions = "*"

[[package]]
name = "pyasn1-modules"
version = "0.2.8"
description = "A collection of ASN.1-based protocols modules."
category = "main"
optional = false
python-versions = "*"

[package.dependencies]
pyasn1 = ">=0.4.6,<0.5.0"

[[package]]
name = "pycodestyle"
version = "2.9.1"
description = "Python style guide checker"
category = "main"
optional = false
python-versions = ">=3.6"

[[package]]
name = "pydantic"
version = "1.10.2"
description = "Data validation and settings management using python type hints"
category = "main"
optional = false
python-versions = ">=3.7"

[package.dependencies]
typing-extensions = ">=4.1.0"

[package.extras]
dotenv = ["python-dotenv (>=0.10.4)"]
email = ["email-validator (>=1.0.3)"]

[[package]]
name = "pydantic-factories"
version = "1.12.0"
description = "Mock data generation for pydantic based models and python dataclasses"
category = "main"
optional = false
python-versions = ">=3.7,<4.0"

[package.dependencies]
faker = "*"
pydantic = ">=1.9.0"
typing-extensions = "*"
xeger = "*"

[[package]]
name = "pyflakes"
version = "2.5.0"
description = "passive checker of Python programs"
category = "main"
optional = false
python-versions = ">=3.6"

[[package]]
name = "pyparsing"
version = "3.0.9"
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
category = "main"
optional = false
python-versions = ">=3.6.8"

[package.extras]
diagrams = ["jinja2", "railroad-diagrams"]

[[package]]
name = "pytest"
version = "7.1.3"
description = "pytest: simple powerful testing with Python"
category = "main"
optional = false
python-versions = ">=3.7"

[package.dependencies]
attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<2.0"
py = ">=1.8.2"
tomli = ">=1.0.0"

[package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]

[[package]]
name = "pytest-cov"
version = "4.0.0"
description = "Pytest plugin for measuring coverage."
category = "main"
optional = false
python-versions = ">=3.6"

[package.dependencies]
coverage = {version = ">=5.2.1", extras = ["toml"]}
pytest = ">=4.6"

[package.extras]
testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]

[[package]]
name = "python-dateutil"
version = "2.8.2"
description = "Extensions to the standard Python datetime module"
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"

[package.dependencies]
six = ">=1.5"

[[package]]
name = "pyyaml"
version = "6.0"
description = "YAML parser and emitter for Python"
category = "main"
optional = false
python-versions = ">=3.6"

[[package]]
name = "requests"
version = "2.28.1"
description = "Python HTTP for Humans."
category = "main"
optional = false
python-versions = ">=3.7, <4"

[package.dependencies]
certifi = ">=2017.4.17"
charset-normalizer = ">=2,<3"
idna = ">=2.5,<4"
urllib3 = ">=1.21.1,<1.27"

[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"]

[[package]]
name = "rsa"
version = "4.9"
description = "Pure-Python RSA implementation"
category = "main"
optional = false
python-versions = ">=3.6,<4"

[package.dependencies]
pyasn1 = ">=0.1.3"

[[package]]
name = "ruamel.yaml"
version = "0.17.21"
description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order"
category = "main"
optional = false
python-versions = ">=3"

[package.dependencies]
"ruamel.yaml.clib" = {version = ">=0.2.6", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""}

[package.extras]
docs = ["ryd"]
jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"]

[[package]]
name = "ruamel.yaml.clib"
version = "0.2.6"
description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml"
category = "main"
optional = false
python-versions = ">=3.5"

[[package]]
name = "s3transfer"
version = "0.6.0"
description = "An Amazon S3 Transfer Manager"
category = "main"
optional = false
python-versions = ">= 3.7"

[package.dependencies]
botocore = ">=1.12.36,<2.0a.0"

[package.extras]
crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"]

[[package]]
name = "setuptools"
version = "65.5.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "main"
optional = false
python-versions = ">=3.7"

[package.extras]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]

[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"

[[package]]
name = "slack-bolt"
version = "1.15.1"
description = "The Bolt Framework for Python"
category = "main"
optional = false
python-versions = ">=3.6"

[package.dependencies]
slack-sdk = ">=3.17.2,<4"

[package.extras]
adapter = ["CherryPy (>=18,<19)", "Django (>=3,<5)", "Flask (>=1,<3)", "Werkzeug (>=2,<3)", "boto3 (<=2)", "bottle (>=0.12,<1)", "chalice (>=1.26.5,<2)", "click (>=7,<8)", "falcon (>=2,<4)", "fastapi (>=0.70.0,<1)", "gunicorn (>=20,<21)", "pyramid (>=1,<3)", "sanic (>=22,<23)", "starlette (>=0.14,<1)", "tornado (>=6,<7)", "uvicorn (<1)", "websocket-client (>=1.2.3,<2)"]
adapter_testing = ["Flask (>=1,<2)", "Werkzeug (>=1,<2)", "boddle (>=0.2,<0.3)", "docker (>=5,<6)", "moto (>=3,<4)", "requests (>=2,<3)", "sanic-testing (>=0.7)"]
async = ["aiohttp (>=3,<4)", "websockets (>=10,<11)"]
testing = ["Flask-Sockets (>=0.2,<1)", "Jinja2 (==3.0.3)", "Werkzeug (>=1,<2)", "aiohttp (>=3,<4)", "black (==22.8.0)", "click (<=8.0.4)", "itsdangerous (==2.0.1)", "pytest (>=6.2.5,<7)", "pytest-asyncio (>=0.18.2,<1)", "pytest-cov (>=3,<4)"]
testing_without_asyncio = ["Flask-Sockets (>=0.2,<1)", "Jinja2 (==3.0.3)", "Werkzeug (>=1,<2)", "black (==22.8.0)", "click (<=8.0.4)", "itsdangerous (==2.0.1)", "pytest (>=6.2.5,<7)", "pytest-cov (>=3,<4)"]

[[package]]
name = "slack-sdk"
version = "3.19.1"
description = "The Slack API Platform SDK for Python"
category = "main"
optional = false
python-versions = ">=3.6.0"

[package.extras]
optional = ["SQLAlchemy (>=1,<2)", "aiodns (>1.0)", "aiohttp (>=3.7.3,<4)", "boto3 (<=2)", "websocket-client (>=1,<2)", "websockets (>=10,<11)"]
testing = ["Flask (>=1,<2)", "Flask-Sockets (>=0.2,<1)", "Jinja2 (==3.0.3)", "Werkzeug (<2)", "black (==22.8.0)", "boto3 (<=2)", "click (==8.0.4)", "codecov (>=2,<3)", "databases (>=0.5)", "flake8 (>=5,<6)", "itsdangerous (==1.1.0)", "moto (>=3,<4)", "psutil (>=5,<6)", "pytest (>=6.2.5,<7)", "pytest-asyncio (<1)", "pytest-cov (>=2,<3)"]

[[package]]
name = "smmap"
version = "5.0.0"
description = "A pure Python implementation of a sliding window memory map manager"
category = "main"
optional = false
python-versions = ">=3.6"

[[package]]
name = "structlog"
version = "22.1.0"
description = "Structured Logging for Python"
category = "main"
optional = false
python-versions = ">=3.7"

[package.extras]
dev = ["cogapp", "coverage[toml]", "freezegun (>=0.2.8)", "furo", "myst-parser", "pre-commit", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "rich", "simplejson", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "tomli", "twisted"]
docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "twisted"]
tests = ["coverage[toml]", "freezegun (>=0.2.8)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "simplejson"]

[[package]]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
category = "main"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"

[[package]]
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
category = "main"
optional = false
python-versions = ">=3.7"

[[package]]
name = "typing-extensions"
version = "4.4.0"
description = "Backported and Experimental Type Hints for Python 3.7+"
category = "main"
optional = false
python-versions = ">=3.7"

[[package]]
name = "ujson"
version = "5.5.0"
description = "Ultra fast JSON encoder and decoder for Python"
category = "main"
optional = false
python-versions = ">=3.7"

[[package]]
name = "uritemplate"
version = "4.1.1"
description = "Implementation of RFC 6570 URI Templates"
category = "main"
optional = false
python-versions = ">=3.6"

[[package]]
name = "urllib3"
version = "1.26.12"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4"

[package.extras]
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]

[[package]]
name = "virtualenv"
version = "20.16.5"
description = "Virtual Python Environment builder"
category = "main"
optional = false
python-versions = ">=3.6"

[package.dependencies]
distlib = ">=0.3.5,<1"
filelock = ">=3.4.1,<4"
platformdirs = ">=2.4,<3"

[package.extras]
docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"]
testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"]

[[package]]
name = "xeger"
version = "0.3.5"
description = "A library for generating random strings from a valid regular expression."
category = "main"
optional = false
python-versions = "*"

[[package]]
name = "xxhash"
version = "3.0.0"
description = "Python binding for xxHash"
category = "main"
optional = false
python-versions = ">=3.6"

[metadata]
lock-version = "1.1"
python-versions = "^3.10"
content-hash = "13d7eb98ad28452bd8982c9f4694f2c9feca49ee8824e0f2d20eda810660348b"

[metadata.files]
aiofiles = [
    {file = "aiofiles-22.1.0-py3-none-any.whl", hash = "sha256:1142fa8e80dbae46bb6339573ad4c8c0841358f79c6eb50a493dceca14621bad"},
    {file = "aiofiles-22.1.0.tar.gz", hash = "sha256:9107f1ca0b2a5553987a94a3c9959fe5b491fdf731389aa5b7b1bd0733e32de6"},
]
asgiref = [
    {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"},
    {file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"},
]
attrs = [
    {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
    {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
]
black = [
    {file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"},
    {file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"},
    {file = "black-22.10.0-1fixedarch-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6"},
    {file = "black-22.10.0-1fixedarch-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d"},
    {file = "black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4"},
    {file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"},
    {file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"},
    {file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"},
    {file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"},
    {file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"},
    {file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"},
    {file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"},
    {file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"},
    {file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"},
    {file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"},
    {file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"},
    {file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"},
]
boto3 = [
    {file = "boto3-1.24.88-py3-none-any.whl", hash = "sha256:6b4cf1cd0be65202c4cf0e4c69099bac3620bcd4049ca25a5e223c668401dd69"},
    {file = "boto3-1.24.88.tar.gz", hash = "sha256:93934343cac76084600a520e5be70c52152364d0c410681c2e25c2290f0e151c"},
]
botocore = [
    {file = "botocore-1.27.88-py3-none-any.whl", hash = "sha256:de4e087b24cd3bc369eb2e27f8fe94a6499f7dea08c919fba13cefb2496bd2bb"},
    {file = "botocore-1.27.88.tar.gz", hash = "sha256:ded0a4035baf91eb358ef501c92a8512543f5ab7658f459df3077a70a555b5cd"},
]
cachetools = [
    {file = "cachetools-5.2.0-py3-none-any.whl", hash = "sha256:f9f17d2aec496a9aa6b76f53e3b614c965223c061982d434d160f930c698a9db"},
    {file = "cachetools-5.2.0.tar.gz", hash = "sha256:6a94c6402995a99c3970cc7e4884bb60b4a8639938157eeed436098bf9831757"},
]
certifi = [
    {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"},
    {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"},
]
cfgv = [
    {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
    {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
]
charset-normalizer = [
    {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"},
    {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"},
]
click = [
    {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
    {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
]
colorama = [
    {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
    {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
]
coverage = [
    {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"},
    {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"},
    {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"},
    {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"},
    {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"},
    {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"},
    {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"},
    {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"},
    {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"},
    {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"},
    {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"},
    {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"},
    {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"},
    {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"},
    {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"},
    {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"},
    {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"},
    {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"},
    {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"},
    {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"},
    {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"},
    {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"},
    {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"},
    {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"},
    {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"},
    {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"},
    {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"},
    {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"},
    {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"},
    {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"},
    {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"},
    {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"},
    {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"},
    {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"},
    {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"},
    {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"},
    {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"},
    {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"},
    {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"},
    {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"},
    {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"},
    {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"},
    {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"},
    {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"},
    {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"},
    {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"},
    {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"},
    {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"},
    {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"},
    {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"},
]
deepdiff = [
    {file = "deepdiff-5.8.1-py3-none-any.whl", hash = "sha256:e9aea49733f34fab9a0897038d8f26f9d94a97db1790f1b814cced89e9e0d2b7"},
    {file = "deepdiff-5.8.1.tar.gz", hash = "sha256:8d4eb2c4e6cbc80b811266419cb71dd95a157094a3947ccf937a94d44943c7b8"},
]
distlib = [
    {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
    {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"},
]
Faker = [
    {file = "Faker-15.1.1-py3-none-any.whl", hash = "sha256:096c15e136adb365db24d8c3964fe26bfc68fe060c9385071a339f8c14e09c8a"},
    {file = "Faker-15.1.1.tar.gz", hash = "sha256:a741b77f484215c3aab2604100669657189548f440fcb2ed0f8b7ee21c385629"},
]
filelock = [
    {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"},
    {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"},
]
flake8 = [
    {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"},
    {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"},
]
gitdb = [
    {file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"},
    {file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"},
]
GitPython = [
    {file = "GitPython-3.1.29-py3-none-any.whl", hash = "sha256:41eea0deec2deea139b459ac03656f0dd28fc4a3387240ec1d3c259a2c47850f"},
    {file = "GitPython-3.1.29.tar.gz", hash = "sha256:cc36bfc4a3f913e66805a28e84703e419d9c264c1077e537b54f0e1af85dbefd"},
]
google-api-core = [
    {file = "google-api-core-2.10.2.tar.gz", hash = "sha256:10c06f7739fe57781f87523375e8e1a3a4674bf6392cd6131a3222182b971320"},
    {file = "google_api_core-2.10.2-py3-none-any.whl", hash = "sha256:34f24bd1d5f72a8c4519773d99ca6bf080a6c4e041b4e9f024fe230191dda62e"},
]
google-api-python-client = [
    {file = "google-api-python-client-2.64.0.tar.gz", hash = "sha256:0dc4c967a5c795e981af01340f1bd22173a986534de968b5456cb208ed6775a6"},
    {file = "google_api_python_client-2.64.0-py2.py3-none-any.whl", hash = "sha256:90545cd71969f8bcf15a6362c2a8c44c38b94ec35a88cfd60cf2c0df68a5eb74"},
]
google-auth = [
    {file = "google-auth-2.12.0.tar.gz", hash = "sha256:f12d86502ce0f2c0174e2e70ecc8d36c69593817e67e1d9c5e34489120422e4b"},
    {file = "google_auth-2.12.0-py2.py3-none-any.whl", hash = "sha256:98f601773978c969e1769f97265e732a81a8e598da3263895023958d456ee625"},
]
google-auth-httplib2 = [
    {file = "google-auth-httplib2-0.1.0.tar.gz", hash = "sha256:a07c39fd632becacd3f07718dfd6021bf396978f03ad3ce4321d060015cc30ac"},
    {file = "google_auth_httplib2-0.1.0-py2.py3-none-any.whl", hash = "sha256:31e49c36c6b5643b57e82617cb3e021e3e1d2df9da63af67252c02fa9c1f4a10"},
]
googleapis-common-protos = [
    {file = "googleapis-common-protos-1.56.4.tar.gz", hash = "sha256:c25873c47279387cfdcbdafa36149887901d36202cb645a0e4f29686bf6e4417"},
    {file = "googleapis_common_protos-1.56.4-py2.py3-none-any.whl", hash = "sha256:8eb2cbc91b69feaf23e32452a7ae60e791e09967d81d4fcc7fc388182d1bd394"},
]
httplib2 = [
    {file = "httplib2-0.20.4-py3-none-any.whl", hash = "sha256:8b6a905cb1c79eefd03f8669fd993c36dc341f7c558f056cb5a33b5c2f458543"},
    {file = "httplib2-0.20.4.tar.gz", hash = "sha256:58a98e45b4b1a48273073f905d2961666ecf0fbac4250ea5b47aef259eb5c585"},
]
identify = [
    {file = "identify-2.5.6-py2.py3-none-any.whl", hash = "sha256:b276db7ec52d7e89f5bc4653380e33054ddc803d25875952ad90b0f012cbcdaa"},
    {file = "identify-2.5.6.tar.gz", hash = "sha256:6c32dbd747aa4ceee1df33f25fed0b0f6e0d65721b15bd151307ff7056d50245"},
]
idna = [
    {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
    {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
]
iniconfig = [
    {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
    {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
isort = [
    {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
    {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
]
jinja2 = [
    {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
    {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
]
jmespath = [
    {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"},
    {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"},
]
jsonschema2md2 = [
    {file = "jsonschema2md2-0.6.0-py3-none-any.whl", hash = "sha256:d71fe2c8418a93821597bfe151636e25eccedc41afa29e9ca875951f4966e451"},
    {file = "jsonschema2md2-0.6.0.tar.gz", hash = "sha256:9c397bf3977290267dde4293e88f33feee41f90d9f77c61857447be7ce16d15b"},
]
markupsafe = [
    {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
    {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"},
    {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"},
    {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"},
    {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"},
    {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"},
    {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"},
    {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"},
    {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"},
    {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"},
    {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"},
    {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"},
    {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"},
    {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"},
    {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"},
    {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"},
    {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"},
    {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"},
    {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"},
    {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"},
    {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"},
    {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"},
    {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"},
    {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"},
    {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"},
    {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"},
    {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"},
    {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"},
    {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"},
    {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"},
    {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"},
    {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"},
    {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"},
    {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"},
    {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"},
    {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"},
    {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"},
    {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"},
    {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"},
    {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"},
]
mccabe = [
    {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
    {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
]
mypy-extensions = [
    {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
    {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
nodeenv = [
    {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"},
    {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"},
]
ordered-set = [
    {file = "ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8"},
    {file = "ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562"},
]
packaging = [
    {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
    {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
]
pathspec = [
    {file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"},
    {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"},
]
platformdirs = [
    {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
    {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
]
pluggy = [
    {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
    {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
pre-commit = [
    {file = "pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"},
    {file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"},
]
protobuf = [
    {file = "protobuf-4.21.7-cp310-abi3-win32.whl", hash = "sha256:c7cb105d69a87416bd9023e64324e1c089593e6dae64d2536f06bcbe49cd97d8"},
    {file = "protobuf-4.21.7-cp310-abi3-win_amd64.whl", hash = "sha256:3ec85328a35a16463c6f419dbce3c0fc42b3e904d966f17f48bae39597c7a543"},
    {file = "protobuf-4.21.7-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:db9056b6a11cb5131036d734bcbf91ef3ef9235d6b681b2fc431cbfe5a7f2e56"},
    {file = "protobuf-4.21.7-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:ca200645d6235ce0df3ccfdff1567acbab35c4db222a97357806e015f85b5744"},
    {file = "protobuf-4.21.7-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:b019c79e23a80735cc8a71b95f76a49a262f579d6b84fd20a0b82279f40e2cc1"},
    {file = "protobuf-4.21.7-cp37-cp37m-win32.whl", hash = "sha256:d3f89ccf7182293feba2de2739c8bf34fed1ed7c65a5cf987be00311acac57c1"},
    {file = "protobuf-4.21.7-cp37-cp37m-win_amd64.whl", hash = "sha256:a74d96cd960b87b4b712797c741bb3ea3a913f5c2dc4b6cbe9c0f8360b75297d"},
    {file = "protobuf-4.21.7-cp38-cp38-win32.whl", hash = "sha256:8e09d1916386eca1ef1353767b6efcebc0a6859ed7f73cb7fb974feba3184830"},
    {file = "protobuf-4.21.7-cp38-cp38-win_amd64.whl", hash = "sha256:9e355f2a839d9930d83971b9f562395e13493f0e9211520f8913bd11efa53c02"},
    {file = "protobuf-4.21.7-cp39-cp39-win32.whl", hash = "sha256:f370c0a71712f8965023dd5b13277444d3cdfecc96b2c778b0e19acbfd60df6e"},
    {file = "protobuf-4.21.7-cp39-cp39-win_amd64.whl", hash = "sha256:9643684232b6b340b5e63bb69c9b4904cdd39e4303d498d1a92abddc7e895b7f"},
    {file = "protobuf-4.21.7-py2.py3-none-any.whl", hash = "sha256:8066322588d4b499869bf9f665ebe448e793036b552f68c585a9b28f1e393f66"},
    {file = "protobuf-4.21.7-py3-none-any.whl", hash = "sha256:58b81358ec6c0b5d50df761460ae2db58405c063fd415e1101209221a0a810e1"},
    {file = "protobuf-4.21.7.tar.gz", hash = "sha256:71d9dba03ed3432c878a801e2ea51e034b0ea01cf3a4344fb60166cb5f6c8757"},
]
py = [
    {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
    {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
]
pyasn1 = [
    {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"},
    {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"},
]
pyasn1-modules = [
    {file = "pyasn1-modules-0.2.8.tar.gz", hash = "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e"},
    {file = "pyasn1_modules-0.2.8-py2.py3-none-any.whl", hash = "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"},
]
pycodestyle = [
    {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"},
    {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"},
]
pydantic = [
    {file = "pydantic-1.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb6ad4489af1bac6955d38ebcb95079a836af31e4c4f74aba1ca05bb9f6027bd"},
    {file = "pydantic-1.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1f5a63a6dfe19d719b1b6e6106561869d2efaca6167f84f5ab9347887d78b98"},
    {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:352aedb1d71b8b0736c6d56ad2bd34c6982720644b0624462059ab29bd6e5912"},
    {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19b3b9ccf97af2b7519c42032441a891a5e05c68368f40865a90eb88833c2559"},
    {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e9069e1b01525a96e6ff49e25876d90d5a563bc31c658289a8772ae186552236"},
    {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:355639d9afc76bcb9b0c3000ddcd08472ae75318a6eb67a15866b87e2efa168c"},
    {file = "pydantic-1.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae544c47bec47a86bc7d350f965d8b15540e27e5aa4f55170ac6a75e5f73b644"},
    {file = "pydantic-1.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4c805731c33a8db4b6ace45ce440c4ef5336e712508b4d9e1aafa617dc9907f"},
    {file = "pydantic-1.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d49f3db871575e0426b12e2f32fdb25e579dea16486a26e5a0474af87cb1ab0a"},
    {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c90345ec7dd2f1bcef82ce49b6235b40f282b94d3eec47e801baf864d15525"},
    {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b5ba54d026c2bd2cb769d3468885f23f43710f651688e91f5fb1edcf0ee9283"},
    {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05e00dbebbe810b33c7a7362f231893183bcc4251f3f2ff991c31d5c08240c42"},
    {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2d0567e60eb01bccda3a4df01df677adf6b437958d35c12a3ac3e0f078b0ee52"},
    {file = "pydantic-1.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:c6f981882aea41e021f72779ce2a4e87267458cc4d39ea990729e21ef18f0f8c"},
    {file = "pydantic-1.10.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4aac8e7103bf598373208f6299fa9a5cfd1fc571f2d40bf1dd1955a63d6eeb5"},
    {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a7b66c3f499108b448f3f004801fcd7d7165fb4200acb03f1c2402da73ce4c"},
    {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bedf309630209e78582ffacda64a21f96f3ed2e51fbf3962d4d488e503420254"},
    {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9300fcbebf85f6339a02c6994b2eb3ff1b9c8c14f502058b5bf349d42447dcf5"},
    {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:216f3bcbf19c726b1cc22b099dd409aa371f55c08800bcea4c44c8f74b73478d"},
    {file = "pydantic-1.10.2-cp37-cp37m-win_amd64.whl", hash = "sha256:dd3f9a40c16daf323cf913593083698caee97df2804aa36c4b3175d5ac1b92a2"},
    {file = "pydantic-1.10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b97890e56a694486f772d36efd2ba31612739bc6f3caeee50e9e7e3ebd2fdd13"},
    {file = "pydantic-1.10.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9cabf4a7f05a776e7793e72793cd92cc865ea0e83a819f9ae4ecccb1b8aa6116"},
    {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06094d18dd5e6f2bbf93efa54991c3240964bb663b87729ac340eb5014310624"},
    {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc78cc83110d2f275ec1970e7a831f4e371ee92405332ebfe9860a715f8336e1"},
    {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ee433e274268a4b0c8fde7ad9d58ecba12b069a033ecc4645bb6303c062d2e9"},
    {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c2abc4393dea97a4ccbb4ec7d8658d4e22c4765b7b9b9445588f16c71ad9965"},
    {file = "pydantic-1.10.2-cp38-cp38-win_amd64.whl", hash = "sha256:0b959f4d8211fc964772b595ebb25f7652da3f22322c007b6fed26846a40685e"},
    {file = "pydantic-1.10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c33602f93bfb67779f9c507e4d69451664524389546bacfe1bee13cae6dc7488"},
    {file = "pydantic-1.10.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5760e164b807a48a8f25f8aa1a6d857e6ce62e7ec83ea5d5c5a802eac81bad41"},
    {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6eb843dcc411b6a2237a694f5e1d649fc66c6064d02b204a7e9d194dff81eb4b"},
    {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b8795290deaae348c4eba0cebb196e1c6b98bdbe7f50b2d0d9a4a99716342fe"},
    {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e0bedafe4bc165ad0a56ac0bd7695df25c50f76961da29c050712596cf092d6d"},
    {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e05aed07fa02231dbf03d0adb1be1d79cabb09025dd45aa094aa8b4e7b9dcda"},
    {file = "pydantic-1.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:c1ba1afb396148bbc70e9eaa8c06c1716fdddabaf86e7027c5988bae2a829ab6"},
    {file = "pydantic-1.10.2-py3-none-any.whl", hash = "sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709"},
    {file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"},
]
pydantic-factories = [
    {file = "pydantic_factories-1.12.0-py3-none-any.whl", hash = "sha256:f022d1df1923936a6a6fbbeb0b3470dd0ff914bd118dba5d39327ff5138b5854"},
    {file = "pydantic_factories-1.12.0.tar.gz", hash = "sha256:a0125162334b47a04a7b44438372737b8bf43f2c9fd8e2d521f7bc16f319e0f7"},
]
pyflakes = [
    {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"},
    {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"},
]
pyparsing = [
    {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
    {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
]
pytest = [
    {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"},
    {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"},
]
pytest-cov = [
    {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"},
    {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"},
]
python-dateutil = [
    {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
    {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
]
pyyaml = [
    {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
    {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"},
    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
    {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
    {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
    {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"},
    {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"},
    {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"},
    {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"},
    {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"},
    {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"},
    {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"},
    {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"},
    {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"},
    {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"},
    {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"},
    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"},
    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"},
    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"},
    {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"},
    {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"},
    {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"},
    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"},
    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"},
    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"},
    {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"},
    {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"},
    {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"},
    {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"},
    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"},
    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"},
    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"},
    {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"},
    {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
    {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
]
requests = [
    {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
    {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
]
rsa = [
    {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"},
    {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"},
]
"ruamel.yaml" = [
    {file = "ruamel.yaml-0.17.21-py3-none-any.whl", hash = "sha256:742b35d3d665023981bd6d16b3d24248ce5df75fdb4e2924e93a05c1f8b61ca7"},
    {file = "ruamel.yaml-0.17.21.tar.gz", hash = "sha256:8b7ce697a2f212752a35c1ac414471dc16c424c9573be4926b56ff3f5d23b7af"},
]
"ruamel.yaml.clib" = [
    {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6e7be2c5bcb297f5b82fee9c665eb2eb7001d1050deaba8471842979293a80b0"},
    {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:066f886bc90cc2ce44df8b5f7acfc6a7e2b2e672713f027136464492b0c34d7c"},
    {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:221eca6f35076c6ae472a531afa1c223b9c29377e62936f61bc8e6e8bdc5f9e7"},
    {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-win32.whl", hash = "sha256:1070ba9dd7f9370d0513d649420c3b362ac2d687fe78c6e888f5b12bf8bc7bee"},
    {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:77df077d32921ad46f34816a9a16e6356d8100374579bc35e15bab5d4e9377de"},
    {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:cfdb9389d888c5b74af297e51ce357b800dd844898af9d4a547ffc143fa56751"},
    {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7b2927e92feb51d830f531de4ccb11b320255ee95e791022555971c466af4527"},
    {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win32.whl", hash = "sha256:ada3f400d9923a190ea8b59c8f60680c4ef8a4b0dfae134d2f2ff68429adfab5"},
    {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win_amd64.whl", hash = "sha256:de9c6b8a1ba52919ae919f3ae96abb72b994dd0350226e28f3686cb4f142165c"},
    {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d67f273097c368265a7b81e152e07fb90ed395df6e552b9fa858c6d2c9f42502"},
    {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:72a2b8b2ff0a627496aad76f37a652bcef400fd861721744201ef1b45199ab78"},
    {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d3c620a54748a3d4cf0bcfe623e388407c8e85a4b06b8188e126302bcab93ea8"},
    {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win32.whl", hash = "sha256:9efef4aab5353387b07f6b22ace0867032b900d8e91674b5d8ea9150db5cae94"},
    {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win_amd64.whl", hash = "sha256:846fc8336443106fe23f9b6d6b8c14a53d38cef9a375149d61f99d78782ea468"},
    {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0847201b767447fc33b9c235780d3aa90357d20dd6108b92be544427bea197dd"},
    {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:78988ed190206672da0f5d50c61afef8f67daa718d614377dcd5e3ed85ab4a99"},
    {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:210c8fcfeff90514b7133010bf14e3bad652c8efde6b20e00c43854bf94fa5a6"},
    {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win32.whl", hash = "sha256:a49e0161897901d1ac9c4a79984b8410f450565bbad64dbfcbf76152743a0cdb"},
    {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win_amd64.whl", hash = "sha256:bf75d28fa071645c529b5474a550a44686821decebdd00e21127ef1fd566eabe"},
    {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a32f8d81ea0c6173ab1b3da956869114cae53ba1e9f72374032e33ba3118c233"},
    {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7f7ecb53ae6848f959db6ae93bdff1740e651809780822270eab111500842a84"},
    {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:61bc5e5ca632d95925907c569daa559ea194a4d16084ba86084be98ab1cec1c6"},
    {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win32.whl", hash = "sha256:89221ec6d6026f8ae859c09b9718799fea22c0e8da8b766b0b2c9a9ba2db326b"},
    {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win_amd64.whl", hash = "sha256:31ea73e564a7b5fbbe8188ab8b334393e06d997914a4e184975348f204790277"},
    {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc6a613d6c74eef5a14a214d433d06291526145431c3b964f5e16529b1842bed"},
    {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:1866cf2c284a03b9524a5cc00daca56d80057c5ce3cdc86a52020f4c720856f0"},
    {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:1b4139a6ffbca8ef60fdaf9b33dec05143ba746a6f0ae0f9d11d38239211d335"},
    {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win32.whl", hash = "sha256:3fb9575a5acd13031c57a62cc7823e5d2ff8bc3835ba4d94b921b4e6ee664104"},
    {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win_amd64.whl", hash = "sha256:825d5fccef6da42f3c8eccd4281af399f21c02b32d98e113dbc631ea6a6ecbc7"},
    {file = "ruamel.yaml.clib-0.2.6.tar.gz", hash = "sha256:4ff604ce439abb20794f05613c374759ce10e3595d1867764dd1ae675b85acbd"},
]
s3transfer = [
    {file = "s3transfer-0.6.0-py3-none-any.whl", hash = "sha256:06176b74f3a15f61f1b4f25a1fc29a4429040b7647133a463da8fa5bd28d5ecd"},
    {file = "s3transfer-0.6.0.tar.gz", hash = "sha256:2ed07d3866f523cc561bf4a00fc5535827981b117dd7876f036b0c1aca42c947"},
]
setuptools = [
    {file = "setuptools-65.5.0-py3-none-any.whl", hash = "sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356"},
    {file = "setuptools-65.5.0.tar.gz", hash = "sha256:512e5536220e38146176efb833d4a62aa726b7bbff82cfbc8ba9eaa3996e0b17"},
]
six = [
    {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
    {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
slack-bolt = [
    {file = "slack_bolt-1.15.1-py2.py3-none-any.whl", hash = "sha256:583a319734ee7de2fca758605c92253de7b3c9ef72d0c38251126c055af0d1c1"},
    {file = "slack_bolt-1.15.1.tar.gz", hash = "sha256:8ead9331d7733a1ed364c4c57cf7a10fcb8c4be509ddba778c04e9efee9883cf"},
]
slack-sdk = [
    {file = "slack_sdk-3.19.1-py2.py3-none-any.whl", hash = "sha256:a0bafb9e53ffbc408a8b5a42443525aac2c0c7347223d9373dd9d41cf90c9343"},
    {file = "slack_sdk-3.19.1.tar.gz", hash = "sha256:5db78a3ed3c58b3b0a2bdbe40bac43c7b00edcbc12ff6a7778ac5cbe3a3a2899"},
]
smmap = [
    {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"},
    {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"},
]
structlog = [
    {file = "structlog-22.1.0-py3-none-any.whl", hash = "sha256:760d37b8839bd4fe1747bed7b80f7f4de160078405f4b6a1db9270ccbfce6c30"},
    {file = "structlog-22.1.0.tar.gz", hash = "sha256:94b29b1d62b2659db154f67a9379ec1770183933d6115d21f21aa25cfc9a7393"},
]
toml = [
    {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
    {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
tomli = [
    {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
    {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
typing-extensions = [
    {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
    {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
]
ujson = [
    {file = "ujson-5.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ff4928dc1e9704b567171c16787238201fdbf023665573c12c02146fe1e02eec"},
    {file = "ujson-5.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1dc2f46c31ef22b0aaa28cd71be897bea271e700636658d573df9c43c49ebbd0"},
    {file = "ujson-5.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6019e3480d933d3698f2ecb4b46d64bfadd64e718f04fac36e681f3254b49a93"},
    {file = "ujson-5.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5179088ef6487c475604b7898731a6ddeeada7702cfb2162155b016703a8475"},
    {file = "ujson-5.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c04ae27e076d81a3839047d8eed57c1e17e361640616fd520d752375e3ba8f0c"},
    {file = "ujson-5.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:60a4b481978ea2aad8fe8af1ecc271624d01b3cf4b09e9b643dd2fe19c07634c"},
    {file = "ujson-5.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7a09d203983104918c62f2eef9406f24c355511f9217967df23e70fa7f5b54ff"},
    {file = "ujson-5.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b9812638d7aa8ecda2e8e1513fb4da999249603bffab7439a5f8f0bb362b0db"},
    {file = "ujson-5.5.0-cp310-cp310-win32.whl", hash = "sha256:33cd9084fefc74cbacf88c92fd260b61211e00bcde38d640c369e5dc34a2b4e1"},
    {file = "ujson-5.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:765d46f3d5e7a1d48075035e2d1a9164f683e3fccde834ca04602e6c588835bc"},
    {file = "ujson-5.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:278aa9d7cb56435c96d19f5d702e026bcf69f824e24b41e9b52706abd3565837"},
    {file = "ujson-5.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9585892091ae86045135d6a6129a644142d6a51b23e1428bb5de6d10bc0ce0c7"},
    {file = "ujson-5.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cdc46859024501c20ab74ad542cdf2f08b94b5ce384f2f569483fa3ed926d04"},
    {file = "ujson-5.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5bea13c73f36c4346808df3fa806596163a7962b6d28001ca2a391cab856089"},
    {file = "ujson-5.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f3f4240d99d55eb97cb012e9adf401f5ed9cd827af0341ac44603832202b0d2"},
    {file = "ujson-5.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d93940664a5ccfd79f72dcb939b0c31a3479889f14f0eb95ec52976f8c0cae7d"},
    {file = "ujson-5.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:880c84ce59f49776cf120f77e7ca04877c97c6887917078dbc369eb47004d7cf"},
    {file = "ujson-5.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:977bf5be704a88d46bf5b228df8b44521b1f3119d741062191608b3a6a38f224"},
    {file = "ujson-5.5.0-cp311-cp311-win32.whl", hash = "sha256:e0b36257dc90194784531c3b922d8d31fb2b4d8e5adfd27aff4eee7174176365"},
    {file = "ujson-5.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:a34a5f034b339f69ef7f6a134c22d04b92e07b6ddc1dd65382e7e4ec65d6437d"},
    {file = "ujson-5.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f26544bc10c83a2ff9aa2e093500c1b473f327faae31fb468d591e5823333376"},
    {file = "ujson-5.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fd797a4837ba10671954e7c09010cec7aca67e09d193f4920a16beea5f66f65"},
    {file = "ujson-5.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d7cfac2547c93389fa303fc0c0eb6698825564e8389c41c9b60009c746207b6"},
    {file = "ujson-5.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4875cafc9a6482c04c7df52a725d1c41beb74913c0ff4ec8f189f1954a2afe9"},
    {file = "ujson-5.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0762a4fdf86e01f3f8d8b6b7158d01fdd870799ff3f402b676e358fcd879e7eb"},
    {file = "ujson-5.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6c7ae6e0778ab9610f5e80e0595957d101ab8de18c32a8c053a19943ef4831d0"},
    {file = "ujson-5.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:94874584b733a18b310b0e954d53168e62cd4a0fd9db85b1903f0902a7eb33e8"},
    {file = "ujson-5.5.0-cp37-cp37m-win32.whl", hash = "sha256:3b74467564814fbce322427a5664e6bcc7dae6dbc8acbef76300fe43ca4072ab"},
    {file = "ujson-5.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:59cdcd934385f36e8bd76aedc234371cc75c848d95bdce804ac8aa8744cfeffa"},
    {file = "ujson-5.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2e506ecf89b6b9d304362ccef770831ec242a52c89dab1b4aabf1ab0eb1d5ed6"},
    {file = "ujson-5.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:10095160dbe6bba8059ad6677a01da251431f4c68041bf796dcac0956b34f8f7"},
    {file = "ujson-5.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5035bb997d163f346c22abcec75190e7e756a5349e7c708bd3d5fd7066a9a854"},
    {file = "ujson-5.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7d12f2d2df195c8c4e49d2cdbad640353a856c62ca2c624d8b47aa33b65a2a2"},
    {file = "ujson-5.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a485117f97312bef45f5d79d2ff97eff4da503b8a04f3691f59d31141686459"},
    {file = "ujson-5.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:21678d7e068707e4d54bdfeb8c250ebc548b51e499aed778b22112ca31a79669"},
    {file = "ujson-5.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a9b1320d8363a42d857fae8065a2174d38217cdd58cd8dc4f48d54e0591271e"},
    {file = "ujson-5.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:701e81e047f5c0cffd4ac828efca68b0bd270c616654966a051e9a5f836b385e"},
    {file = "ujson-5.5.0-cp38-cp38-win32.whl", hash = "sha256:1cef44ea4973344baed3d50a5da4a8843de3a6af7dea7fadf0a594e53ce5892f"},
    {file = "ujson-5.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e510d288e613d6927796dfb728e13e4530fc83b9ccac5888a21f7860486eab21"},
    {file = "ujson-5.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e1135264bcd40965cd35b0869e36952f54825024befdc7a923df9a7d83cfd800"},
    {file = "ujson-5.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:703fd69d9cb21d6ec2086789df9be2cf8140a76ff127050c24007ea8940dcd3b"},
    {file = "ujson-5.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:849f2ff40264152f25589cb48ddb4a43d14db811f841ec73989bfc0c8c4853fa"},
    {file = "ujson-5.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf416a93e1331820c77e3429df26946dbd4fe105e9b487cd2d1b7298b75784a8"},
    {file = "ujson-5.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:593a0f6fb0e186c5ba65465ed6f6215a30d1efa898c25e74de1c8577a1bff6d0"},
    {file = "ujson-5.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7c20cc83b0df47129ec6ed8a47fa7dcfc309c5bad029464004162738502568bb"},
    {file = "ujson-5.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6f83be8257b2f2dd6dea5ee62cd28db90584da7a7af1fba77a2102fc7943638a"},
    {file = "ujson-5.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8141f654432cf75144d6103bfac2286b8adf23467201590b173a74535d6be22d"},
    {file = "ujson-5.5.0-cp39-cp39-win32.whl", hash = "sha256:3fe1aea596f9539fc20cd9e52f098c842afc090168824fd4ca9744fe13151a03"},
    {file = "ujson-5.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:a655f7b755cfc5c07f2116b6dcf0ba148c89adef9a6d40c1b0f1fada878c4345"},
    {file = "ujson-5.5.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f19f11055ba2961eb39bdb1ff15763a53fca4fa0b5b624da3c7a528e83cdd09c"},
    {file = "ujson-5.5.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d87c817b292efb748f1974f37e8bb8a8772ef92f05f84e507159360814bcc3f"},
    {file = "ujson-5.5.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f9681ec4c60d0da590552427d770636d9079038c30b265f507ccde23caa7823"},
    {file = "ujson-5.5.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f63d1ae1ca17bb2c847e298c7bcf084a73d56d434b4c50509fb93a4b4300b0b2"},
    {file = "ujson-5.5.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:95603eff711b8f3b9596e1c961dbeb745a792ba1904141612f194e07edd71e5f"},
    {file = "ujson-5.5.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2d90414e3b4b44b39825049185959488e084ea7fcaf6124afd5c00893938b09d"},
    {file = "ujson-5.5.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7471d4486f23518cff343f1eec6c68d1b977ed74c3e6cc3e1ac896b9b7d68645"},
    {file = "ujson-5.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee9a2c9a4b2421e77f8fe33ed0621dea03c66c710707553020b1e32f3afb6240"},
    {file = "ujson-5.5.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a8cb3c8637006c5bd8237ebb5992a76ba06e39988ad5cff2096227443e8fd6a"},
    {file = "ujson-5.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d9c89c521dc90c7564358e525f849b93ad1d710553c1491f66b8cce8113bc901"},
    {file = "ujson-5.5.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2ab011e3556a9a1d9461bd686870c527327765ed02fe53550531d6609a8a33ff"},
    {file = "ujson-5.5.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:603607f56a0ee84d9cd2c7e9b1d29b18a70684b94ee34f07b9ffe8dc9c8a9f81"},
    {file = "ujson-5.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75bef34e69e7effb7b4849e3f830e3174d2cc6ec7273503fdde111c222dc9b3"},
    {file = "ujson-5.5.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abfe83e082c9208891e2158c1b5044a650ecec408b823bf6bf16cd7f8085cafa"},
    {file = "ujson-5.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4ef4ab8352861b99bd7fedb1fc6df3ea7f7d5216c789ba6d859e4ea06f1a4c45"},
    {file = "ujson-5.5.0.tar.gz", hash = "sha256:b25077a971c7da47bd6846a912a747f6963776d90720c88603b1b55d81790780"},
]
uritemplate = [
    {file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"},
    {file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"},
]
urllib3 = [
    {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"},
    {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"},
]
virtualenv = [
    {file = "virtualenv-20.16.5-py3-none-any.whl", hash = "sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27"},
    {file = "virtualenv-20.16.5.tar.gz", hash = "sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da"},
]
xeger = [
    {file = "xeger-0.3.5.tar.gz", hash = "sha256:2a91341fc2c814b27917b8bd24e8d212c8a3b904d98e9a6703d27484c2cb0f82"},
]
xxhash = [
    {file = "xxhash-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:219cba13991fd73cf21a5efdafa5056f0ae0b8f79e5e0112967e3058daf73eea"},
    {file = "xxhash-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3fcbb846af15eff100c412ae54f4974ff277c92eacd41f1ec7803a64fd07fa0c"},
    {file = "xxhash-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f475fa817ff7955fc118fc1ca29a6e691d329b7ff43f486af36c22dbdcff1db"},
    {file = "xxhash-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9200a90f02ff6fd5fb63dea107842da71d8626d99b768fd31be44f3002c60bbe"},
    {file = "xxhash-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a1403e4f551c9ef7bcef09af55f1adb169f13e4de253db0887928e5129f87af1"},
    {file = "xxhash-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa7f6ca53170189a2268c83af0980e6c10aae69e6a5efa7ca989f89fff9f8c02"},
    {file = "xxhash-3.0.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b63fbeb6d9c93d50ae0dc2b8a8b7f52f2de19e40fe9edc86637bfa5743b8ba2"},
    {file = "xxhash-3.0.0-cp310-cp310-win32.whl", hash = "sha256:31f25efd10b6f1f6d5c34cd231986d8aae9a42e042daa90b783917f170807869"},
    {file = "xxhash-3.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:807e88ed56e0fb347cb57d5bf44851f9878360fed700f2f63e622ef4eede87a5"},
    {file = "xxhash-3.0.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6d612c55a75d84d25898f6c5ad6a589aa556d1cb9af770b6c574ee62995167f6"},
    {file = "xxhash-3.0.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f9309fcaf73f93df3101f03a61dc30644adff3e8d0044fff8c0c195dbbe63e2"},
    {file = "xxhash-3.0.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2273fe40720e86346a17f06ef95cd60ee0d66ffce7cf55e390ef7350112b16d"},
    {file = "xxhash-3.0.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc6f3a334587c83c5ba56c19b254a97542ce1fc05ccfd66fbf568e6117718d65"},
    {file = "xxhash-3.0.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36cf410da5bfcca51ac3c2c51a3317dcd7af91f70fa61eca57fba39554f06ae3"},
    {file = "xxhash-3.0.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:21752a3e9a2391d91bd51f4aa2fe028ae14ba6a8d37db9ebe00ccac10be5ac4a"},
    {file = "xxhash-3.0.0-cp36-cp36m-win32.whl", hash = "sha256:322068a063ef156455a401ab720f0892f2d2dd1540c1a308e95a7cbf356df51c"},
    {file = "xxhash-3.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2984fa9a880587c0bfa46d32717b2d209863ee68727ea0fc17f05fce25efa692"},
    {file = "xxhash-3.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6493dd938b360235da81b1c79d8cd048c4f11977e1159b4e744c54f98d3a7bb4"},
    {file = "xxhash-3.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb9eca32f9b4acc7149db2c86f8108167b9929b7da1887d4287a90cfdb3ea53a"},
    {file = "xxhash-3.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4125e70e4e1d79992d81de837a0586aa0241665dbc5ce01b9c89330ed5cbb66"},
    {file = "xxhash-3.0.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:583bea142569485bdb0c5900e804058c16edba1850b74519688c22bc546e6175"},
    {file = "xxhash-3.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f3adf2891acc18abacd15113e9cbbefd30e5f4ecaae32c23e5486fc09c76ea5"},
    {file = "xxhash-3.0.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed65a2671d380ae05262ce1e4ccc2b63f3c30506d207bf6fae8cd72be0ad65d4"},
    {file = "xxhash-3.0.0-cp37-cp37m-win32.whl", hash = "sha256:c604b3dcac9d37e3fceaa11884927024953260cc4224d9b89400d16e6cf34021"},
    {file = "xxhash-3.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1c6fc59e182506496544bc6d426bcf6077066ed1b40cfcd937f707cc06c7ef50"},
    {file = "xxhash-3.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5628375dbb76d33b93b44854a6c5433e2a78115e03ea2ae1bb74a34ab012a43f"},
    {file = "xxhash-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:687aa4373690f23a3f43cc23d81005304d284ff6c041bff1f967664ab6410f36"},
    {file = "xxhash-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fa2100fb68b163e99370561c9e29ed37b9153fe99443600bea28829150eb0e4"},
    {file = "xxhash-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:891d7651431a055f76fe2c8f86c593c3dede8ec5b10ca55e8ff5c9fdceb55f0b"},
    {file = "xxhash-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:197c32d7b62be02957ca31aa69febadf9c5a34ef953053ea16e2c72465bc450f"},
    {file = "xxhash-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91fa4df41bda3cbec4084d9696028780b47128c1f8450d1ad9c3e4b6bf8b1f99"},
    {file = "xxhash-3.0.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cd38b766fc40e9fe37b80112656d2e5a0cb2f9bc12e01b286353b5ecd2768e8"},
    {file = "xxhash-3.0.0-cp38-cp38-win32.whl", hash = "sha256:4258ef78f5a7d1f9c595846134c7d81a868c74942051453258eb383498662d4d"},
    {file = "xxhash-3.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:b82b1cf4407ad908e04e864473cc3baa8e764c7bbebea959150764cc681a1611"},
    {file = "xxhash-3.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:da4d91e28418469b29eed8635c08af28b588e51cd04288bed1ba1cf60f2d91f6"},
    {file = "xxhash-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48aab36169b0c00e586cb4eb2814ab8bfed686933126019906f917ff9a78c99e"},
    {file = "xxhash-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b0d522570c9ccea6203b3d96ac7f0cfc1d29e613640475d513be432545c48cc"},
    {file = "xxhash-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6054434ddb060685e86e7457f52d188b0886834baaa532f9f78b4f2b53cfd9b"},
    {file = "xxhash-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbf546ca5f5903ceeb46d9e6abf81f3a64edb95bb7dbe0f75283eec93a7eb2a0"},
    {file = "xxhash-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22704f23f23ccbe892cee3e7568c67f07ac25beaa2d1cff183274005d9d39149"},
    {file = "xxhash-3.0.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83198e223bcc4b2418b5282ac930e444738c2a33859dee4e570b25c8433d83a2"},
    {file = "xxhash-3.0.0-cp39-cp39-win32.whl", hash = "sha256:3bcd4cd9b22293ea1c08822518fbb6d933c2960d66662d468a1945a45cace194"},
    {file = "xxhash-3.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:f5dd4c37da3408d56ae942dc103f4ae3b43510daa4f5accd0a411fc6e914f10a"},
    {file = "xxhash-3.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:485f172abc03f78afd4f38dbdbb5665f59c5487126fa4c3181c6582cda4de03b"},
    {file = "xxhash-3.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:035248b3d7ab6deb7b247278494d293b9faccfa853078319d25e2926f566b2f8"},
    {file = "xxhash-3.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30ae90c0cfd10ffe852c6b0f263253782eea74a8189d5f2440f6595c1e8047e"},
    {file = "xxhash-3.0.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8fd203d8a3c013e679722047ef4f061f690c6cff49380622444bca4c30f3bf23"},
    {file = "xxhash-3.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:6d60059aaef12a01c0cc24f1d7aaaab7933ae9f4b7adfd9ebbd37dc7ceac1745"},
    {file = "xxhash-3.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:676c97bf7cc298b65eec0368c2cb5611d87a8e876930843311ca728f69292752"},
    {file = "xxhash-3.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2245c6e20e96e3f8fdfb61ad6bc5cde6ce8a1c2b93aa4a32a27bba7ab3aeaf12"},
    {file = "xxhash-3.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ae926a52d020085a2d7f69d0e2155cbf819ae409f2e5dbb345dd40a6462de32"},
    {file = "xxhash-3.0.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a2efdcb811be3edc520b78364c11a1e54f5d8e5db895a9ff2bcdd4a7ffa36a5"},
    {file = "xxhash-3.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:885b3a851980056707ab99a2c19c35dfe2c2ba5f602066dbfcd8af45ea855760"},
    {file = "xxhash-3.0.0.tar.gz", hash = "sha256:30b2d97aaf11fb122023f6b44ebb97c6955e9e00d7461a96415ca030b5ceb9c7"},
]

docker-compose.yaml METASEP
version: "3"
services:
  iambic:
    environment:
      - IAMBIC_CONFIG=arn:aws:secretsmanager:us-west-2:759357822767:secret:dev/iambic-full
      - AWS_PROFILE=arn:aws:iam::940552945933:role/NoqCentralRoleCorpNoqDev
      - IAMBIC_CONFIG_ASSUME_ROLE=arn:aws:iam::759357822767:role/development_admin
      - AWS_ACCESS_KEY_ID
      - AWS_SECRET_ACCESS_KEY
      - AWS_SESSION_TOKEN
    build:
      context: .
      dockerfile: Dockerfile
    image: iambic
    ports:
      - 9000:8080
    volumes:
      - ~/.aws:/root/.aws:cached
      - .:/app:cached
    entrypoint:
      - "/usr/bin/aws-lambda-rie"
      - "/usr/bin/python"
      - "-m"
      - "awslambdaric"
    command: "iambic.lambda.app.handler"

docker-compose-cicd.yaml METASEP
version: "3"
services:
  iambic-cicd:
    # NOTE: This compose file loads the image from ECR, not from what you are running
    # locally.
    image: public.ecr.aws/s2p9s3r8/iambic:latest
    environment:
      - IAMBIC_CONFIG=arn:aws:secretsmanager:us-west-2:759357822767:secret:dev/iambic-full
      - IAMBIC_CONFIG_ASSUME_ROLE=arn:aws:iam::759357822767:role/IambicSpokeRole
      - AWS_ACCESS_KEY_ID
      - AWS_SECRET_ACCESS_KEY
      - AWS_SESSION_TOKEN
      - AWS_DEFAULT_REGION
      - AWS_REGION
    volumes:
      - ~/.aws:/root/.aws:cached
    entrypoint: ""
    command:
      - "python"
      - "-m"
      - "iambic.lambda.app"
      - "import"

README.md METASEP
# iambic

> IAM-better-in-code
The python package used to generate, parse, and execute IAMbic yaml templates.

Features: 
|     |     |
| --- | --- |
| Multi-Account Aware <br>(Automatically recognizes and populates<br>new accounts with default resources and identities) | X   |
| Stateless (and *fast!*) | X   |
| Continuous Drift Correction Out of the Box<br>(*What you see is what you git)* | X   |
| Dynamically define identity permissions<br>across accounts | X   |
| Centralize and link all human and cloud identities in a<br>single repository | X   |
| Simple YAML DSL<br>(Even a developer can understand it!) | X   |
| Easy Rollback Changes<br>(*git revert* actually *reverts)* | X   |
| Transpile into Terraform | Coming<br>Soon! |

Noq Enterprise offers many more enhancements on top of Iambic. Visit [https://www.noq.dev](https://www.noq.dev) to learn more and sign up for early access.

|     |     |
| --- | --- |
| Self-Service ChatOps and Web UI<br>(With type-aheads for **all** the things) | Coming<br>Soon! |
| Generate Cookie-Cutter Team Roles based on<br>usage data | Coming<br>Soon! |
| Alert on Unauthorized Out-of-band Changes<br>(Convert to change request with one click) | Coming<br>Soon! |
| Automatically Approve Low-Risk Permissions<br>Requests | Coming<br>Soon! |
| Discover the IAM Permissions of a<br>Local Application | Coming<br>Soon! |
| Lint changes automatically with Access<br>Analyzer | Coming<br>Soon! |
| Automatically Generate Service Control Policies<br>base on usage | Coming<br>Soon! |
| Prune Unused IAM Permissions, Access, Identities,<br>and Credentials | Coming<br>Soon! |
| Generate Dynamic Reports based on<br>human identities, cloud identities, access, <br>and permissions | Coming<br>Soon! |

## How to run

`python -m iambic.main import -c demo/config.yaml`

## Additional Documentation

* [Schemas](docs/SCHEMA.md)
* [Contributing](docs/CONTRIBUTING.md)
* [Docker](docs/DOCKER.md)

Dockerfile METASEP
ARG FUNCTION_DIR="/app/"
ARG AWS_LINUX_VERSION="2022"
ARG PYTHON_VERSION="3.10.8"


FROM amazonlinux:${AWS_LINUX_VERSION} as python-layer

ARG PYTHON_VERSION

# install python
RUN yum update -y
RUN yum groupinstall "Development Tools" -y
RUN yum install libffi-devel bzip2-devel wget openssl openssl-devel sqlite-devel -y
RUN wget https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz
RUN tar -xf Python-${PYTHON_VERSION}.tgz
RUN cd Python-${PYTHON_VERSION}/ && \
    ./configure --enable-optimizations --enable-loadable-sqlite-extensions && \
    make install
RUN ln -s /Python-${PYTHON_VERSION}/python /usr/bin/python
RUN python -m pip install --upgrade pip


FROM amazonlinux:${AWS_LINUX_VERSION} as base-layer

RUN yum groupinstall "Development Tools" -y
RUN yum install git -y

# copy over python
ARG PYTHON_VERSION
COPY --from=python-layer /Python-${PYTHON_VERSION} /Python-${PYTHON_VERSION}
COPY --from=python-layer /usr/local/bin /usr/local/bin
COPY --from=python-layer /usr/local/lib /usr/local/lib
RUN ln -s /Python-${PYTHON_VERSION}/python /usr/bin/python

ARG FUNCTION_DIR
RUN mkdir -p ${FUNCTION_DIR}
WORKDIR ${FUNCTION_DIR}


FROM base-layer as build-layer

######## YOUR OWN SETUP PROCESS HERE ########################
# copy over requirements and install those
# COPY . .
# RUN pip install . --target "${FUNCTION_DIR}"

# copy over configuration and service code then install it
# COPY config/ ${FUNCTION_DIR}/config/
# COPY service/ ${FUNCTION_DIR}/
# RUN pip install . --target "${FUNCTION_DIR}"
######## ########################### ########################

# install lambda runtime interface client for python
RUN pip install awslambdaric --target "${FUNCTION_DIR}"


FROM base-layer as runtime-layer

# copy in the built dependencies
ARG FUNCTION_DIR
COPY --from=build-layer ${FUNCTION_DIR} ${FUNCTION_DIR}
WORKDIR ${FUNCTION_DIR}

# (optional) add lambda runtime interface emulator
ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie /usr/bin/aws-lambda-rie
RUN chmod 755 /usr/bin/aws-lambda-rie
# ENTRYPOINT [ "/usr/bin/aws-lambda-rie", "python", "-m", "awslambdaric" ]
ENTRYPOINT [ "python", "-m", "awslambdaric" ]

# ######## REFERENCE YOUR OWN HANDLER HERE ########################
# CMD [ "main.app" ]```
######## ############################### ########################
# Install Requirements
# Install the function's dependencies
RUN pip3 install poetry awslambdaric argh watchdog
WORKDIR ${LAMBDA_TASK_ROOT}
COPY pyproject.toml ${LAMBDA_TASK_ROOT}
# Do not create virtualenv
RUN poetry config virtualenvs.create false
# Only install dependencies
RUN poetry install --no-root

# Copy function code
COPY . ${LAMBDA_TASK_ROOT}

# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile)
CMD [ "python", "-m", "iambic.lambda.app.handler" ]
iambic/slack/notifications.py METASEP
from iambic.config.models import Config


async def send_iam_mutation_message(
    config,
    identity: str,
    actor: str,
    event_source: str,
    event_name: str,
    session_name: str,
    cloudtrail_event: dict,
):
    event = event_source.split(".")[0] + ":" + event_name
    blocks = [
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": "An unauthorized Cloud Identity Modification was detected and automatically remediated.\n\n*<fakeLink.toEmployeeProfile.com|Click here to view CloudTrail Logs>*",
            },
        },
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": f"*Identity:*\t<fakeLink.toNoqRole.com|{identity}>",
            },
        },
        {"type": "section", "text": {"type": "mrkdwn", "text": f"*Action:*\t{event}"}},
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": f"*Actor:*\t<fakeLink.toNoqRole.com|{actor}>",
            },
        },
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": f"*Session Name:*\t{session_name}",
            },
        },
        {
            "type": "actions",
            "elements": [
                {
                    "type": "button",
                    "text": {
                        "type": "plain_text",
                        "emoji": True,
                        "text": "Approve and Submit Request",
                    },
                    "style": "primary",
                    "value": "approve",
                },
                {
                    "type": "button",
                    "text": {"type": "plain_text", "emoji": True, "text": "Ignore"},
                    "style": "danger",
                    "value": "ignore",
                },
            ],
        },
    ]

    await send_slack_notification_to_channel(
        config,
        config.slack["alert_channel_id"],
        "An unauthorized Cloud Identity Modification was detected and automatically remediated.",
        blocks,
    )


async def send_slack_notification_to_channel(
    config: Config, channel_id, text, blocks
) -> bool:
    if not (slack_app := config.slack_app):
        return False

    slack_app.client.chat_postMessage(channel=channel_id, text=text, blocks=blocks)
    return True

iambic/slack/__init__.py METASEP

iambic/request_handler/git_apply.py METASEP
import asyncio

from iambic.config.models import Config
from iambic.core.context import ctx
from iambic.core.git import (
    create_templates_for_deleted_files,
    create_templates_for_modified_files,
    retrieve_git_changes,
)
from iambic.core.logger import log
from iambic.core.models import TemplateChangeDetails
from iambic.core.parser import load_templates


async def apply_git_changes(
    config_path: str, repo_dir: str
) -> list[TemplateChangeDetails]:
    """Retrieves files added/updated/or removed when comparing the current branch to master

    Works by taking the diff and adding implied changes to the templates that were modified.
    These are the changes and this function detects:
        Deleting a file
        Removing 1 or more aws_accounts from included_accounts
        Adding 1 or more aws_accounts to excluded_accounts

    :param config_path:
    :param repo_dir:
    :return:
    """
    config = Config.load(config_path)
    config.set_account_defaults()
    file_changes = await retrieve_git_changes(repo_dir)
    if (
        not file_changes["new_files"]
        and not file_changes["modified_files"]
        and not file_changes["deleted_files"]
    ):
        log.info("No changes found.")
        return []

    templates = load_templates(
        [git_diff.path for git_diff in file_changes["new_files"]]
    )
    templates.extend(create_templates_for_deleted_files(file_changes["deleted_files"]))
    templates.extend(
        create_templates_for_modified_files(config, file_changes["modified_files"])
    )

    template_changes = await asyncio.gather(
        *[template.apply(config) for template in templates]
    )
    template_changes = [
        template_change
        for template_change in template_changes
        if template_change.proposed_changes
    ]

    if ctx.execute and template_changes:
        log.info("Finished applying changes.")
    elif not ctx.execute:
        log.info("Finished scanning for changes.")
    else:
        log.info("No changes found.")

    return template_changes

iambic/request_handler/generate.py METASEP
import asyncio
import os

from iambic.aws.iam.role.template_generation import (
    ROLE_RESPONSE_DIR,
    generate_aws_role_templates,
)
from iambic.config.models import Config
from iambic.google.group.template_generation import generate_group_templates


async def generate_templates(configs: list[Config], output_dir: str):
    # TODO: Create a setting to enable support for google groups
    # TODO: Ensure google_groups are not excluded from sync
    response_dir_list = [output_dir, ROLE_RESPONSE_DIR]
    for response_dir in response_dir_list:
        os.makedirs(str(response_dir), exist_ok=True)

    tasks = [generate_aws_role_templates(configs, output_dir)]
    for config in configs:
        for project in config.google_projects:
            for subject in project.subjects:
                tasks.append(
                    generate_group_templates(
                        config, subject.domain, output_dir, project
                    )
                )

    await asyncio.gather(*tasks)

iambic/request_handler/detect.py METASEP
# Intentionally hacky for demo
import json

from iambic.config.models import Config
from iambic.core.logger import log
from iambic.slack.notifications import send_iam_mutation_message


async def detect_changes(config: Config) -> bool:
    queue_arn = config.sqs.get("queues", {}).get("iam_mutation", {}).get("arn", "")
    if not queue_arn:
        log.info("No queue arn found. Returning")
        return False

    queue_name = queue_arn.split(":")[-1]
    region_name = queue_arn.split(":")[3]
    session = config.get_boto_session_from_arn(queue_arn, region_name)
    identity = session.client("sts").get_caller_identity()
    identity_arn_with_session_name = (
        identity["Arn"].replace(":sts:", ":iam:").replace("assumed-role", "role")
    )
    # TODO: This only works for same account identities. We need to do similar to NoqMeter,
    # check all roles we have access to on all accounts, or store this in configuration.
    # Then exclude these from the list of roles to check.

    identity_arn = "/".join(identity_arn_with_session_name.split("/")[0:2])
    sqs = session.client("sqs", region_name=region_name)
    queue_url_res = sqs.get_queue_url(QueueName=queue_name)
    queue_url = queue_url_res.get("QueueUrl")
    messages = sqs.receive_message(QueueUrl=queue_url, MaxNumberOfMessages=10).get(
        "Messages", []
    )

    while messages:
        processed_messages = []
        for message in messages:
            try:
                processed_messages.append(
                    {
                        "Id": message["MessageId"],
                        "ReceiptHandle": message["ReceiptHandle"],
                    }
                )
                message_body = json.loads(message["Body"])
                try:
                    if "Message" in message_body:
                        decoded_message = json.loads(message_body["Message"])["detail"]
                    else:
                        decoded_message = message_body["detail"]
                except Exception as err:
                    log.debug(
                        "Unable to process message", error=str(err), message=message
                    )
                    processed_messages.append(
                        {
                            "Id": message["MessageId"],
                            "ReceiptHandle": message["ReceiptHandle"],
                        }
                    )
                    continue
                role_name = decoded_message["requestParameters"]["roleName"]
                role_account_id = decoded_message.get(
                    "account", decoded_message.get("recipientAccountId")
                )
                role_arn = f"arn:aws:iam::{role_account_id}:role/{role_name}"
                actor = (
                    decoded_message.get("userIdentity", {})
                    .get("sessionContext", {})
                    .get("sessionIssuer", {})
                    .get("arn", "")
                )
                session_name = (
                    decoded_message.get("userIdentity", {})
                    .get("principalId")
                    .split(":")[-1]
                )
                if actor != identity_arn:
                    await send_iam_mutation_message(
                        config=config,
                        identity=role_arn,
                        actor=actor,
                        event_source=decoded_message["eventSource"],
                        event_name=decoded_message["eventName"],
                        session_name=session_name,
                        cloudtrail_event=decoded_message,
                    )
            except Exception as err:
                log.debug("Unable to process message", error=str(err), message=message)
                continue

        sqs.delete_message_batch(QueueUrl=queue_url, Entries=processed_messages)
        messages = sqs.receive_message(QueueUrl=queue_url, MaxNumberOfMessages=10).get(
            "Messages", []
        )
    return True

iambic/request_handler/apply.py METASEP
import asyncio
import os

from iambic.aws.utils import remove_expired_resources
from iambic.config.models import Config
from iambic.core.context import ctx
from iambic.core.logger import log
from iambic.core.models import TemplateChangeDetails
from iambic.core.parser import load_templates
from iambic.core.utils import yaml


async def apply_changes(
    config: Config, template_paths: list[str]
) -> list[TemplateChangeDetails]:
    template_changes = await asyncio.gather(
        *[template.apply(config) for template in load_templates(template_paths)]
    )
    template_changes = [
        template_change
        for template_change in template_changes
        if template_change.proposed_changes
    ]

    if ctx.execute and template_changes:
        log.info("Finished applying changes.")
    elif not ctx.execute:
        log.info("Finished scanning for changes.")
    else:
        log.info("No changes found.")

    return template_changes


async def flag_expired_resources(template_paths: list[str]):
    log.info("Scanning for expired resources")
    templates = await asyncio.gather(
        *[
            remove_expired_resources(
                template, template.resource_type, template.resource_id
            )
            for template in load_templates(template_paths)
        ]
    )

    for template in templates:
        with open(
            os.path.join(os.path.dirname(__file__), template.file_path), "w"
        ) as f:
            f.write(yaml.dump(template.dict(exclude_none=True, exclude_unset=True)))

    log.info("Expired resource scan complete.")

iambic/request_handler/__init__.py METASEP

iambic/lambda/app.py METASEP
import base64
import os
import sys
import time
from enum import Enum

import boto3

from iambic.core.models import BaseModel
from iambic.core.utils import yaml
from iambic.main import (
    run_apply,
    run_clone_repos,
    run_detect,
    run_git_apply,
    run_import,
    run_plan,
)

CONFIG_PATH = os.path.expanduser("~/.iambic/config.yaml")
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
REPO_BASE_PATH = os.path.expanduser("~/.iambic/repos/")
os.makedirs(os.path.dirname(REPO_BASE_PATH), exist_ok=True)


class LambdaCommand(Enum):
    # `import` is reserved in Python. So we just prepend `run_` to everything.
    run_import = "import"
    run_plan = "plan"
    run_apply = "apply"
    run_detect = "detect"
    run_git_apply = "git_apply"
    run_clone_git_repos = "clone_git_repos"


class LambdaContext(BaseModel):
    command: str


def handler(event, context):
    return run_handler(event, context)


def run_handler(event=None, context=None):
    """
    Default handler for AWS Lambda. It is split out from the actual
    handler so we can also run via IDE run configurations
    """
    if not context:
        context = {"command": "import"}
    lambda_context = LambdaContext(**context)
    if not (iambic_config_path := os.getenv("IAMBIC_CONFIG")):
        raise NotImplementedError("You must set the IAMBIC_CONFIG environment variable")
    if not iambic_config_path.startswith("arn:aws:secretsmanager:"):
        raise NotImplementedError(
            "IAMBIC_CONFIG must be an ARN to a secret in AWS Secrets Manager"
        )
    iambic_assume_role = os.getenv("IAMBIC_CONFIG_ASSUME_ROLE")
    secret_arn = iambic_config_path
    region_name = secret_arn.split(":")[3]
    session = boto3.Session(region_name=region_name)
    if iambic_assume_role:
        sts = session.client("sts")
        role_params = dict(
            RoleArn=iambic_assume_role,
            RoleSessionName="iambic",
        )
        role = sts.assume_role(**role_params)
        session = boto3.Session(
            region_name=region_name,
            aws_access_key_id=role["Credentials"]["AccessKeyId"],
            aws_secret_access_key=role["Credentials"]["SecretAccessKey"],
            aws_session_token=role["Credentials"]["SessionToken"],
        )

    client = session.client(service_name="secretsmanager", region_name=region_name)
    get_secret_value_response = client.get_secret_value(SecretId=secret_arn)
    config_text = (
        get_secret_value_response["SecretString"]
        if "SecretString" in get_secret_value_response
        else base64.b64decode(get_secret_value_response["SecretBinary"])
    )
    config = yaml.load(config_text)
    with open(CONFIG_PATH, "w") as f:
        yaml.dump(config, f)
    match lambda_context.command:
        case LambdaCommand.run_import.value:
            return run_import((CONFIG_PATH,), REPO_BASE_PATH)
        case LambdaCommand.run_plan.value:
            return run_plan(CONFIG_PATH, None, REPO_BASE_PATH)
        case LambdaCommand.run_apply.value:
            return run_apply(True, CONFIG_PATH, None, REPO_BASE_PATH)
        case LambdaCommand.run_detect.value:
            return run_detect(
                CONFIG_PATH,
            )
        case LambdaCommand.run_git_apply.value:
            return run_git_apply(CONFIG_PATH, REPO_BASE_PATH)
        case LambdaCommand.run_clone_git_repos.value:
            return run_clone_repos(CONFIG_PATH, REPO_BASE_PATH)
        case _:
            raise NotImplementedError(f"Unknown command {lambda_context.command}")


if __name__ == "__main__":
    if len(sys.argv) < 2:
        raise Exception("You must pass a command")
    command = sys.argv[1]
    # `schedule` is just for development and demo purposes
    if command == "schedule":
        while True:
            run_handler(None, {"command": "clone_git_repos"})
            run_handler(None, {"command": "apply"})
            time.sleep(1)
    else:
        run_handler(None, {"command": command})

iambic/lambda/__init__.py METASEP

iambic/google/models.py METASEP
import asyncio
from enum import Enum

from iambic.aws.models import ExpiryModel
from iambic.config.models import Config, GoogleProject
from iambic.core.context import ctx
from iambic.core.logger import log
from iambic.core.models import AccountChangeDetails, BaseTemplate, TemplateChangeDetails


class WhoCanInvite(Enum):
    ALL_MANAGERS_CAN_INVITE = "ALL_MANAGERS_CAN_INVITE"
    ALL_MEMBERS_CAN_INVITE = "ALL_MEMBERS_CAN_INVITE"


class WhoCanJoin(Enum):
    ALL_IN_DOMAIN_CAN_JOIN = "ALL_IN_DOMAIN_CAN_JOIN"
    ANYONE_CAN_JOIN = "ANYONE_CAN_JOIN"
    CAN_REQUEST_TO_JOIN = "CAN_REQUEST_TO_JOIN"


class WhoCanPostMessage(Enum):
    ALL_IN_DOMAIN_CAN_POST = "ALL_IN_DOMAIN_CAN_POST"
    ALL_MANAGERS_CAN_POST = "ALL_MANAGERS_CAN_POST"
    ALL_MEMBERS_CAN_POST = "ALL_MEMBERS_CAN_POST"
    ANYONE_CAN_POST = "ANYONE_CAN_POST"
    NONE_CAN_POST = "NONE_CAN_POST"


class WhoCanViewGroup(Enum):
    ALL_IN_DOMAIN_CAN_VIEW = "ALL_IN_DOMAIN_CAN_VIEW"
    ALL_MANAGERS_CAN_VIEW = "ALL_MANAGERS_CAN_VIEW"
    ALL_MEMBERS_CAN_VIEW = "ALL_MEMBERS_CAN_VIEW"
    ANYONE_CAN_VIEW = "ANYONE_CAN_VIEW"


class WhoCanViewMembership(Enum):
    ALL_IN_DOMAIN_CAN_VIEW = "ALL_IN_DOMAIN_CAN_VIEW"
    ALL_MANAGERS_CAN_VIEW = "ALL_MANAGERS_CAN_VIEW"
    ALL_MEMBERS_CAN_VIEW = "ALL_MEMBERS_CAN_VIEW"
    ANYONE_CAN_VIEW = "ANYONE_CAN_VIEW"


class GroupMemberRole(Enum):
    OWNER = "OWNER"
    MANAGER = "MANAGER"
    MEMBER = "MEMBER"


class GroupMemberSubscription(Enum):
    EACH_EMAIL = "EACH_EMAIL"
    DIGEST = "DIGEST"
    ABRIDGED = "ABRIDGED"
    NO_EMAIL = "NO_EMAIL"


class Posting(Enum):
    ALLOWED = "ALLOWED"
    NOT_ALLOWED = "NOT_ALLOWED"
    MODERATED = "MODERATED"


class GroupMemberType(Enum):
    USER = "USER"
    GROUP = "GROUP"
    EXTERNAL = "EXTERNAL"


class GroupMemberStatus(Enum):
    ACTIVE = "ACTIVE"
    INACTIVE = "INACTIVE"
    PENDING = "PENDING"


class GoogleTemplate(BaseTemplate, ExpiryModel):
    async def _apply_to_account(
        self, google_project: GoogleProject
    ) -> AccountChangeDetails:
        raise NotImplementedError

    async def apply(self, config: Config) -> TemplateChangeDetails:
        tasks = []
        template_changes = TemplateChangeDetails(
            resource_id=self.email,
            resource_type=self.template_type,
            template_path=self.file_path,
        )
        log_params = dict(
            resource_type=self.resource_type, resource_id=self.resource_id
        )
        for account in config.google_projects:
            # if evaluate_on_google_account(self, account):
            if ctx.execute:
                log_str = "Applying changes to resource."
            else:
                log_str = "Detecting changes for resource."
            log.info(log_str, account=str(account), **log_params)
            tasks.append(self._apply_to_account(account))

        account_changes = await asyncio.gather(*tasks)
        template_changes.proposed_changes = [
            account_change
            for account_change in account_changes
            if any(account_change.proposed_changes)
        ]
        if account_changes and ctx.execute:
            log.info(
                "Successfully applied resource changes to all Google projects.",
                **log_params,
            )
        elif account_changes:
            log.info(
                "Successfully detected required resource changes on all Google projects.",
                **log_params,
            )
        else:
            log.debug("No changes detected for resource on any account.", **log_params)

        return template_changes

    @property
    def resource_id(self) -> str:
        return self.email

    @property
    def resource_type(self) -> str:
        return "google:group"

iambic/google/__init__.py METASEP

iambic/core/utils.py METASEP
import asyncio
import glob
import os
import pathlib
import re
from io import StringIO
from typing import Union

import aiofiles
from asgiref.sync import sync_to_async
from ruamel.yaml import YAML

from iambic.core import noq_json as json

NOQ_TEMPLATE_REGEX = r".*template_type:\n?.*NOQ::"


def camel_to_snake(str_obj: str) -> str:
    return re.sub("([a-z0-9])([A-Z])", r"\1_\2", str_obj).lower()


def camel_to_kebab(str_obj: str) -> str:
    return re.sub("([a-z0-9])([A-Z])", r"\1-\2", str_obj).lower()


def snake_to_camelback(str_obj: str) -> str:
    return re.sub(r"_([a-z])", lambda x: x.group(1).upper(), str_obj)


def snake_to_camelcap(str_obj: str) -> str:
    str_obj = camel_to_snake(
        str_obj
    ).title()  # normalize string and add required case convention
    return str_obj.replace("_", "")  # Remove underscores


async def resource_file_upsert(
    file_path: Union[str | pathlib.Path],
    content_as_dict: dict,
    replace_file: bool = False,
):
    if not replace_file and os.path.exists(file_path):
        async with aiofiles.open(file_path, mode="r") as f:
            content_dict = json.loads(await f.read())
            content_as_dict = {**content_dict, **content_as_dict}

    async with aiofiles.open(file_path, mode="w") as f:
        await f.write(json.dumps(content_as_dict, indent=2))


async def file_regex_search(file_path: str, re_pattern: str) -> Union[str | None]:
    async with aiofiles.open(file_path, mode="r") as f:
        file_content = await f.read()
        if re.search(re_pattern, file_content):
            return file_path


async def gather_templates(repo_dir: str, template_type: str = None) -> list[str]:
    if template_type and template_type.startswith("NOQ::"):
        # Strip the prefix so it plays nice with NOQ_TEMPLATE_REGEX
        template_type = template_type.replace("NOQ::", "")

    regex_pattern = (
        rf"{NOQ_TEMPLATE_REGEX}.*{template_type}"
        if template_type
        else NOQ_TEMPLATE_REGEX
    )
    file_paths = glob.glob(f"{repo_dir}/**/*.yaml", recursive=True)
    file_paths += glob.glob(f"{repo_dir}*.yaml", recursive=True)
    file_paths = await asyncio.gather(
        *[file_regex_search(fp, regex_pattern) for fp in file_paths]
    )
    return [fp for fp in file_paths if fp]


async def aio_wrapper(fnc, *args, **kwargs):
    thread_sensitive = kwargs.pop("thread_sensitive", False)
    return await sync_to_async(fnc, thread_sensitive=thread_sensitive)(*args, **kwargs)


class NoqYaml(YAML):
    def dump(self, data, stream=None, **kw):
        inefficient = False
        if stream is None:
            inefficient = True
            stream = StringIO()
        YAML.dump(self, data, stream, **kw)
        if inefficient:
            return stream.getvalue()


class NoqSemaphore:
    def __init__(
        self, callback_function: any, batch_size: int, callback_is_async: bool = True
    ):
        """Makes a reusable semaphore that wraps a provided function.
        Useful for batch processing things that could be rate limited.

        Example prints hello there 3 times in quick succession, waits 3 seconds then processes another 3:
            from datetime import datetime

            async def hello_there():
                print(f"Hello there - {datetime.utcnow()}")
                await asyncio.sleep(3)

            hello_there_semaphore = NoqSemaphore(hello_there, 3)
            asyncio.run(hello_there_semaphore.process([{} for _ in range(10)]))
        """
        self.limit = asyncio.Semaphore(batch_size)
        self.callback_function = callback_function
        self.callback_is_async = callback_is_async

    async def handle_message(self, **kwargs):
        async with self.limit:
            if self.callback_is_async:
                return await self.callback_function(**kwargs)

            return await aio_wrapper(self.callback_function, **kwargs)

    async def process(self, messages: list[dict]):
        return await asyncio.gather(
            *[asyncio.create_task(self.handle_message(**msg)) for msg in messages]
        )


typ = "rt"
yaml = NoqYaml(typ=typ)
yaml.preserve_quotes = True
yaml.indent(mapping=2, sequence=4, offset=2)
yaml.representer.ignore_aliases = lambda *data: True
yaml.width = 4096

iambic/core/template_generation.py METASEP
from collections import defaultdict
from typing import Union

import xxhash

from iambic.config.models import AWSAccount
from iambic.core import noq_json as json
from iambic.core.parser import load_templates
from iambic.core.utils import gather_templates


async def get_existing_template_file_map(repo_dir: str, template_type: str) -> dict:
    """Used to keep track of existing templates on import

     Write to the existing file before creating a new one.

    :param repo_dir:
    :param template_type:
    :return: {resource_id: file_path}
    """
    templates = load_templates(await gather_templates(repo_dir, template_type))
    return {template.resource_id: template.file_path for template in templates}


def templatize_resource(aws_account: AWSAccount, resource):
    resource_type = type(resource)

    if isinstance(resource, dict) or isinstance(resource, list):
        resource = json.dumps(resource)
    elif resource_type != str:
        resource = str(resource)

    resource = resource.replace(aws_account.account_id, "{{account_id}}")
    resource = resource.replace(aws_account.account_name, "{{account_name}}")
    for var in aws_account.variables:
        resource = resource.replace(var.value, "{{{}}}".format(var.key))

    return (
        json.loads(resource)
        if resource_type == dict or resource_type == list
        else resource_type(resource)
    )


def base_group_int_attribute(account_vals: dict) -> dict[int, list[dict[str, str]]]:
    """Groups an int attribute by a shared name across of aws_accounts

    Just call group_int_or_str_attribute

    :param account_vals: dict(resource_val: int = list[str])
    :return: dict(attribute_val: int = list[dict(account_id: str)])
    """
    response: dict[int, list[dict]] = defaultdict(list)

    for account_id, resource_val in account_vals.items():
        response[resource_val].append({"account_id": account_id})

    return response


async def base_group_str_attribute(
    aws_account_map: dict[str, AWSAccount], account_resources: list[dict]
) -> dict[str, list]:
    """Groups a string attribute by a shared name across of aws_accounts

    The ability to pass in and maintain arbitrary keys is necessary for
        parsing resource names related to a boto3 response

    Call group_int_or_str_attribute instead unless you need to transform this response.
    An example would be grouping role names for generating the template where you need to keep the file_path ref.

    :param aws_account_map: dict(account_id:str = AWSAccount)
    :param account_resources: list[dict(account_id:str, resources=list[dict(resource_val: str, **)])]
    :return: dict(attribute_val: str = list[dict(resource_val: str, account_id: str, **)])
    """

    """
    Create map with different representations of a resource value for each account

    The purpose is to add the 2 ways look-ups are done and maintain o(1) performance.
    The resource_val to the corresponding list element in account_resources[elem]["resources"]
        Under resource_val_map
    The reverse of resource_val_map which is an int representing the elem with a list of all resource_val reprs
        Under elem_resource_val_map
    """
    for account_resource_elem, account_resource in enumerate(account_resources):
        account_resources[account_resource_elem]["resource_val_map"] = dict()
        account_resources[account_resource_elem]["elem_resource_val_map"] = dict()
        aws_account = aws_account_map[account_resource["account_id"]]
        for resource_elem, resource in enumerate(account_resource["resources"]):
            account_resource["resources"][resource_elem][
                "account_id"
            ] = account_resource["account_id"]
            resource_val = resource["resource_val"]
            templatized_resource_val = templatize_resource(aws_account, resource_val)

            account_resources[account_resource_elem]["resource_val_map"][
                resource_val
            ] = resource_elem
            account_resources[account_resource_elem]["elem_resource_val_map"][
                resource_elem
            ] = [resource_val]
            if templatized_resource_val != resource_val:
                account_resources[account_resource_elem]["resource_val_map"][
                    templatized_resource_val
                ] = resource_elem
                account_resources[account_resource_elem]["elem_resource_val_map"][
                    resource_elem
                ].append(templatized_resource_val)

    grouped_resource_map = defaultdict(
        list
    )  # val:str = list(dict(name: str, path: str, account_id: str))
    # Iterate everything looking for shared names across aws_accounts
    for outer_elem in range(len(account_resources)):
        for resource_val, outer_resource_elem in account_resources[outer_elem][
            "resource_val_map"
        ].items():
            if outer_resource_elem is None:  # It hit on something already
                continue
            for inner_elem in range(outer_elem + 1, len(account_resources)):
                if (
                    inner_resource_elem := account_resources[inner_elem][
                        "resource_val_map"
                    ].get(resource_val)
                ) is not None:
                    if not grouped_resource_map.get(resource_val):
                        # Null out the outer_resource_elem in all the places
                        for rn in account_resources[outer_elem][
                            "elem_resource_val_map"
                        ][outer_resource_elem]:
                            account_resources[outer_elem]["resource_val_map"][rn] = None
                        account_resources[outer_elem]["elem_resource_val_map"][
                            outer_resource_elem
                        ] = []

                        grouped_resource_map[resource_val] = [
                            account_resources[inner_elem]["resources"][
                                inner_resource_elem
                            ],
                            account_resources[outer_elem]["resources"][
                                outer_resource_elem
                            ],
                        ]
                    else:
                        grouped_resource_map[resource_val].append(
                            account_resources[inner_elem]["resources"][
                                inner_resource_elem
                            ]
                        )

                    for rn in account_resources[inner_elem]["elem_resource_val_map"][
                        inner_resource_elem
                    ]:
                        account_resources[inner_elem]["resource_val_map"][rn] = None
                    account_resources[inner_elem]["elem_resource_val_map"][
                        inner_resource_elem
                    ] = []

    # Set the remaining attributes unique attributes
    for account_resource in account_resources:
        for elem, resource_vals in account_resource["elem_resource_val_map"].items():
            if not resource_vals:
                continue
            elif len(resource_vals) == 1:
                resource_val = resource_vals[0]
            else:
                # Take priority over raw output
                resource_val = [rv for rv in resource_vals if "{{" not in rv][0]

            account_resource["resources"][elem]["account_id"] = account_resource[
                "account_id"
            ]
            grouped_resource_map[resource_val] = [account_resource["resources"][elem]]

    return grouped_resource_map


async def base_group_dict_attribute(
    aws_account_map: dict[str, AWSAccount], account_resources: list[dict]
) -> list[dict]:
    """Groups an attribute that is a dict or list of dicts with matching aws_accounts

    Call group_dict_attribute instead unless you need to transform this response.
    An example would be tags which also contain role_access.

    :param aws_account_map: dict(account_id:str = AWSAccount)
    :param account_resources: list[dict(account_id:str, resources=list[dict])]
    :return: list[dict(included_accounts: str, resource_val=list[dict]|dict)]
    """
    """
    Create map with different representations of a resource value for each account

    Create a resource_hash to the corresponding list element in account_resources[elem]["resources"]
        Under resource_hash_map
    Create a reverse of resource_hash_map which is an int representing the elem with a list of all resource_hash reprs
        Under elem_resource_hash_map
    """
    hash_map = dict()

    for account_resource_elem, account_resource in enumerate(account_resources):
        account_resources[account_resource_elem]["resource_hash_map"] = dict()
        account_resources[account_resource_elem]["elem_resource_hash_map"] = dict()
        aws_account = aws_account_map[account_resource["account_id"]]
        for resource_elem, resource in enumerate(account_resource["resources"]):
            account_resource["resources"][resource_elem][
                "account_id"
            ] = account_resource["account_id"]
            # Set raw dict
            resource_hash = xxhash.xxh32(
                json.dumps(resource["resource_val"])
            ).hexdigest()
            hash_map[resource_hash] = resource["resource_val"]
            # Set dict with attempted interpolation
            templatized_dict = templatize_resource(
                aws_account, resource["resource_val"]
            )
            templatized_resource_hash = xxhash.xxh32(
                json.dumps(templatized_dict)
            ).hexdigest()
            hash_map[templatized_resource_hash] = templatized_dict
            # Define resource hash mappings
            account_resources[account_resource_elem]["resource_hash_map"][
                resource_hash
            ] = resource_elem
            account_resources[account_resource_elem]["elem_resource_hash_map"][
                resource_elem
            ] = [resource_hash]
            if templatized_resource_hash != resource_hash:
                account_resources[account_resource_elem]["resource_hash_map"][
                    templatized_resource_hash
                ] = resource_elem
                account_resources[account_resource_elem]["elem_resource_hash_map"][
                    resource_elem
                ].append(templatized_resource_hash)

    grouped_resource_map = (
        dict()
    )  # val:str = list(dict(name: str, path: str, account_id: str))
    # Iterate everything looking for shared names across aws_accounts
    for outer_elem in range(len(account_resources)):
        for resource_hash, outer_resource_elem in account_resources[outer_elem][
            "resource_hash_map"
        ].items():
            if outer_resource_elem is None:  # It hit on something already
                continue
            for inner_elem in range(outer_elem + 1, len(account_resources)):
                if (
                    inner_resource_elem := account_resources[inner_elem][
                        "resource_hash_map"
                    ].get(resource_hash)
                ) is not None:
                    if not grouped_resource_map.get(resource_hash):
                        grouped_resource_map[resource_hash] = {
                            "resource_val": hash_map[resource_hash],
                            "included_accounts": [
                                account_resources[inner_elem]["account_id"],
                                account_resources[outer_elem]["account_id"],
                            ],
                        }

                        # Null out the outer_resource_elem in all the places
                        for rn in account_resources[outer_elem][
                            "elem_resource_hash_map"
                        ][outer_resource_elem]:
                            account_resources[outer_elem]["resource_hash_map"][
                                rn
                            ] = None
                        account_resources[outer_elem]["elem_resource_hash_map"][
                            outer_resource_elem
                        ] = []
                    else:
                        grouped_resource_map[resource_hash]["included_accounts"].append(
                            account_resources[inner_elem]["account_id"]
                        )

                    # Null out the inner_resource_elem in all the places
                    for rn in account_resources[inner_elem]["elem_resource_hash_map"][
                        inner_resource_elem
                    ]:
                        account_resources[inner_elem]["resource_hash_map"][rn] = None
                    account_resources[inner_elem]["elem_resource_hash_map"][
                        inner_resource_elem
                    ] = []

    # Set the remaining attributes unique attributes
    for account_resource in account_resources:
        for elem, resource_hashes in account_resource["elem_resource_hash_map"].items():
            if not resource_hashes:
                continue
            elif len(resource_hashes) == 1:
                resource_hash = resource_hashes[0]
            else:
                # Take priority over raw output
                resource_hash = [
                    rv for rv in resource_hashes if "{{" not in str(hash_map[rv])
                ][0]

            grouped_resource_map[resource_hash] = {
                "resource_val": hash_map[resource_hash],
                "included_accounts": [account_resource["account_id"]],
            }

    return list(grouped_resource_map.values())


async def set_included_accounts_for_grouped_attribute(
    aws_account_map: dict[str, AWSAccount],
    number_of_accounts_resource_on: int,
    grouped_attribute,
) -> Union[list | dict]:
    """Takes a grouped attribute and formats its included aws_accounts to * or a list of account names

    :param aws_account_map: {account_id: aws_account}
    :param number_of_accounts_resource_on:
    :param grouped_attribute:
    :return:
    """
    if isinstance(grouped_attribute, dict):  # via base_group_str_attribute
        for k, resource_vals in grouped_attribute.items():
            if len(resource_vals) == number_of_accounts_resource_on:
                included_accounts = ["*"]
            else:
                included_accounts = [
                    aws_account_map[rv["account_id"]].account_name
                    for rv in resource_vals
                ]
            grouped_attribute[k] = included_accounts

        return grouped_attribute

    elif isinstance(grouped_attribute, list):  # Generated via base_group_dict_attribute
        for elem in range(len(grouped_attribute)):
            if (
                len(grouped_attribute[elem]["included_accounts"])
                == number_of_accounts_resource_on
            ):
                grouped_attribute[elem]["included_accounts"] = ["*"]
            else:
                included_accounts = [
                    aws_account_map[rv].account_name
                    for rv in grouped_attribute[elem]["included_accounts"]
                ]
                grouped_attribute[elem]["included_accounts"] = included_accounts

        return grouped_attribute


async def group_int_or_str_attribute(
    aws_account_map: dict[str, AWSAccount],
    number_of_accounts_resource_on: int,
    account_resources: Union[dict | list[dict]],
    key: Union[int | str],
) -> Union[int | str | list[dict]]:
    """Groups an attribute by aws_accounts, formats the attribute and normalizes the included aws_accounts.

    :param aws_account_map:
    :param number_of_accounts_resource_on:
    :param account_resources: dict(account_id: str = int_val) | list[dict(account_id:str, resources=list[dict])]
    :param key: Used to form the list[dict] response when there are multiple values for the attribute.
    :return:
    """
    if isinstance(account_resources, list):
        grouped_attribute = await base_group_str_attribute(
            aws_account_map, account_resources
        )
    else:
        grouped_attribute = base_group_int_attribute(account_resources)

    if len(grouped_attribute) == 1:
        return list(grouped_attribute.keys())[0]

    response = []
    grouped_attribute = await set_included_accounts_for_grouped_attribute(
        aws_account_map, number_of_accounts_resource_on, grouped_attribute
    )

    for resource_val, included_accounts in grouped_attribute.items():
        if included_accounts[0] == "*":
            response.append({key: resource_val})
        else:
            response.append({key: resource_val, "included_accounts": included_accounts})

    return response


async def group_dict_attribute(
    aws_account_map: dict[str, AWSAccount],
    number_of_accounts_resource_on: int,
    account_resources: list[dict],
    is_dict_attr: bool = True,
) -> Union[dict | list[dict]]:
    """Groups an attribute by aws_accounts, formats the attribute and normalizes the included aws_accounts.

    :param aws_account_map: {account_id: aws_account}
    :param number_of_accounts_resource_on:
    :param account_resources: list[dict(account_id:str, resources=list[dict])]
    :param is_dict_attr: If false and only one hit, still return as a list. Useful for things like inline_policies.
    :return:
    """

    response = []
    grouped_attributes = await set_included_accounts_for_grouped_attribute(
        aws_account_map,
        number_of_accounts_resource_on,
        (await base_group_dict_attribute(aws_account_map, account_resources)),
    )

    if len(grouped_attributes) == 1 and is_dict_attr:
        attr_val = grouped_attributes[0]["resource_val"]
        included_accounts = grouped_attributes[0]["included_accounts"]
        if included_accounts != ["*"]:
            attr_val["included_accounts"] = included_accounts

        return attr_val

    for grouped_attr in grouped_attributes:
        attr_val = grouped_attr["resource_val"]
        included_accounts = grouped_attr["included_accounts"]
        if included_accounts != ["*"]:
            attr_val["included_accounts"] = included_accounts

        response.append(attr_val)

    return response

iambic/core/parser.py METASEP
from iambic.aws.models import AWSTemplate
from iambic.config.templates import TEMPLATE_TYPE_MAP
from iambic.core.logger import log
from iambic.core.utils import yaml


def load_templates(template_paths: list[str]) -> list[AWSTemplate]:
    templates = []

    for template_path in template_paths:
        try:
            template_dict = yaml.load(open(template_path))
            template_cls = TEMPLATE_TYPE_MAP[template_dict["template_type"]]
            templates.append(template_cls(file_path=template_path, **template_dict))
        except KeyError:
            log.critical(
                "Invalid template type",
                file_path=template_path,
                template_type=template_dict["template_type"],
            )
            raise

    return templates

iambic/core/noq_json.py METASEP
import json
from datetime import datetime
from decimal import Decimal
from uuid import UUID

import ujson
from deepdiff.model import PrettyOrderedSet


class SetEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (frozenset, set, PrettyOrderedSet)):
            return list(obj)
        if isinstance(obj, Decimal):
            return float(obj)
        if isinstance(obj, datetime):
            return obj.timestamp()
        if isinstance(obj, Exception):
            return str(obj)
        if isinstance(obj, UUID):
            return str(obj)
        return json.JSONEncoder.default(self, obj)


def dumps(
    obj: any,
    ensure_ascii: bool = True,
    double_precision: int = 9,
    encode_html_chars: bool = False,
    escape_forward_slashes: bool = False,
    sort_keys: bool = False,
    indent: int = 0,
    **kwargs
) -> str:
    # Try fast "ujson is 3x faster than the standard json library" first
    try:
        result = ujson.dumps(
            obj,
            ensure_ascii,
            double_precision,
            encode_html_chars,
            escape_forward_slashes,
            sort_keys,
            indent,
            **kwargs
        )
    except TypeError:
        result = json.dumps(
            obj,
            cls=SetEncoder,
            ensure_ascii=ensure_ascii,
            sort_keys=sort_keys,
            indent=indent,
            **kwargs
        )
    return result


def loads(s: str, **kwargs) -> any:
    try:
        result = ujson.loads(s, **kwargs)
    except ValueError:
        result = json.loads(s, **kwargs)
    return result

iambic/core/models.py METASEP
import json
from enum import Enum
from typing import Optional, Union

from jinja2 import BaseLoader, Environment
from pydantic import BaseModel as PydanticBaseModel
from pydantic import Field

from iambic.aws.utils import apply_to_account
from iambic.config.models import AWSAccount, Config
from iambic.core.utils import snake_to_camelcap, yaml


class BaseModel(PydanticBaseModel):
    def get_attribute_val_for_account(
        self, aws_account: AWSAccount, attr: str, as_boto_dict: bool = True
    ):
        attr_val = getattr(self, attr)

        if as_boto_dict and hasattr(attr_val, "_apply_resource_dict"):
            return attr_val._apply_resource_dict(aws_account)
        elif not isinstance(attr_val, list):
            return attr_val

        matching_definitions = [
            val for val in attr_val if apply_to_account(val, aws_account)
        ]
        if len(matching_definitions) == 0:
            # Fallback to the default definition
            return self.__fields__[attr].default
        elif as_boto_dict:
            return [
                match._apply_resource_dict(aws_account)
                if hasattr(match, "_apply_resource_dict")
                else match
                for match in matching_definitions
            ]
        else:
            return matching_definitions

    def _apply_resource_dict(self, aws_account: AWSAccount = None) -> dict:
        exclude_keys = {
            "deleted",
            "expires_at",
            "included_accounts",
            "excluded_accounts",
            "included_orgs",
            "excluded_orgs",
            "owner",
            "template_type",
            "file_path",
        }
        exclude_keys.update(self.exclude_keys)

        if aws_account:
            resource_dict = {
                k: self.get_attribute_val_for_account(aws_account, k)
                for k in self.__dict__.keys()
                if k not in exclude_keys
            }
            resource_dict = {k: v for k, v in resource_dict.items() if bool(v)}
        else:
            resource_dict = self.dict(
                exclude=exclude_keys, exclude_none=True, exclude_unset=False
            )

        return {self.case_convention(k): v for k, v in resource_dict.items()}

    def apply_resource_dict(self, aws_account: AWSAccount) -> dict:
        response = self._apply_resource_dict(aws_account)
        variables = {var.key: var.value for var in aws_account.variables}
        variables["account_id"] = aws_account.account_id
        variables["account_name"] = aws_account.account_name
        if hasattr(self, "owner") and (owner := getattr(self, "owner", None)):
            variables["owner"] = owner

        rtemplate = Environment(loader=BaseLoader()).from_string(json.dumps(response))
        data = rtemplate.render(**variables)
        return json.loads(data)

    @property
    def exclude_keys(self) -> set:
        return set()

    @property
    def case_convention(self):
        return snake_to_camelcap

    @property
    def resource_type(self) -> str:
        raise NotImplementedError

    @property
    def resource_id(self) -> str:
        raise NotImplementedError


class ProposedChangeType(Enum):
    CREATE = "Create"
    UPDATE = "Update"
    DELETE = "Delete"
    ATTACH = "Attach"
    DETACH = "Detach"


class ProposedChange(PydanticBaseModel):
    change_type: ProposedChangeType
    attribute: Optional[str]
    resource_id: Optional[str]
    resource_type: Optional[str]
    current_value: Optional[Union[list | dict | str | int]]
    new_value: Optional[Union[list | dict | str | int]]
    change_summary: Optional[dict]


class AccountChangeDetails(PydanticBaseModel):
    account: Union[str | int]
    resource_id: Union[str | int]
    current_value: Optional[dict]
    new_value: Optional[dict]
    proposed_changes: list[ProposedChange] = Field(default=[])


class TemplateChangeDetails(PydanticBaseModel):
    resource_id: str
    resource_type: str
    template_path: str
    # Supports multi-account providers and single-account providers
    proposed_changes: Optional[list[AccountChangeDetails] | list[ProposedChange]]

    def dict(
        self,
        *,
        include: Optional[
            Union["AbstractSetIntStr", "MappingIntStrAny"]  # noqa
        ] = None,
        exclude: Optional[
            Union["AbstractSetIntStr", "MappingIntStrAny"]  # noqa
        ] = None,
        by_alias: bool = False,
        skip_defaults: Optional[bool] = None,
        exclude_unset: bool = True,
        exclude_defaults: bool = False,
        exclude_none: bool = True,
    ) -> "DictStrAny":  # noqa
        response = self.json(
            include=include,
            exclude=exclude,
            by_alias=by_alias,
            skip_defaults=skip_defaults,
            exclude_unset=exclude_unset,
            exclude_defaults=exclude_defaults,
            exclude_none=exclude_none,
        )
        return json.loads(response)


class BaseTemplate(BaseModel):
    template_type: str
    file_path: str
    read_only: Optional[bool] = Field(
        False,
        description="If set to True, iambic will only log drift instead of apply changes when drift is detected.",
    )

    def dict(
        self,
        *,
        include: Optional[
            Union["AbstractSetIntStr", "MappingIntStrAny"]  # noqa
        ] = None,
        exclude: Optional[
            Union["AbstractSetIntStr", "MappingIntStrAny"]  # noqa
        ] = None,
        by_alias: bool = False,
        skip_defaults: Optional[bool] = None,
        exclude_unset: bool = True,
        exclude_defaults: bool = False,
        exclude_none: bool = True,
    ) -> "DictStrAny":  # noqa
        if exclude:
            exclude.add("file_path")
        else:
            exclude = {"file_path"}

        template_dict = self.json(
            include=include,
            exclude=exclude,
            by_alias=by_alias,
            skip_defaults=skip_defaults,
            exclude_unset=exclude_unset,
            exclude_defaults=exclude_defaults,
            exclude_none=exclude_none,
        )
        template_dict = json.loads(template_dict)
        template_dict["template_type"] = self.template_type
        return template_dict

    def write(self):
        as_yaml = yaml.dump(self.dict())
        # Force template_type to be at the top of the yaml
        template_type_str = f"template_type: {self.template_type}"
        as_yaml = as_yaml.replace(f"\n{template_type_str}", "")
        as_yaml = f"{template_type_str}\n{as_yaml}"
        with open(self.file_path, "w") as f:
            f.write(as_yaml)

    async def apply(self, config: Config) -> TemplateChangeDetails:
        raise NotImplementedError

    @classmethod
    def load(cls, file_path: str):
        return cls(file_path=file_path, **yaml.load(open(file_path)))

iambic/core/logger.py METASEP
import logging
import os

import boto3
import structlog

LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
logging.basicConfig(level=LOG_LEVEL)
structlog.configure(
    processors=[
        structlog.processors.add_log_level,
        structlog.processors.StackInfoRenderer(),
        structlog.dev.set_exc_info,
        structlog.processors.TimeStamper("%Y/%m/%d %H:%M:%S", utc=False),
        structlog.dev.ConsoleRenderer(),
    ],
    wrapper_class=structlog.make_filtering_bound_logger(
        logging.getLevelName(LOG_LEVEL)
    ),
    context_class=dict,
    logger_factory=structlog.PrintLoggerFactory(),
    cache_logger_on_first_use=False,
)
log = structlog.get_logger("NoqForm")

if LOG_LEVEL == "DEBUG":
    boto3.set_stream_logger("", "DEBUG")
else:
    default_logging_levels = {
        "boto3": "CRITICAL",
        "boto": "CRITICAL",
        "botocore": "CRITICAL",
    }
    for logger, level in default_logging_levels.items():
        logging.getLogger(logger).setLevel(level)

iambic/core/git.py METASEP
import os
import re
from io import StringIO
from typing import Optional

from deepdiff import DeepDiff
from git import Repo
from git.exc import GitCommandError
from pydantic import BaseModel as PydanticBaseModel

from iambic.aws.models import Deleted
from iambic.config.models import Config
from iambic.config.templates import TEMPLATE_TYPE_MAP
from iambic.core.logger import log
from iambic.core.utils import NOQ_TEMPLATE_REGEX, file_regex_search, yaml


class GitDiff(PydanticBaseModel):
    path: str
    content: Optional[str] = None
    is_deleted: Optional[bool] = False


async def clone_git_repos(config, repo_base_path: str) -> None:
    # TODO: Formalize the model for secrets
    repos = {}
    for repository in config.secrets.get("git", {}).get("repositories", []):
        repo_name = repository["name"]
        git_uri = repository["uri"]
        repo_path = os.path.join(repo_base_path, repo_name)
        try:
            repo = Repo.clone_from(git_uri, repo_path)
            repos[repo_name] = repo
        except GitCommandError as e:
            if "already exists and is not an empty directory" not in e.stderr:
                raise
            repo = Repo(repo_path)
            for remote in repo.remotes:
                remote.fetch()
            repo.git.pull()
            repos[repo_name] = repo
    return repos


async def retrieve_git_changes(repo_dir: str) -> dict[str, list[GitDiff]]:
    repo = Repo(repo_dir)

    # Fetch latest
    for remote in repo.remotes:
        remote.fetch()
    # Last commit of the current branch
    commit_feature = repo.head.commit.tree
    # Comparing against main
    commit_origin_main = repo.commit("origin/main")
    # TODO: We should consider if the default branch is named other than `main`
    diff_index = commit_origin_main.diff(commit_feature)
    files = {
        "new_files": [],
        "deleted_files": [],
        "modified_files": [],
    }

    # Collect all new files
    for file_obj in diff_index.iter_change_type("A"):
        if (path := str(os.path.join(repo_dir, file_obj.b_path))).endswith(
            ".yaml"
        ) and (await file_regex_search(path, NOQ_TEMPLATE_REGEX)):
            file = GitDiff(path=str(os.path.join(repo_dir, path)))
            files["new_files"].append(file)

    # Collect all deleted files
    for file_obj in diff_index.iter_change_type("D"):
        if (path := file_obj.b_path).endswith(".yaml"):
            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                is_deleted=True,
            )
            if re.search(NOQ_TEMPLATE_REGEX, file.content):
                files["deleted_files"].append(file)

    # Collect all modified files
    for file_obj in diff_index.iter_change_type("M"):
        if (path := str(os.path.join(repo_dir, file_obj.b_path))).endswith(
            ".yaml"
        ) and (await file_regex_search(path, NOQ_TEMPLATE_REGEX)):
            if (
                main_path := str(os.path.join(repo_dir, file_obj.a_path))
            ) != path:  # File was renamed
                deleted_file = GitDiff(
                    path=str(os.path.join(repo_dir, main_path)),
                    content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                    is_deleted=True,
                )

                if re.search(NOQ_TEMPLATE_REGEX, deleted_file.content):
                    template_dict = yaml.load(open(path))
                    main_template_dict = yaml.load(StringIO(deleted_file.content))
                    if not DeepDiff(
                        template_dict, main_template_dict, ignore_order=True
                    ):
                        continue  # Just renamed but no file changes

                    template_cls = TEMPLATE_TYPE_MAP[
                        main_template_dict["template_type"]
                    ]
                    main_template = template_cls(
                        file_path=deleted_file.path, **main_template_dict
                    )
                    template = template_cls(file_path=path, **template_dict)
                    if main_template.resource_id != template.resource_id:
                        files["deleted_files"].append(deleted_file)
                        files["new_files"].append(GitDiff(path=path))
                        continue

            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
            )
            files["modified_files"].append(file)

    return files


def create_templates_for_deleted_files(deleted_files: list[GitDiff]) -> list:
    """
    Create a class instance of the deleted file content with its template type
    If it wasn't deleted, set it to deleted
    Add that instance to templates
    """
    templates = []
    for git_diff in deleted_files:
        template_dict = yaml.load(StringIO(git_diff.content))
        template_cls = TEMPLATE_TYPE_MAP[template_dict["template_type"]]
        template = template_cls(file_path=git_diff.path, **template_dict)
        if template.deleted is True:
            continue
        template.deleted = True
        log.info("Template marked as deleted", file_path=git_diff.path)
        templates.append(template)

    return templates


def create_templates_for_modified_files(
    config: Config, modified_files: list[GitDiff]
) -> list:

    """
    Create a class instance of the original file content and the new file content with its template type
    Check for aws_accounts that were removed from included_accounts or added to excluded_accounts
    Update the template to be applied to delete the role from the aws_accounts that hit on the above statement
    """
    templates = []
    for git_diff in modified_files:
        main_template_dict = yaml.load(StringIO(git_diff.content))
        template_cls = TEMPLATE_TYPE_MAP[main_template_dict["template_type"]]

        main_template = template_cls(file_path=git_diff.path, **main_template_dict)

        template_dict = yaml.load(open(git_diff.path))
        template = template_cls(file_path=git_diff.path, **template_dict)
        deleted_included_accounts = []
        # deleted_exclude_accounts are aws_accounts that are included in the current commit so can't be deleted
        deleted_exclude_accounts = [*template.included_accounts]
        deleted_exclude_accounts_str = "\n".join(deleted_exclude_accounts)

        # Catch aws_accounts that were in included aws_accounts but have been removed
        if "*" not in deleted_exclude_accounts:
            if "*" in main_template.included_accounts:
                """
                Catch aws_accounts that were implicitly removed from included_accounts.
                Example:
                    main branch included_accounts:
                        - *
                    current commit included_accounts:
                        - staging
                        - dev

                If config.aws_accounts included prod, staging, and dev this will catch that prod is no longer included.
                    This means marking prod for deletion as it has been implicitly deleted.
                """
                for aws_account in config.aws_accounts:
                    account_regex = (
                        rf"({aws_account.account_id}|{aws_account.account_name})"
                    )
                    if re.search(account_regex, deleted_exclude_accounts_str):
                        log.debug(
                            "Resource on account not marked deletion.",
                            account=account_regex,
                            template=git_diff.path,
                        )
                        continue

                    log.info(
                        "Marking resource for deletion on account.",
                        reason="Implicitly removed from included_accounts",
                        account=account_regex,
                        template=git_diff.path,
                    )
                    deleted_included_accounts.append(account_regex)
                    template.included_accounts.append(account_regex)
            else:
                """
                Catch aws_accounts that were explicitly removed from included_accounts.
                Example:
                    main branch included_accounts:
                        - prod
                        - staging
                        - dev
                    current commit included_accounts:
                        - staging
                        - dev

                This means marking prod for deletion as it has been implicitly deleted.
                """
                for account in main_template.included_accounts:
                    if re.search(account, deleted_exclude_accounts_str):
                        log.debug(
                            "Resource on account not marked deletion.",
                            account=account,
                            template=git_diff.path,
                        )
                        continue

                    log.info(
                        "Marking resource for deletion on account.",
                        reason="Explicitly removed from included_accounts.",
                        account=account,
                        template=git_diff.path,
                    )
                    deleted_included_accounts.append(account)
                    template.included_accounts.append(account)

        main_template_included_accounts_str = "\n".join(main_template.included_accounts)
        main_template_excluded_accounts_str = (
            "\n".join(main_template.excluded_accounts)
            if main_template.excluded_accounts
            else None
        )
        template_excluded_accounts = []
        """
        Catch aws_accounts that have been implicitly excluded.
        Example:
            included_accounts:
                - *
            main branch excluded_accounts: []
            current commit excluded_accounts:
                - prod

        If config.aws_accounts included prod, staging, and dev this will catch that prod is no longer included.
            This means marking prod for deletion as it has been implicitly deleted.
        """
        for account in template.excluded_accounts:
            if main_template_excluded_accounts_str and re.search(
                account, main_template_excluded_accounts_str
            ):
                # The account was already excluded so add it to the template_excluded_accounts
                log.debug(
                    "Resource already excluded on account.",
                    account=account,
                    template=git_diff.path,
                )
                template_excluded_accounts.append(account)
            elif (
                re.search(account, main_template_included_accounts_str)
                or "*" in main_template.included_accounts
            ):
                # The account was previously included so mark it for deletion
                log.info(
                    "Marking resource for deletion on account.",
                    reason="Account added to excluded_accounts for resource.",
                    account=account,
                    template=git_diff.path,
                )
                deleted_included_accounts.append(account)
                template.included_accounts.append(account)
            else:
                # The account wasn't included or excluded before this so add it back to template_excluded_accounts
                log.debug(
                    "Newly excluded account.", account=account, template=git_diff.path
                )
                template_excluded_accounts.append(account)

        template.excluded_accounts = template_excluded_accounts

        if deleted_included_accounts and template.deleted is not True:
            deleted_obj = Deleted(
                deleted=True,
                included_accounts=deleted_included_accounts,
                excluded_accounts=[ea for ea in deleted_exclude_accounts if ea != "*"],
            )
            if isinstance(template.deleted, bool):
                template.deleted = [deleted_obj]
            else:
                template.deleted.append(deleted_obj)

        templates.append(template)

    return templates

iambic/core/context.py METASEP
class ExecutionContext:
    eval_only: bool = False

    @property
    def execute(self):
        return not self.eval_only


ctx = ExecutionContext()

iambic/core/__init__.py METASEP

iambic/config/templates.py METASEP
from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.role.models import RoleTemplate
from iambic.google.group.models import GroupTemplate

TEMPLATES = [RoleTemplate, GroupTemplate, ManagedPolicyTemplate]
TEMPLATE_TYPE_MAP = {
    template.__fields__["template_type"].default: template for template in TEMPLATES
}

iambic/config/models.py METASEP
import base64
from enum import Enum
from typing import List, Optional

import boto3
import botocore
import googleapiclient.discovery
from google.oauth2 import service_account
from pydantic import BaseModel, Field, constr
from slack_bolt import App as SlackBoltApp

from iambic.aws.utils import RegionName
from iambic.core.logger import log
from iambic.core.utils import aio_wrapper, yaml


class Variable(BaseModel):
    key: str
    value: str


class AWSAccount(BaseModel):
    account_id: constr(min_length=12, max_length=12) = Field(
        None, description="The AWS Account ID"
    )
    org_id: Optional[str] = Field(
        None,
        description="A unique identifier designating the identity of the organization",
    )
    account_name: Optional[str] = None
    default_region: Optional[RegionName] = Field(
        RegionName.us_east_1,
        description="Default region to use when making AWS requests",
    )
    aws_profile: Optional[str] = Field(
        None,
        description="The AWS profile used when making calls to the account",
    )
    assume_role_arn: Optional[str] = Field(
        None,
        description="The role arn to assume into when making calls to the account",
    )
    external_id: Optional[str] = Field(
        None,
        description="The external id to use for assuming into a role when making calls to the account",
    )
    role_access_tag: Optional[str] = Field(
        None,
        description="The key of the tag used to store users and groups that can assume into the role the tag is on",
    )
    variables: Optional[List[Variable]] = Field(
        [],
        description="A list of variables to be used when creating templates",
    )
    boto3_session_map: Optional[dict] = None
    read_only: Optional[bool] = Field(
        False,
        description="If set to True, iambic will only log drift instead of apply changes when drift is detected.",
    )

    def get_boto3_session(self, region_name: str = None):
        region_name = region_name or self.default_region

        if self.boto3_session_map is None:
            self.boto3_session_map = {}
        elif boto3_session := self.boto3_session_map.get(region_name):
            return boto3_session

        if self.aws_profile:
            try:
                self.boto3_session_map[region_name] = boto3.Session(
                    profile_name=self.aws_profile, region_name=region_name
                )
            except Exception as err:
                log.exception(err)
            else:
                return self.boto3_session_map[region_name]

        session = boto3.Session(region_name=region_name)
        if self.assume_role_arn:
            try:
                sts = session.client("sts")
                role_params = dict(
                    RoleArn=self.assume_role_arn,
                    RoleSessionName="iambic",
                )
                if self.external_id:
                    role_params["ExternalId"] = self.external_id
                role = sts.assume_role(**role_params)
                self.boto3_session_map[region_name] = boto3.Session(
                    region_name=region_name,
                    aws_access_key_id=role["Credentials"]["AccessKeyId"],
                    aws_secret_access_key=role["Credentials"]["SecretAccessKey"],
                    aws_session_token=role["Credentials"]["SessionToken"],
                )
            except Exception as err:
                log.exception(err)
            else:
                return self.boto3_session_map[region_name]

        self.boto3_session_map[region_name] = session
        return self.boto3_session_map[region_name]

    def get_boto3_client(self, service: str, region_name: str = None):
        return self.get_boto3_session(region_name).client(
            service, config=botocore.client.Config(max_pool_connections=50)
        )

    def __str__(self):
        return f"{self.account_name} - ({self.account_id})"

    def __init__(self, **kwargs):
        super(AWSAccount, self).__init__(**kwargs)
        self.default_region = self.default_region.value


class GoogleSubjects(BaseModel):
    domain: str
    service_account: str


class GoogleProject(BaseModel):
    project_id: str
    project_name: Optional[str]
    subjects: list[GoogleSubjects]
    type: str
    private_key_id: str
    private_key: str
    client_email: str
    client_id: str
    auth_uri: str
    token_uri: str
    auth_provider_x509_cert_url: str
    client_x509_cert_url: str
    variables: Optional[List[Variable]] = Field(
        [],
        description="A list of variables to be used when creating templates",
    )
    read_only: Optional[bool] = Field(
        False,
        description="If set to True, iambic will only log drift instead of apply changes when drift is detected.",
    )
    _service_connection_map: dict = {}

    def __str__(self):
        if self.project_name:
            return f"{self.project_name} - ({self.project_id})"

        return self.project_id

    async def get_service_connection(
        self,
        service_name: str,
        service_path: str,
        domain: str,
        cache_discovery: bool = False,
    ):
        # sourcery skip: raise-specific-error
        key = f"{domain}:{service_name}:{service_path}"
        if service_conn := self._service_connection_map.get(key):
            return service_conn

        admin_credentials = service_account.Credentials.from_service_account_info(
            self.dict(
                include={
                    "type",
                    "project_id",
                    "private_key_id",
                    "private_key",
                    "client_email",
                    "client_id",
                    "auth_uri",
                    "token_uri",
                    "auth_provider_x509_cert_url",
                    "client_x509_cert_url",
                }
            ),
            scopes=[
                "https://www.googleapis.com/auth/admin.directory.user.security",
                "https://www.googleapis.com/auth/admin.reports.audit.readonly",
                "https://www.googleapis.com/auth/admin.directory.user",
                "https://www.googleapis.com/auth/admin.directory.group",
                "https://www.googleapis.com/auth/admin.directory.group.member",
            ],
        )

        admin_delegated_credentials = None

        for s in self.subjects:
            if s.domain == domain:
                admin_delegated_credentials = admin_credentials.with_subject(
                    s.service_account
                )
                break
        if not admin_delegated_credentials:
            raise Exception(f"Could not find service account for domain {domain}")

        self._service_connection_map[key] = await aio_wrapper(
            googleapiclient.discovery.build,
            service_name,
            service_path,
            credentials=admin_delegated_credentials,
            cache_discovery=cache_discovery,
            thread_sensitive=True,
        )
        return self._service_connection_map[key]


class ExtendsConfigKey(Enum):
    AWS_SECRETS_MANAGER = "AWS_SECRETS_MANAGER"


class ExtendsConfig(BaseModel):
    key: ExtendsConfigKey
    value: str
    assume_role_arn: Optional[str]
    external_id: Optional[str]


class Config(BaseModel):
    aws_accounts: List[AWSAccount]
    google_projects: List[GoogleProject] = []
    extends: List[ExtendsConfig] = []
    secrets: Optional[dict] = None
    role_access_tag: Optional[str] = Field(
        "noq-authorized",
        description="The key of the tag used to store users and groups that can assume into the role the tag is on",
    )
    variables: Optional[List[Variable]] = Field(
        [],
        description="A list of variables to be used when creating templates. "
        "These apply to all aws_accounts but can be overwritten by an account.",
    )
    slack_app: Optional[str] = None
    sqs: Optional[dict] = {}
    slack: Optional[dict] = {}

    class Config:
        arbitrary_types_allowed = True

    def set_account_defaults(self):
        for elem, account in enumerate(self.aws_accounts):
            if not account.role_access_tag:
                self.aws_accounts[elem].role_access_tag = self.role_access_tag

            for variable in self.variables:
                if variable.key not in [av.key for av in account.variables]:
                    self.aws_accounts[elem].variables.append(variable)

    def get_aws_secret(self, extend: ExtendsConfig) -> dict:
        """TODO: Secrets should be moved to the account to prevent an anti-pattern
        It also makes it required for every account to have access to the account the secret exists on
        Example: If the secret is in prod
          A build in the staging account won't work unless it has access to the prod secret
        """
        secret_arn = extend.value
        region_name = secret_arn.split(":")[3]
        session = boto3.Session(region_name=region_name)
        if extend.assume_role_arn:
            sts = session.client("sts")
            role_params = dict(
                RoleArn=extend.assume_role_arn,
                RoleSessionName="iambic",
            )
            if extend.external_id:
                role_params["ExternalId"] = self.external_id
            role = sts.assume_role(**role_params)
            session = boto3.Session(
                region_name=region_name,
                aws_access_key_id=role["Credentials"]["AccessKeyId"],
                aws_secret_access_key=role["Credentials"]["SecretAccessKey"],
                aws_session_token=role["Credentials"]["SessionToken"],
            )

        client = session.client(service_name="secretsmanager")
        get_secret_value_response = client.get_secret_value(SecretId=secret_arn)
        if "SecretString" in get_secret_value_response:
            return_val = get_secret_value_response["SecretString"]
        else:
            return_val = base64.b64decode(get_secret_value_response["SecretBinary"])

        return yaml.load(return_val)

    def configure_slack(self):
        if self.secrets and (
            slack_bot_token := self.secrets.get("slack", {}).get("bot_token")
        ):
            self.slack_app = SlackBoltApp(token=slack_bot_token)

    def configure_google(self):
        if self.secrets and (google_secrets := self.secrets.get("google")):
            self.google_projects = [GoogleProject(**x) for x in google_secrets]

    def combine_extended_configs(self):
        if self.extends:
            for extend in self.extends:
                if extend.key == ExtendsConfigKey.AWS_SECRETS_MANAGER:
                    for k, v in self.get_aws_secret(extend).items():
                        if not getattr(self, k):
                            setattr(self, k, v)

    def get_boto_session_from_arn(self, arn: str, region_name: str = None):
        region_name = region_name or arn.split(":")[3]
        account_id = arn.split(":")[4]
        aws_account_map = {account.account_id: account for account in self.aws_accounts}
        aws_account = aws_account_map[account_id]
        return aws_account.get_boto3_session(region_name)

    @classmethod
    def load(cls, file_path: str):
        c = cls(file_path=file_path, **yaml.load(open(file_path)))
        c.combine_extended_configs()
        c.configure_slack()
        c.configure_google()
        return c

iambic/config/__init__.py METASEP

iambic/aws/sqs/util.py METASEP

iambic/aws/sqs/__init__.py METASEP

iambic/main.py METASEP
import asyncio
import pathlib
import warnings

import click

from iambic.config.models import Config
from iambic.core import noq_json as json
from iambic.core.context import ctx
from iambic.core.git import clone_git_repos
from iambic.core.logger import log
from iambic.core.models import TemplateChangeDetails
from iambic.core.utils import gather_templates
from iambic.request_handler.apply import apply_changes, flag_expired_resources
from iambic.request_handler.detect import detect_changes
from iambic.request_handler.generate import generate_templates
from iambic.request_handler.git_apply import apply_git_changes

warnings.filterwarnings("ignore", category=FutureWarning, module="botocore.client")


def output_proposed_changes(template_changes: list[TemplateChangeDetails]):
    if template_changes:
        file_name = "proposed_changes.json"
        log.info(f"A detailed summary of descriptions was saved to {file_name}")

        with open(file_name, "w") as f:
            f.write(
                json.dumps(
                    [template_change.dict() for template_change in template_changes],
                    indent=2,
                )
            )


@click.group()
def cli():
    ...


@cli.command()
@click.option(
    "--config",
    "-c",
    "config_path",
    type=click.Path(exists=True),
    help="The config.yaml file path to apply. Example: ./prod/config.yaml",
)
@click.option(
    "--template",
    "-t",
    "templates",
    required=False,
    multiple=True,
    type=click.Path(exists=True),
    help="The template file path(s) to apply. Example: ./aws/roles/engineering.yaml",
)
@click.option(
    "--repo-dir",
    "-d",
    "repo_dir",
    required=False,
    type=click.Path(exists=True),
    help="The repo directory containing the templates. Example: ~/noq-templates",
)
def plan(config_path: str, templates: list[str], repo_dir: str):
    run_plan(config_path, templates, repo_dir)


def run_plan(config_path: str, templates: list[str], repo_dir: str):
    if not templates:
        templates = asyncio.run(gather_templates(repo_dir or str(pathlib.Path.cwd())))

    asyncio.run(flag_expired_resources(templates))

    config = Config.load(config_path)
    config.set_account_defaults()
    ctx.eval_only = True
    output_proposed_changes(asyncio.run(apply_changes(config, templates)))


@cli.command()
@click.option(
    "--config",
    "-c",
    "config_path",
    type=click.Path(exists=True),
    help="The config.yaml file path to apply. Example: ./prod/config.yaml",
)
def detect(config_path: str):
    run_detect(config_path)


def run_detect(config_path: str):
    config = Config.load(config_path)
    config.set_account_defaults()
    asyncio.run(detect_changes(config))


@cli.command()
@click.option(
    "--config",
    "-c",
    "config_path",
    type=click.Path(exists=True),
    help="The config.yaml file path to apply. Example: ./prod/config.yaml",
)
@click.option(
    "--repo_base_path",
    "-d",
    "repo_base_path",
    required=True,
    type=click.Path(exists=True),
    help="The repo base directory that should contain the templates. Example: ~/iambic/templates",
)
def clone_repos(config_path: str, repo_base_path: str):
    run_clone_repos(config_path, repo_base_path)


def run_clone_repos(config_path: str, repo_base_path: str):
    config = Config.load(config_path)
    config.set_account_defaults()
    asyncio.run(clone_git_repos(config, repo_base_path))


@cli.command()
@click.option(
    "--no-prompt",
    is_flag=True,
    show_default=True,
    help="Apply changes without asking for permission?",
)
@click.option(
    "--config",
    "-c",
    "config_path",
    type=click.Path(exists=True),
    help="The config.yaml file path to apply. Example: ./prod/config.yaml",
)
@click.option(
    "--template",
    "-t",
    "templates",
    required=False,
    multiple=True,
    type=click.Path(exists=True),
    help="The template file path(s) to apply. Example: ./aws/roles/engineering.yaml",
)
@click.option(
    "--repo-dir",
    "-d",
    "repo_dir",
    required=False,
    type=click.Path(exists=True),
    help="The repo directory containing the templates. Example: ~/noq-templates",
)
def apply(no_prompt: bool, config_path: str, templates: list[str], repo_dir: str):
    run_apply(no_prompt, config_path, templates, repo_dir)


def run_apply(no_prompt: bool, config_path: str, templates: list[str], repo_dir: str):
    if not templates:
        templates = asyncio.run(gather_templates(repo_dir or str(pathlib.Path.cwd())))

    config = Config.load(config_path)
    config.set_account_defaults()
    ctx.eval_only = not no_prompt
    template_changes = asyncio.run(apply_changes(config, templates))
    output_proposed_changes(template_changes)

    if ctx.eval_only and template_changes and click.confirm("Proceed?"):
        ctx.eval_only = False
        asyncio.run(apply_changes(config, templates))
    asyncio.run(detect_changes(config))


@cli.command(name="git-apply")
@click.option(
    "--config",
    "-c",
    "config_path",
    type=click.Path(exists=True),
    help="The config.yaml file path to apply. Example: ./prod/config.yaml",
)
@click.option(
    "--repo-dir",
    "-d",
    "repo_dir",
    required=False,
    type=click.Path(exists=True),
    help="The repo directory containing the templates. Example: ~/noq-templates",
)
def git_apply(config_path: str, repo_dir: str):
    run_git_apply(config_path, repo_dir)


def run_git_apply(config_path: str, repo_dir: str):
    template_changes = asyncio.run(
        apply_git_changes(config_path, repo_dir or str(pathlib.Path.cwd()))
    )
    output_proposed_changes(template_changes)


@cli.command(name="import")
@click.option(
    "--config",
    "-c",
    "config_paths",
    multiple=True,
    type=click.Path(exists=True),
    help="The config.yaml file paths. Example: ./prod/config.yaml",
)
@click.option(
    "--repo-dir",
    "-d",
    "repo_dir",
    required=False,
    type=click.Path(exists=True),
    help="The repo directory containing the templates. Example: ~/noq-templates",
)
def import_(config_paths: list[str], repo_dir: str):
    run_import(config_paths, repo_dir or str(pathlib.Path.cwd()))


def run_import(config_paths: list[str], repo_dir: str):
    configs = []
    for config_path in config_paths:
        config = Config.load(config_path)
        config.set_account_defaults()
        configs.append(config)
    asyncio.run(generate_templates(configs, repo_dir or str(pathlib.Path.cwd())))


if __name__ == "__main__":
    cli()

iambic/__init__.py METASEP

iambic/aws/iam/role/utils.py METASEP
import asyncio
import json
from typing import Union

from deepdiff import DeepDiff

from iambic.aws.utils import paginated_search
from iambic.core.context import ctx
from iambic.core.logger import log
from iambic.core.models import ProposedChange, ProposedChangeType
from iambic.core.utils import aio_wrapper


async def get_role_inline_policy_names(role_name: str, iam_client):
    return await paginated_search(
        iam_client.list_role_policies, "PolicyNames", RoleName=role_name
    )


async def get_role_instance_profiles(role_name: str, iam_client):
    return await paginated_search(
        iam_client.list_instance_profiles_for_role,
        "InstanceProfiles",
        RoleName=role_name,
    )


async def list_roles(iam_client):
    return await paginated_search(iam_client.list_roles, "Roles")


async def list_role_tags(role_name: str, iam_client):
    return await paginated_search(iam_client.list_role_tags, "Tags", RoleName=role_name)


async def get_role_policy(role_name: str, policy_name: str, iam_client):
    return await aio_wrapper(
        iam_client.get_role_policy, RoleName=role_name, PolicyName=policy_name
    )


async def get_role_inline_policies(
    role_name: str, iam_client, as_dict: bool = True
) -> Union[list | dict]:
    policy_names = await get_role_inline_policy_names(role_name, iam_client)
    policies = await asyncio.gather(
        *[
            get_role_policy(role_name, policy_name, iam_client)
            for policy_name in policy_names
        ]
    )
    if as_dict:
        return {policy["PolicyName"]: policy["PolicyDocument"] for policy in policies}
    else:
        return [
            {"PolicyName": policy["PolicyName"], **policy["PolicyDocument"]}
            for policy in policies
        ]


async def get_role_managed_policies(role_name: str, iam_client) -> list[dict[str, str]]:
    marker: dict[str, str] = {}
    policies = []

    while True:
        response = await aio_wrapper(
            iam_client.list_attached_role_policies, RoleName=role_name, **marker
        )
        policies.extend(response["AttachedPolicies"])

        if response["IsTruncated"]:
            marker["Marker"] = response["Marker"]
        else:
            break

    return policies


async def get_role(role_name: str, iam_client):
    try:
        current_role = (await aio_wrapper(iam_client.get_role, RoleName=role_name))[
            "Role"
        ]
        current_role["ManagedPolicies"] = await get_role_managed_policies(
            role_name, iam_client
        )
        current_role["InlinePolicies"] = await get_role_inline_policies(
            role_name, iam_client, as_dict=False
        )
    except iam_client.exceptions.NoSuchEntityException:
        current_role = {}

    return current_role


async def apply_role_tags(
    role_name: str,
    iam_client,
    template_tags: list[dict],
    existing_tags: list[dict],
    log_params: dict,
) -> list[ProposedChange]:
    existing_tag_map = {tag["Key"]: tag["Value"] for tag in existing_tags}
    template_tag_map = {tag["Key"]: tag["Value"] for tag in template_tags}
    tags_to_apply = [
        tag for tag in template_tags if tag["Value"] != existing_tag_map.get(tag["Key"])
    ]
    tasks = []
    response = []

    if tags_to_remove := [
        tag["Key"] for tag in existing_tags if not template_tag_map.get(tag["Key"])
    ]:
        log_str = "Stale tags discovered."
        response.append(
            ProposedChange(
                change_type=ProposedChangeType.DETACH,
                attribute="tags",
                change_summary={"TagKeys": tags_to_remove},
            )
        )
        if ctx.execute:
            log_str = f"{log_str} Removing tags..."
            tasks.append(
                aio_wrapper(
                    iam_client.untag_role, RoleName=role_name, TagKeys=tags_to_remove
                )
            )
        log.info(log_str, tags=tags_to_remove, **log_params)

    if tags_to_apply:
        log_str = "New tags discovered in AWS."
        for tag in tags_to_apply:
            response.append(
                ProposedChange(
                    change_type=ProposedChangeType.ATTACH,
                    attribute="tags",
                    new_value=tag,
                )
            )
        if ctx.execute:
            log_str = f"{log_str} Adding tags..."
            tasks.append(
                aio_wrapper(iam_client.tag_role, RoleName=role_name, Tags=tags_to_apply)
            )
        log.info(log_str, tags=tags_to_apply, **log_params)

    if tasks:
        await asyncio.gather(*tasks)

    return response


async def update_assume_role_policy(
    role_name,
    iam_client,
    template_policy_document: dict,
    existing_policy_document: str,
    log_params: dict,
) -> list[ProposedChange]:
    response = []
    policy_drift = None

    if existing_policy_document:
        if isinstance(existing_policy_document, str):
            existing_policy_document = json.loads(existing_policy_document)

        policy_drift = await aio_wrapper(
            DeepDiff,
            existing_policy_document,
            template_policy_document,
            ignore_order=True,
        )

    if not existing_policy_document or bool(policy_drift):
        log_str = "Changes to the AssumeRolePolicyDocument discovered."
        if policy_drift:
            response.append(
                ProposedChange(
                    change_type=ProposedChangeType.UPDATE,
                    attribute="assume_role_policy_document",
                    change_summary=policy_drift,
                    current_value=existing_policy_document,
                    new_value=template_policy_document,
                )
            )
        else:
            response.append(
                ProposedChange(
                    change_type=ProposedChangeType.CREATE,
                    attribute="assume_role_policy_document",
                    new_value=template_policy_document,
                )
            )

        if ctx.execute:
            boto_action = "Creating" if existing_policy_document else "Updating"
            log_str = f"{log_str} {boto_action} AssumeRolePolicyDocument..."
            await aio_wrapper(
                iam_client.update_assume_role_policy,
                RoleName=role_name,
                PolicyDocument=json.dumps(template_policy_document),
            )
        log.info(log_str, **log_params)

    return response


async def apply_role_managed_policies(
    role_name,
    iam_client,
    template_policies: list[dict],
    existing_policies: list[dict],
    log_params: dict,
) -> list[ProposedChange]:
    tasks = []
    response = []
    template_policies = [policy["PolicyArn"] for policy in template_policies]
    existing_managed_policies = [policy["PolicyArn"] for policy in existing_policies]

    # Create new managed policies
    new_managed_policies = [
        policy_arn
        for policy_arn in template_policies
        if policy_arn not in existing_managed_policies
    ]
    if new_managed_policies:
        log_str = "New managed policies discovered."
        for policy_arn in new_managed_policies:
            response.append(
                ProposedChange(
                    change_type=ProposedChangeType.ATTACH,
                    resource_id=policy_arn,
                    attribute="managed_policies",
                )
            )
        if ctx.execute:
            log_str = f"{log_str} Adding managed policies..."
            tasks = [
                aio_wrapper(
                    iam_client.attach_role_policy,
                    RoleName=role_name,
                    PolicyArn=policy_arn,
                )
                for policy_arn in new_managed_policies
            ]
        log.info(log_str, managed_policies=new_managed_policies, **log_params)

    # Delete existing managed policies not in template
    existing_managed_policies = [
        policy_arn
        for policy_arn in existing_managed_policies
        if policy_arn not in template_policies
    ]
    if existing_managed_policies:
        log_str = "Stale managed policies discovered."
        for policy_arn in existing_managed_policies:
            response.append(
                ProposedChange(
                    change_type=ProposedChangeType.DETACH,
                    resource_id=policy_arn,
                    attribute="managed_policies",
                )
            )
        if ctx.execute:
            log_str = f"{log_str} Removing managed policies..."
            tasks.extend(
                [
                    aio_wrapper(
                        iam_client.detach_role_policy,
                        RoleName=role_name,
                        PolicyArn=policy_arn,
                    )
                    for policy_arn in existing_managed_policies
                ]
            )
        log.info(log_str, managed_policies=existing_managed_policies, **log_params)

    if tasks:
        await asyncio.gather(*tasks)

    return response


async def apply_role_inline_policies(
    role_name,
    iam_client,
    template_policies: list[dict],
    existing_policies: list[dict],
    log_params: dict,
) -> list[ProposedChange]:
    tasks = []
    response = []
    template_policy_map = {
        policy["PolicyName"]: {k: v for k, v in policy.items() if k != "PolicyName"}
        for policy in template_policies
    }
    existing_policy_map = {
        policy["PolicyName"]: {k: v for k, v in policy.items() if k != "PolicyName"}
        for policy in existing_policies
    }

    for policy_name in existing_policy_map.keys():
        if not template_policy_map.get(policy_name):
            log_str = "Stale inline policies discovered."
            if ctx.execute:
                log_str = f"{log_str} Removing inline policy..."
                response.append(
                    ProposedChange(
                        change_type=ProposedChangeType.DELETE,
                        resource_id=policy_name,
                        attribute="inline_policies",
                    )
                )
                tasks.append(
                    aio_wrapper(
                        iam_client.delete_role_policy,
                        RoleName=role_name,
                        PolicyName=policy_name,
                    )
                )
            log.info(log_str, policy_name=policy_name, **log_params)

    for policy_name, policy_document in template_policy_map.items():
        existing_policy_doc = existing_policy_map.get(policy_name)
        policy_drift = None
        if existing_policy_doc:
            policy_drift = await aio_wrapper(
                DeepDiff,
                existing_policy_doc,
                policy_document,
                ignore_order=True,
            )

        if not existing_policy_doc or policy_drift:
            if policy_drift:
                log_params["policy_drift"] = policy_drift
                boto_action = "Updating"
                resource_existence = "Stale"
                response.append(
                    ProposedChange(
                        change_type=ProposedChangeType.UPDATE,
                        resource_id=policy_name,
                        attribute="inline_policies",
                        change_summary=policy_drift,
                        current_value=existing_policy_doc,
                        new_value=policy_document,
                    )
                )
            else:
                boto_action = "Creating"
                resource_existence = "New"
                response.append(
                    ProposedChange(
                        change_type=ProposedChangeType.CREATE,
                        resource_id=policy_name,
                        attribute="inline_policies",
                        new_value=policy_document,
                    )
                )

            log_str = f"{resource_existence} inline policies discovered."
            if ctx.execute:
                log_str = f"{log_str} {boto_action} inline policy..."
                tasks.append(
                    aio_wrapper(
                        iam_client.put_role_policy,
                        RoleName=role_name,
                        PolicyName=policy_name,
                        PolicyDocument=json.dumps(policy_document),
                    )
                )
            log.info(log_str, policy_name=policy_name, **log_params)

    if tasks:
        await asyncio.gather(*tasks)

    return response


async def delete_iam_role(role_name: str, iam_client, log_params: dict):
    instance_profiles = await get_role_instance_profiles(role_name, iam_client)

    tasks = []
    for instance_profile in instance_profiles:
        tasks.append(
            aio_wrapper(
                iam_client.remove_role_from_instance_profile,
                RoleName=role_name,
                InstanceProfileName=instance_profile["InstanceProfileName"],
            )
        )
    await asyncio.gather(*tasks)

    tasks = []
    for instance_profile in instance_profiles:
        tasks.append(
            aio_wrapper(
                iam_client.delete_instance_profile,
                InstanceProfileName=instance_profile["InstanceProfileName"],
            )
        )
    await asyncio.gather(*tasks)

    tasks = []
    # Detach managed policies
    managed_policies = await get_role_managed_policies(role_name, iam_client)
    managed_policies = [policy["PolicyArn"] for policy in managed_policies]
    log.info(
        "Detaching managed policies.", managed_policies=managed_policies, **log_params
    )
    for policy in managed_policies:
        tasks.append(
            aio_wrapper(
                iam_client.detach_role_policy, RoleName=role_name, PolicyArn=policy
            )
        )

    # Delete inline policies
    inline_policies = await get_role_inline_policies(role_name, iam_client)
    inline_policies = list(inline_policies.keys())
    log.info(
        "Deleting inline policies.", managed_policies=inline_policies, **log_params
    )
    for policy_name in inline_policies:
        tasks.append(
            aio_wrapper(
                iam_client.delete_role_policy,
                RoleName=role_name,
                PolicyName=policy_name,
            )
        )

    # Actually perform the deletion of Managed & Inline policies
    await asyncio.gather(*tasks)
    # Now that everything has been removed from the role, delete the role itself
    await aio_wrapper(iam_client.delete_role, RoleName=role_name)

iambic/aws/iam/role/template_generation.py METASEP
import os
import pathlib

import aiofiles

from iambic.aws.iam.role.models import RoleTemplate
from iambic.aws.iam.role.utils import (
    get_role_inline_policies,
    get_role_managed_policies,
    list_role_tags,
    list_roles,
)
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_dict_attribute,
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
    set_included_accounts_for_grouped_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

ROLE_RESPONSE_DIR = pathlib.Path.home().joinpath(".iambic", "resources", "aws", "roles")


def get_role_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "roles")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_role_file_path(role_dir: str, role_name: str):
    file_name = (
        role_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(role_dir, f"{file_name}.yaml"))


def get_account_role_resource_dir(account_id: str) -> str:
    account_role_response_dir = os.path.join(ROLE_RESPONSE_DIR, account_id)
    os.makedirs(account_role_response_dir, exist_ok=True)
    return account_role_response_dir


async def generate_account_role_resource_files(aws_account: AWSAccount) -> dict:
    account_role_response_dir = get_account_role_resource_dir(aws_account.account_id)
    role_resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, roles=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_roles = await list_roles(iam_client)

    log.info(
        "Retrieved AWS IAM Roles.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        role_count=len(account_roles),
    )

    for account_role in account_roles:
        role_path = os.path.join(
            account_role_response_dir, f'{account_role["RoleName"]}.json'
        )
        response["roles"].append(
            {
                "path": role_path,
                "name": account_role["RoleName"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(file_path=role_path, content_as_dict=account_role, replace_file=True)
        )

    await role_resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Roles.",
        account_id=aws_account.account_id,
        role_count=len(account_roles),
    )

    return response


async def set_role_resource_tags(
    role_name: str, role_resource_path: str, aws_account: AWSAccount
):
    iam_client = aws_account.get_boto3_client("iam")
    role_tags = await list_role_tags(role_name, iam_client)
    await resource_file_upsert(role_resource_path, {"Tags": role_tags}, False)


async def set_role_resource_inline_policies(
    role_name: str, role_resource_path: str, aws_account: AWSAccount
):
    iam_client = aws_account.get_boto3_client("iam")
    role_inline_policies = await get_role_inline_policies(role_name, iam_client)
    for k in role_inline_policies.keys():
        role_inline_policies[k]["policy_name"] = k

    role_inline_policies = list(role_inline_policies.values())
    await resource_file_upsert(
        role_resource_path, {"InlinePolicies": role_inline_policies}, False
    )


async def set_role_resource_managed_policies(
    role_name: str, role_resource_path: str, aws_account: AWSAccount
):
    iam_client = aws_account.get_boto3_client("iam")
    role_managed_policies = await get_role_managed_policies(role_name, iam_client)
    await resource_file_upsert(
        role_resource_path, {"ManagedPolicies": role_managed_policies}, False
    )


async def create_templated_role(  # noqa: C901
    global_config: Config,
    aws_account_map: dict[str, AWSAccount],
    role_name: str,
    role_refs: list[dict],
    role_dir: str,
    existing_template_map: dict,
):
    account_id_to_role_map = {}
    num_of_accounts = len(role_refs)
    for role_ref in role_refs:
        async with aiofiles.open(role_ref["path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_role_map[role_ref["account_id"]] = normalize_boto3_resp(
                content_dict
            )

    # Generate the params used for attribute creation
    role_template_params = {"role_name": role_name}
    path_resources = list()
    description_resources = list()
    managed_policy_resources = list()
    assume_role_policy_document_resources = list()
    inline_policy_document_resources = list()
    permissions_boundary_resources = list()
    tag_resources = list()
    max_session_duration_resources = dict()
    for account_id, role_dict in account_id_to_role_map.items():
        max_session_duration_resources[account_id] = role_dict["max_session_duration"]
        path_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": role_dict["path"]}],
            }
        )
        assume_role_policy_document_resources.append(
            {
                "account_id": account_id,
                "resources": [
                    {"resource_val": role_dict["assume_role_policy_document"]}
                ],
            }
        )

        if managed_policies := role_dict.get("managed_policies"):
            managed_policy_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": mp} for mp in managed_policies],
                }
            )

        if permissions_boundary := role_dict.get("permissions_boundary"):
            permissions_boundary_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": permissions_boundary}],
                }
            )

        if inline_policies := role_dict.get("inline_policies"):
            inline_policy_document_resources.append(
                {
                    "account_id": account_id,
                    "resources": [
                        {"resource_val": inline_policy}
                        for inline_policy in inline_policies
                    ],
                }
            )

        if tags := role_dict.get("tags"):
            tag_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": tag} for tag in tags],
                }
            )

        if description := role_dict.get("description"):
            description_resources.append(
                {"account_id": account_id, "resources": [{"resource_val": description}]}
            )

    if len(role_refs) != len(aws_account_map):
        role_template_params["included_accounts"] = [
            aws_account_map[role_ref["account_id"]].account_name
            for role_ref in role_refs
        ]

    path = await group_int_or_str_attribute(
        aws_account_map, num_of_accounts, path_resources, "path"
    )
    if path != "/":
        role_template_params["path"] = path

    max_session_duration = await group_int_or_str_attribute(
        aws_account_map,
        num_of_accounts,
        max_session_duration_resources,
        "max_session_duration",
    )
    if max_session_duration != 3600:
        role_template_params["max_session_duration"] = max_session_duration

    if assume_role_policy_document_resources:
        role_template_params[
            "assume_role_policy_document"
        ] = await group_dict_attribute(
            aws_account_map, num_of_accounts, assume_role_policy_document_resources
        )

    if permissions_boundary_resources:
        role_template_params["permissions_boundary"] = await group_dict_attribute(
            aws_account_map, num_of_accounts, permissions_boundary_resources
        )

    if description_resources:
        role_template_params["description"] = await group_int_or_str_attribute(
            aws_account_map, num_of_accounts, description_resources, "description"
        )

    if managed_policy_resources:
        role_template_params["managed_policies"] = await group_dict_attribute(
            aws_account_map, num_of_accounts, managed_policy_resources, False
        )

    if inline_policy_document_resources:
        role_template_params["inline_policies"] = await group_dict_attribute(
            aws_account_map, num_of_accounts, inline_policy_document_resources, False
        )

    if tag_resources:
        tags = []
        role_access = []
        tag_lists = await base_group_dict_attribute(aws_account_map, tag_resources)
        for tag_val in tag_lists:
            included_accounts = tag_val["included_accounts"]
            tag = tag_val["resource_val"]
            if tag["key"] == global_config.role_access_tag and tag["value"]:
                role_access.append(
                    {
                        "included_accounts": included_accounts,
                        "groups": tag["value"].split(":"),
                    }
                )
            elif tag["value"]:
                tags.append({"included_accounts": included_accounts, **tag})

        if tags:
            role_template_params[
                "tags"
            ] = await set_included_accounts_for_grouped_attribute(
                aws_account_map, num_of_accounts, tags
            )
            for elem in range(len(role_template_params["tags"])):
                if role_template_params["tags"][elem]["included_accounts"] == ["*"]:
                    role_template_params["tags"][elem].pop("included_accounts")

        if role_access:
            role_template_params[
                "role_access"
            ] = await set_included_accounts_for_grouped_attribute(
                aws_account_map, num_of_accounts, role_access
            )
            for elem in range(len(role_template_params["role_access"])):
                if role_template_params["role_access"][elem]["included_accounts"] == [
                    "*"
                ]:
                    role_template_params["role_access"][elem].pop("included_accounts")

    try:
        role = RoleTemplate(
            file_path=existing_template_map.get(
                role_name, get_templated_role_file_path(role_dir, role_name)
            ),
            **role_template_params,
        )
        role.write()
    except Exception as err:
        log.info(str(err), role_params=role_template_params)


async def generate_aws_role_templates(configs: list[Config], base_output_dir: str):
    aws_account_map = get_aws_account_map(configs)
    existing_template_map = await get_existing_template_file_map(
        base_output_dir, "AWS::IAM::Role"
    )
    role_dir = get_role_dir(base_output_dir)
    generate_account_role_resource_files_semaphore = NoqSemaphore(
        generate_account_role_resource_files, 5
    )
    set_role_resource_inline_policies_semaphore = NoqSemaphore(
        set_role_resource_inline_policies, 20
    )
    set_role_resource_managed_policies_semaphore = NoqSemaphore(
        set_role_resource_managed_policies, 30
    )
    set_role_resource_tags_semaphore = NoqSemaphore(set_role_resource_tags, 75)

    log.info("Generating AWS role templates.")
    log.info(
        "Beginning to retrieve AWS IAM Roles.", accounts=list(aws_account_map.keys())
    )

    account_roles = await generate_account_role_resource_files_semaphore.process(
        [{"aws_account": aws_account} for aws_account in aws_account_map.values()]
    )
    messages = []
    for account_role in account_roles:
        for role in account_role["roles"]:
            messages.append(
                {
                    "role_name": role["name"],
                    "role_resource_path": role["path"],
                    "aws_account": aws_account_map[account_role["account_id"]],
                }
            )

    log.info("Setting inline policies in role templates")
    await set_role_resource_inline_policies_semaphore.process(messages)
    log.info("Setting managed policies in role templates")
    await set_role_resource_managed_policies_semaphore.process(messages)
    log.info("Setting tags in role templates")
    await set_role_resource_tags_semaphore.process(messages)
    log.info("Finished retrieving role details")

    # Use these for testing `create_templated_role`
    # account_role_output = json.dumps(account_roles)
    # with open("account_role_output.json", "w") as f:
    #     f.write(account_role_output)
    # with open("account_role_output.json") as f:
    #     account_roles = json.loads(f.read())

    log.info("Grouping roles")
    # Move everything to required structure
    for account_role_elem in range(len(account_roles)):
        for role_elem in range(len(account_roles[account_role_elem]["roles"])):
            role_name = account_roles[account_role_elem]["roles"][role_elem].pop("name")
            account_roles[account_role_elem]["roles"][role_elem][
                "resource_val"
            ] = role_name

        account_roles[account_role_elem]["resources"] = account_roles[
            account_role_elem
        ].pop("roles", [])

    grouped_role_map = await base_group_str_attribute(aws_account_map, account_roles)

    log.info("Writing templated roles")
    for role_name, role_refs in grouped_role_map.items():
        await create_templated_role(
            configs[0],
            aws_account_map,
            role_name,
            role_refs,
            role_dir,
            existing_template_map,
        )

    log.info("Finished templated role generation")

iambic/aws/iam/role/models.py METASEP
import asyncio
import json
from itertools import chain
from typing import Optional, Union

from pydantic import Field, constr

from iambic.aws.iam.models import Description, MaxSessionDuration, Path
from iambic.aws.iam.policy.models import (
    AssumeRolePolicyDocument,
    ManagedPolicyRef,
    PolicyDocument,
)
from iambic.aws.iam.role.utils import (
    apply_role_inline_policies,
    apply_role_managed_policies,
    apply_role_tags,
    delete_iam_role,
    get_role,
    update_assume_role_policy,
)
from iambic.aws.models import ARN_RE, AccessModel, AWSTemplate, ExpiryModel, Tag
from iambic.aws.utils import apply_to_account
from iambic.config.models import AWSAccount
from iambic.core.context import ctx
from iambic.core.logger import log
from iambic.core.models import AccountChangeDetails, ProposedChange, ProposedChangeType
from iambic.core.utils import aio_wrapper


class RoleAccess(ExpiryModel, AccessModel):
    users: list[str] = Field(
        [],
        description="List of users who can assume into the role",
    )
    groups: list[str] = Field(
        [],
        description="List of groups. Users in one or more of the groups can assume into the role",
    )

    @property
    def resource_type(self):
        return "aws:iam:role_access"

    @property
    def resource_id(self):
        return


class PermissionBoundary(ExpiryModel, AccessModel):
    permissions_boundary_type: str
    permissions_boundary_arn: constr(regex=ARN_RE)

    @property
    def resource_type(self):
        return "aws:iam:permission_boundary"

    @property
    def resource_id(self):
        return self.permissions_boundary_arn


class RoleTemplate(AWSTemplate, AccessModel):
    template_type = "NOQ::AWS::IAM::Role"
    role_name: str = Field(
        description="Name of the role",
    )
    description: Optional[Union[str, list[Description]]] = Field(
        "",
        description="Description of the role",
    )
    owner: Optional[str] = None
    max_session_duration: Optional[Union[int, list[MaxSessionDuration]]] = 3600
    path: Optional[Union[str, list[Path]]] = "/"
    permissions_boundary: Optional[
        None | PermissionBoundary | list[PermissionBoundary]
    ] = None
    role_access: Optional[list[RoleAccess]] = Field(
        [],
        description="List of users and groups who can assume into the role",
    )
    assume_role_policy_document: Optional[
        None | AssumeRolePolicyDocument | list[AssumeRolePolicyDocument]
    ] = None
    tags: Optional[list[Tag]] = Field(
        [],
        description="List of tags attached to the role",
    )
    managed_policies: Optional[list[ManagedPolicyRef]] = Field(
        [],
        description="Managed policy arns attached to the role",
    )
    inline_policies: Optional[list[PolicyDocument]] = Field(
        [],
        description="List of the role's inline policies",
    )

    def _apply_resource_dict(self, aws_account: AWSAccount = None) -> dict:
        response = super(RoleTemplate, self)._apply_resource_dict(aws_account)
        response.pop("RoleAccess", None)
        if "Tags" not in response:
            response["Tags"] = []

        # Add RoleAccess Tag to role tags
        role_access = [
            ra._apply_resource_dict(aws_account)
            for ra in self.role_access
            if apply_to_account(ra, aws_account)
        ]
        if role_access:
            value = []
            for role_access_dict in role_access:
                value.extend(role_access_dict.get("Users", []))
                value.extend(role_access_dict.get("Groups", []))
            response["Tags"].append(
                {"Key": aws_account.role_access_tag, "Value": ":".join(value)}
            )

        # Ensure only 1 of the following objects
        # TODO: Have this handled in a cleaner way. Maybe via an attribute on a pydantic field
        if assume_role_policy := response.pop("AssumeRolePolicyDocument", []):
            if isinstance(assume_role_policy, list):
                assume_role_policy = assume_role_policy[0]
            response["AssumeRolePolicyDocument"] = assume_role_policy

        if permissions_boundary := response.pop("PermissionsBoundary", []):
            if isinstance(permissions_boundary, list):
                permissions_boundary = permissions_boundary[0]
            response["PermissionsBoundary"] = permissions_boundary

        if isinstance(response.get("Description"), list):
            response["Description"] = response["Description"][0]["Description"]

        if isinstance(response.get("MaxSessionDuration"), list):
            response["MaxSessionDuration"] = response["MaxSessionDuration"][0][
                "MaxSessionDuration"
            ]

        return response

    def _is_read_only(self, aws_account: AWSAccount):
        return (
            "aws-service-role" in self.path or aws_account.read_only or self.read_only
        )

    async def _apply_to_account(  # noqa: C901
        self, aws_account: AWSAccount
    ) -> AccountChangeDetails:
        boto3_session = aws_account.get_boto3_session()
        client = boto3_session.client("iam")
        account_role = self.apply_resource_dict(aws_account)
        role_name = account_role["RoleName"]
        account_change_details = AccountChangeDetails(
            account=str(aws_account),
            resource_id=role_name,
            new_value=dict(**account_role),
            proposed_changes=[],
        )
        log_params = dict(
            resource_type=self.resource_type,
            resource_id=role_name,
            account=str(aws_account),
        )
        read_only = self._is_read_only(aws_account)

        current_role = await get_role(role_name, client)
        if current_role:
            account_change_details.current_value = {**current_role}  # Create a new dict

        deleted = self.get_attribute_val_for_account(aws_account, "deleted", False)
        if isinstance(deleted, list):
            deleted = deleted[0].deleted

        if deleted:
            if current_role:
                account_change_details.new_value = None
                account_change_details.proposed_changes.append(
                    ProposedChange(
                        change_type=ProposedChangeType.DELETE,
                        resource_id=role_name,
                        resource_type=self.resource_type,
                    )
                )
                log_str = "Active resource found with deleted=false."
                if ctx.execute and not read_only:
                    log_str = f"{log_str} Deleting resource..."
                log.info(log_str, **log_params)

                if ctx.execute:
                    await delete_iam_role(role_name, client, log_params)

            return account_change_details

        role_exists = bool(current_role)
        inline_policies = account_role.pop("InlinePolicies", [])
        managed_policies = account_role.pop("ManagedPolicies", [])
        existing_inline_policies = current_role.pop("InlinePolicies", [])
        existing_managed_policies = current_role.pop("ManagedPolicies", [])
        tasks = []
        try:
            if role_exists:
                tasks.extend(
                    [
                        apply_role_tags(
                            role_name,
                            client,
                            account_role["Tags"],
                            current_role.get("Tags", []),
                            log_params,
                        ),
                        update_assume_role_policy(
                            role_name,
                            client,
                            account_role.pop("AssumeRolePolicyDocument", {}),
                            current_role["AssumeRolePolicyDocument"],
                            log_params,
                        ),
                    ]
                )

                supported_update_keys = ["Description", "MaxSessionDuration"]
                update_resource_log_params = {**log_params}
                update_role_params = {}
                for k in supported_update_keys:
                    if account_role.get(k) is not None and account_role.get(
                        k
                    ) != current_role.get(k):
                        update_resource_log_params[k] = dict(
                            old_value=current_role.get(k), new_value=account_role.get(k)
                        )
                        update_role_params[k] = current_role.get(k)

                if update_role_params:
                    log_str = "Out of date resource found."
                    if ctx.execute:
                        log.info(
                            f"{log_str} Updating resource...",
                            **update_resource_log_params,
                        )
                        tasks.append(
                            aio_wrapper(
                                client.update_role,
                                RoleName=role_name,
                                **{
                                    k: account_role.get(k)
                                    for k in supported_update_keys
                                },
                            )
                        )
                    else:
                        log.info(log_str, **update_resource_log_params)
            else:
                account_change_details.proposed_changes.append(
                    ProposedChange(
                        change_type=ProposedChangeType.CREATE,
                        resource_id=role_name,
                        resource_type=self.resource_type,
                    )
                )
                log_str = "New resource found in code."
                if not ctx.execute:
                    log.info(log_str, **log_params)
                    # Exit now because apply functions won't work if resource doesn't exist
                    return account_change_details

                log_str = f"{log_str} Creating resource..."
                log.info(log_str, **log_params)
                account_role["AssumeRolePolicyDocument"] = json.dumps(
                    account_role["AssumeRolePolicyDocument"]
                )
                await aio_wrapper(client.create_role, **account_role)
        except Exception as e:
            log.error("Unable to generate tasks for resource", error=e, **log_params)
            return account_change_details

        tasks.extend(
            [
                apply_role_managed_policies(
                    role_name,
                    client,
                    managed_policies,
                    existing_managed_policies,
                    log_params,
                ),
                apply_role_inline_policies(
                    role_name,
                    client,
                    inline_policies,
                    existing_inline_policies,
                    log_params,
                ),
            ]
        )
        try:
            changes_made = await asyncio.gather(*tasks)
        except Exception as e:
            log.error("Unable to apply changes to resource", error=e, **log_params)
            return account_change_details
        if any(changes_made):
            account_change_details.proposed_changes.extend(
                list(chain.from_iterable(changes_made))
            )

        if ctx.execute:
            log.debug(
                "Successfully finished execution on account for resource",
                changes_made=bool(account_change_details.proposed_changes),
                **log_params,
            )
        else:
            log.debug(
                "Successfully finished scanning for drift on account for resource",
                requires_changes=bool(account_change_details.proposed_changes),
                **log_params,
            )

        return account_change_details

    @property
    def resource_type(self):
        return "aws:iam:role"

    @property
    def resource_id(self):
        return self.role_name

iambic/aws/iam/role/__init__.py METASEP

iambic/aws/utils.py METASEP
import asyncio
import re
from datetime import datetime
from enum import Enum

from botocore.exceptions import ClientError

from iambic.core.context import ctx
from iambic.core.logger import log
from iambic.core.utils import aio_wrapper, camel_to_snake


async def paginated_search(
    search_fnc, response_key: str, max_results: int = None, **search_kwargs
) -> list:
    """Retrieve and aggregate each paged response, returning a single list of each response object
    :param search_fnc:
    :param response_key:
    :param max_results:
    :return:
    """
    results = []
    retry_count = 0

    while True:
        try:
            response = await aio_wrapper(search_fnc, **search_kwargs)
        except ClientError as err:
            if err.response["Error"]["Code"] == "Throttling":
                if retry_count >= 10:
                    raise
                retry_count += 1
                await asyncio.sleep(retry_count * 2)
                continue
            else:
                raise

        retry_count = 0
        results.extend(response.get(response_key, []))

        if not response["IsTruncated"] or (max_results and len(results) >= max_results):
            return results
        else:
            search_kwargs["Marker"] = response["Marker"]


class RegionName(Enum):
    us_east_1 = "us-east-1"
    us_west_1 = "us-west-1"
    us_west_2 = "us-west-2"
    eu_west_1 = "eu-west-1"
    eu_west_2 = "eu-west-2"
    eu_central_1 = "eu-central-1"
    ap_southeast_1 = "ap-southeast-1"
    ap_southeast_2 = "ap-southeast-2"
    ap_northeast_1 = "ap-northeast-1"
    ap_northeast_2 = "ap-northeast-2"
    sa_east_1 = "sa-east-1"
    cn_north_1 = "cn-north-1"


def normalize_boto3_resp(obj):
    skip_formatting_for = ["condition"]
    if isinstance(obj, dict):
        new_obj = dict()
        for k, v in obj.items():
            k = camel_to_snake(k)
            if isinstance(v, list):
                new_obj[k] = [normalize_boto3_resp(x) for x in v]
            else:
                new_obj[k] = (
                    normalize_boto3_resp(v) if k not in skip_formatting_for else v
                )
        return new_obj
    elif isinstance(obj, list):
        return [normalize_boto3_resp(x) for x in obj]
    else:
        return obj


def get_closest_value(matching_values: list, aws_account):
    if len(matching_values) == 1:
        return matching_values[0]

    account_value_hit = dict(returns=None, specificty=0)

    account_ids = [aws_account.account_id]
    if account_name := aws_account.account_name:
        account_ids.append(account_name.lower())

    for matching_value in matching_values:
        resource_accounts = sorted(matching_value.included_accounts, key=len)
        for resource_account in resource_accounts:
            for account_id in account_ids:
                if resource_account == "*" and account_value_hit["specificty"] == 0:
                    account_value_hit = {"returns": matching_value, "specificty": 1}
                elif re.match(resource_account.lower(), account_id) and len(
                    resource_account
                ) > account_value_hit.get("specificty", 0):
                    account_value_hit = {
                        "returns": matching_value,
                        "specificty": len(resource_account),
                    }
                    break

    return account_value_hit["returns"]


def evaluate_on_account(resource, aws_account) -> bool:
    from iambic.aws.models import AccessModel

    if ctx.execute and (aws_account.read_only or getattr(resource, "read_only", False)):
        return False
    if not issubclass(type(resource), AccessModel):
        return True

    if aws_account.org_id:
        if aws_account.org_id in resource.excluded_orgs:
            return False
        elif not any(
            re.match(org_id, aws_account.org_id) for org_id in resource.included_orgs
        ):
            return False

    account_ids = [aws_account.account_id]
    if account_name := aws_account.account_name:
        account_ids.append(account_name.lower())

    for account_id in account_ids:
        if any(
            re.match(resource_account.lower(), account_id)
            for resource_account in resource.excluded_accounts
        ):
            return False

    for account_id in account_ids:
        if any(
            resource_account == "*" for resource_account in resource.included_accounts
        ):
            return True
        elif any(
            re.match(resource_account.lower(), account_id)
            for resource_account in resource.included_accounts
        ):
            return True

    return False


def apply_to_account(resource, aws_account) -> bool:
    from iambic.aws.models import Deleted

    if hasattr(resource, "deleted"):
        deleted_resource_type = isinstance(resource, Deleted)

        if isinstance(resource.deleted, bool):
            if resource.deleted and not deleted_resource_type:
                return False
        else:
            deleted_obj = resource.get_attribute_val_for_account(aws_account, "deleted")
            deleted_obj = get_closest_value(deleted_obj, aws_account)
            if deleted_obj and deleted_obj.deleted and not deleted_resource_type:
                return False

    return evaluate_on_account(resource, aws_account)


async def remove_expired_resources(
    resource, template_resource_type: str, template_resource_id: str
):
    from iambic.core.models import BaseModel

    if (
        not issubclass(type(resource), BaseModel)
        or not hasattr(resource, "expires_at")
        or getattr(resource, "deleted", None)
    ):
        return resource

    log_params = dict(
        resource_type=resource.resource_type, resource_id=resource.resource_id
    )
    if (
        template_resource_type != resource.resource_type
        or template_resource_id != resource.resource_id
    ):
        log_params["parent_resource_type"] = template_resource_type
        log_params["parent_resource_id"] = template_resource_id

    if hasattr(resource, "expires_at") and resource.expires_at:
        if resource.expires_at < datetime.utcnow():
            resource.deleted = True
            log.info("Expired resource found, marking for deletion", **log_params)
            return resource

    for field_name in resource.__fields__.keys():
        field_val = getattr(resource, field_name)
        if isinstance(field_val, list):
            resource.__fields__[field_name] = await asyncio.gather(
                *[
                    remove_expired_resources(
                        elem, template_resource_type, template_resource_id
                    )
                    for elem in field_val
                ]
            )
        else:
            resource.__fields__[field_name] = await remove_expired_resources(
                field_val, template_resource_type, template_resource_id
            )

    return resource


def get_aws_account_map(configs: list) -> dict:
    """Returns a map containing all account configs across all provided config instances

    :param configs:
    :return: dict(account_id:str = AWSAccount)
    """
    aws_account_map = dict()
    for config in configs:
        config.set_account_defaults()
        for aws_account in config.aws_accounts:
            if aws_account_map.get(aws_account.account_id):
                log.critical(
                    "Account definition found in multiple configs",
                    account_id=aws_account.account_id,
                    account_name=aws_account.account_name,
                )
                raise ValueError
            aws_account_map[aws_account.account_id] = aws_account

    return aws_account_map


def boto3_retry(f):
    async def wrapper(*args, **kwargs):
        max_retries = kwargs.pop("max_retries", 10)
        for attempt in range(max_retries):
            try:
                return await f(*args, **kwargs)
            except ClientError as err:
                if (
                    err.response["Error"]["Code"] == "Throttling"
                    and attempt < max_retries - 1
                ):
                    await asyncio.sleep(1)
                else:
                    raise

    return wrapper

iambic/aws/models.py METASEP
import asyncio
from datetime import datetime
from typing import List, Optional, Union

from pydantic import Field

from iambic.aws.utils import evaluate_on_account
from iambic.config.models import AWSAccount, Config
from iambic.core.context import ctx
from iambic.core.logger import log
from iambic.core.models import (
    AccountChangeDetails,
    BaseModel,
    BaseTemplate,
    TemplateChangeDetails,
)

ARN_RE = r"(^arn:([^:]*):([^:]*):([^:]*):(|\*|[\d]{12}|cloudfront|aws|{{account_id}}):(.+)$)|^\*$"


class AccessModel(BaseModel):
    included_accounts: List = Field(
        ["*"],
        description="A list of account ids and/or account names this statement applies to. "
        "Account ids/names can be represented as a regex and string",
    )
    excluded_accounts: Optional[List] = Field(
        [],
        description="A list of account ids and/or account names this statement explicitly does not apply to. "
        "Account ids/names can be represented as a regex and string",
    )
    included_orgs: List = Field(
        ["*"],
        description="A list of AWS organization ids this statement applies to. "
        "Org ids can be represented as a regex and string",
    )
    excluded_orgs: Optional[List] = Field(
        [],
        description="A list of AWS organization ids this statement explicitly does not apply to. "
        "Org ids can be represented as a regex and string",
    )


class Deleted(AccessModel):
    deleted: bool = Field(
        description="Denotes whether the resource has been removed from AWS."
        "Upon being set to true, the resource will be deleted the next time iambic is ran.",
    )


class ExpiryModel(BaseModel):
    expires_at: Optional[datetime] = Field(
        None, description="The date and time the resource will be/was set to deleted."
    )
    deleted: Optional[Union[bool, List[Deleted]]] = Field(
        False,
        description="Denotes whether the resource has been removed from AWS."
        "Upon being set to true, the resource will be deleted the next time iambic is ran.",
    )


class Tag(ExpiryModel, AccessModel):
    key: str
    value: str

    @property
    def resource_type(self):
        return "Tag"

    @property
    def resource_id(self):
        return self.key


class AWSTemplate(BaseTemplate, ExpiryModel):
    async def _apply_to_account(self, aws_account: AWSAccount) -> AccountChangeDetails:
        raise NotImplementedError

    async def apply(self, config: Config) -> TemplateChangeDetails:
        tasks = []
        template_changes = TemplateChangeDetails(
            resource_id=self.resource_id,
            resource_type=self.resource_type,
            template_path=self.file_path,
        )
        log_params = dict(
            resource_type=self.resource_type, resource_id=self.resource_id
        )
        for account in config.aws_accounts:
            if evaluate_on_account(self, account):
                if ctx.execute:
                    log_str = "Applying changes to resource."
                else:
                    log_str = "Detecting changes for resource."
                log.info(log_str, account=str(account), **log_params)
                tasks.append(self._apply_to_account(account))

        account_changes = await asyncio.gather(*tasks)
        template_changes.proposed_changes = [
            account_change
            for account_change in account_changes
            if any(account_change.proposed_changes)
        ]
        if account_changes and ctx.execute:
            log.info(
                "Successfully applied resource changes to all aws_accounts.",
                **log_params,
            )
        elif account_changes and not ctx.execute:
            log.info(
                "Successfully detected required resource changes on all aws_accounts.",
                **log_params,
            )
        else:
            log.debug("No changes detected for resource on any account.", **log_params)

        return template_changes

iambic/aws/__init__.py METASEP

iambic/aws/iam/models.py METASEP
from typing import Optional

from iambic.aws.models import AccessModel


class Path(AccessModel):
    file_path: str


class MaxSessionDuration(AccessModel):
    max_session_duration: int


class Description(AccessModel):
    description: Optional[str] = ""

iambic/aws/iam/__init__.py METASEP

iambic/aws/iam/policy/utils.py METASEP
import asyncio

from botocore.exceptions import ClientError
from deepdiff import DeepDiff

from iambic.aws.utils import boto3_retry, paginated_search
from iambic.core import noq_json as json
from iambic.core.context import ctx
from iambic.core.logger import log
from iambic.core.models import ProposedChange, ProposedChangeType
from iambic.core.utils import NoqSemaphore, aio_wrapper


async def list_managed_policy_versions(policy_arn: str, iam_client) -> list[dict]:
    return (
        await aio_wrapper(iam_client.list_policy_versions, PolicyArn=policy_arn)
    ).get("Versions", [])


async def list_managed_policy_tags(managed_policy_arn: str, iam_client) -> list[dict]:
    return await paginated_search(
        iam_client.list_policy_tags, "Tags", PolicyArn=managed_policy_arn
    )


@boto3_retry
async def get_managed_policy_version_doc(
    managed_policy_arn: str, version_id: str, iam_client, **kwargs
) -> dict:
    return (
        (
            await aio_wrapper(
                iam_client.get_policy_version,
                PolicyArn=managed_policy_arn,
                VersionId=version_id,
            )
        )
        .get("PolicyVersion", {})
        .get("Document", {})
    )


@boto3_retry
async def get_managed_policy(managed_policy_arn: str, iam_client, **kwargs) -> dict:
    try:
        response = (
            await aio_wrapper(iam_client.get_policy, PolicyArn=managed_policy_arn)
        ).get("Policy", {})
        if response:
            response["PolicyDocument"] = await get_managed_policy_version_doc(
                managed_policy_arn, response.pop("DefaultVersionId"), iam_client
            )
    except ClientError as err:
        if err.response["Error"]["Code"] == "NoSuchEntity":
            response = {}
        else:
            raise

    return response


def get_oldest_policy_version_id(policy_versions: list[dict]) -> str:
    policy_versions = sorted(policy_versions, key=lambda version: version["CreateDate"])
    if not policy_versions[0]["IsDefaultVersion"]:
        return policy_versions[0]["VersionId"]
    elif len(policy_versions) > 1:
        return policy_versions[1]["VersionId"]


async def list_managed_policies(
    iam_client,
    scope: str = "Local",
    only_attached: bool = False,
    path_prefix: str = "/",
    policy_usage_filter: str = None,
):
    get_managed_policy_semaphore = NoqSemaphore(get_managed_policy, 50)
    list_policy_kwargs = dict(
        Scope=scope,
        OnlyAttached=only_attached,
        PathPrefix=path_prefix,
    )
    if policy_usage_filter:
        list_policy_kwargs["PolicyUsageFilter"] = policy_usage_filter

    managed_policies = await paginated_search(
        iam_client.list_policies, response_key="Policies", **list_policy_kwargs
    )
    return await get_managed_policy_semaphore.process(
        [
            {"managed_policy_arn": policy["Arn"], "iam_client": iam_client}
            for policy in managed_policies
        ]
    )


@boto3_retry
async def delete_managed_policy(policy_arn: str, iam_client):
    await aio_wrapper(iam_client.delete_policy, PolicyArn=policy_arn)


@boto3_retry
async def update_managed_policy(
    policy_arn: str,
    iam_client,
    template_policy_document: dict,
    existing_policy_document: dict,
    read_only: bool,
    log_params: dict,
) -> list[ProposedChange]:
    response = []
    if isinstance(existing_policy_document, str):
        existing_policy_document = json.loads(existing_policy_document)
    policy_drift = await aio_wrapper(
        DeepDiff,
        existing_policy_document,
        template_policy_document,
        ignore_order=True,
    )

    if policy_drift:
        log_str = "Changes to the PolicyDocument discovered."
        response.append(
            ProposedChange(
                change_type=ProposedChangeType.UPDATE,
                attribute="policy_document",
                change_summary=policy_drift,
                current_value=existing_policy_document,
                new_value=template_policy_document,
            )
        )

        if not read_only:
            if policy_drift:
                policy_versions = await list_managed_policy_versions(
                    policy_arn, iam_client
                )
                if len(policy_versions) == 5:
                    await aio_wrapper(
                        iam_client.delete_policy_version,
                        PolicyArn=policy_arn,
                        VersionId=get_oldest_policy_version_id(policy_versions),
                    )

            log_str = f"{log_str} Updating PolicyDocument..."
            await aio_wrapper(
                iam_client.create_policy_version,
                PolicyArn=policy_arn,
                PolicyDocument=json.dumps(template_policy_document),
            )

        log.info(log_str, **log_params)
    return response


async def apply_managed_policy_tags(
    policy_arn: str,
    iam_client,
    template_tags: list[dict],
    existing_tags: list[dict],
    read_only: bool,
    log_params: dict,
) -> list[ProposedChange]:
    existing_tag_map = {tag["Key"]: tag["Value"] for tag in existing_tags}
    template_tag_map = {tag["Key"]: tag["Value"] for tag in template_tags}
    tags_to_apply = [
        tag for tag in template_tags if tag["Value"] != existing_tag_map.get(tag["Key"])
    ]
    tasks = []
    response = []

    if tags_to_remove := [
        tag["Key"] for tag in existing_tags if not template_tag_map.get(tag["Key"])
    ]:
        log_str = "Stale tags discovered."
        response.append(
            ProposedChange(
                change_type=ProposedChangeType.DETACH,
                attribute="tags",
                change_summary={"TagKeys": tags_to_remove},
            )
        )
        if not read_only:
            log_str = f"{log_str} Removing tags..."
            tasks.append(
                aio_wrapper(
                    iam_client.untag_policy,
                    PolicyArn=policy_arn,
                    TagKeys=tags_to_remove,
                )
            )
        log.info(log_str, tags=tags_to_remove, **log_params)

    if tags_to_apply:
        log_str = "New tags discovered in AWS."
        for tag in tags_to_apply:
            response.append(
                ProposedChange(
                    change_type=ProposedChangeType.ATTACH,
                    attribute="tags",
                    new_value=tag,
                )
            )
        if ctx.execute:
            log_str = f"{log_str} Adding tags..."
            tasks.append(
                aio_wrapper(
                    iam_client.tag_policy, PolicyArn=policy_arn, Tags=tags_to_apply
                )
            )
        log.info(log_str, tags=tags_to_apply, **log_params)

    if tasks:
        await asyncio.gather(*tasks)

    return response

iambic/aws/iam/policy/models.py METASEP
import asyncio
import json
from itertools import chain
from typing import List, Optional, Union

from jinja2 import BaseLoader, Environment
from pydantic import Field, constr

from iambic.aws.iam.models import Description, Path
from iambic.aws.iam.policy.utils import (
    apply_managed_policy_tags,
    delete_managed_policy,
    get_managed_policy,
    update_managed_policy,
)
from iambic.aws.models import ARN_RE, AccessModel, AWSTemplate, ExpiryModel, Tag
from iambic.config.models import AWSAccount
from iambic.core.context import ctx
from iambic.core.logger import log
from iambic.core.models import (
    AccountChangeDetails,
    BaseModel,
    ProposedChange,
    ProposedChangeType,
)
from iambic.core.utils import aio_wrapper


class Principal(BaseModel):
    aws: Optional[Union[str, list[str]]] = None
    service: Optional[Union[str, list[str]]] = None
    canonical_user: Optional[Union[str, list[str]]] = None
    federated: Optional[Union[str, list[str]]] = None

    def _apply_resource_dict(self, aws_account: AWSAccount = None) -> dict:
        resource_dict = super(Principal, self)._apply_resource_dict(aws_account)
        if aws_val := resource_dict.pop("aws", resource_dict.pop("Aws", None)):
            resource_dict["AWS"] = aws_val
        return resource_dict

    @property
    def resource_type(self) -> str:
        return "aws:iam:policy_principal"

    @property
    def resource_id(self) -> str:
        return self.service or self.canonical_user or self.federated or self.aws


class ConditionalStatement(BaseModel):
    for_all_values: Optional[bool] = None
    for_any_values: Optional[bool] = None


class Condition(BaseModel):
    """TODO: Finish this.

    I put in a pin it because it became obvious this will be a large undertaking.
    The problem stems from the way conditions are structured.

    Take string_equals: Optional[dict] = None for example
    For this one attribute it also requires the following:
        for_all_values__string_equals
        for_any_values__string_equals
        for_all_values__string_equals__if_exists
        for_any_values__string_equals__if_exists
    On top of that we still need to add clarity on what the supported keys are.

    For more info on conditions:
    https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html

    """

    # Key = service/resource_type
    # Value = resource_value | list[resource_value]

    # String conditionals
    string_equals: Optional[dict] = None
    string_not_equals: Optional[dict] = None
    string_equals_ignore_case: Optional[dict] = None
    string_like: Optional[dict] = None
    string_not_like: Optional[dict] = None

    # Numeric Conditionals
    numeric_equals: Optional[dict] = None
    numeric_not_equals: Optional[dict] = None
    numeric_less_than: Optional[dict] = None
    numeric_less_than_equals: Optional[dict] = None
    numeric_greater_than: Optional[dict] = None
    numeric_greater_than_equals: Optional[dict] = None

    # Date Conditionals
    date_equals: Optional[dict] = None
    date_not_equals: Optional[dict] = None
    date_less_than: Optional[dict] = None
    date_less_than_equals: Optional[dict] = None
    date_greater_than: Optional[dict] = None
    date_greater_than_equals: Optional[dict] = None

    # IP Addr Conditionals
    ip_address: Optional[dict] = None
    not_ip_address: Optional[dict] = None

    # ARN Conditionals
    arn_equals: Optional[dict] = None
    arn_like: Optional[dict] = None
    arn_not_equals: Optional[dict] = None
    arn_not_like: Optional[dict] = None

    # Null Conditional
    null: Optional[dict] = None

    # Boolean conditional
    bool: Optional[dict] = None


class PolicyStatement(AccessModel, ExpiryModel):
    effect: str = Field(..., description="Allow | Deny")
    principal: Optional[Union[Principal | str]] = None
    not_principal: Optional[Union[Principal | str]] = None
    action: Optional[Union[List[str] | str]] = Field(
        None,
        description="A single regex or list of regexes. "
        "Values are the actions that can be performed on the resources in the policy statement",
        example="dynamodb:list*",
    )
    not_action: Optional[Union[List[str] | str]] = Field(
        None,
        description="An advanced policy element that explicitly matches everything except the specified list of actions."
        "DON'T use this with effect: allow in the same statement OR policy",
    )
    resource: Optional[Union[List[str] | str]] = Field(
        None,
        description="A single regex or list of regexes. Values specified are the resources the statement applies to",
    )
    not_resource: Optional[Union[List[str] | str]] = Field(
        None,
        description="An advanced policy element that explicitly matches every resource except those specified."
        "DON'T use this with effect: allow and action: '*'",
    )
    condition: Optional[dict] = Field(
        None,
        description="An optional set of conditions to determine of the policy applies to a resource.",
    )
    sid: Optional[str] = Field(
        None,
        description="The Policy Statement ID.",
    )

    @property
    def resource_type(self):
        return "aws:iam:policy_statement"

    @property
    def resource_id(self):
        return self.sid


class AssumeRolePolicyDocument(AccessModel):
    version: Optional[str] = None
    statement: Optional[List[PolicyStatement]] = None

    @property
    def resource_type(self) -> str:
        return "aws:iam:assume_role_policy_document"

    @property
    def resource_id(self) -> str:
        return ""


class PolicyDocument(AccessModel, ExpiryModel):
    policy_name: str = Field(
        description="The name of the policy.",
    )
    version: Optional[str] = None
    statement: Optional[List[PolicyStatement]] = Field(
        None,
        description="List of policy statements",
    )

    @property
    def resource_type(self):
        return "aws:policy_document"

    @property
    def resource_id(self):
        return self.policy_name


class ManagedPolicyDocument(AccessModel):
    version: Optional[str] = None
    statement: Optional[List[PolicyStatement]] = Field(
        None,
        description="List of policy statements",
    )

    def _apply_resource_dict(self, aws_account: AWSAccount = None) -> str:
        resource_dict = super()._apply_resource_dict(aws_account)
        return json.dumps(resource_dict)

    def apply_resource_dict(self, aws_account: AWSAccount) -> dict:
        response = json.loads(self._apply_resource_dict(aws_account))
        variables = {var.key: var.value for var in aws_account.variables}
        variables["account_id"] = aws_account.account_id
        variables["account_name"] = aws_account.account_name

        rtemplate = Environment(loader=BaseLoader()).from_string(json.dumps(response))
        data = rtemplate.render(**variables)
        return json.loads(data)

    @property
    def resource_type(self):
        return "aws:iam:managed_policy:policy_document"

    @property
    def resource_id(self):
        return


class ManagedPolicyTemplate(AWSTemplate, AccessModel):
    template_type = "NOQ::IAM::ManagedPolicy"
    policy_name: str = Field(
        description="The name of the policy.",
    )
    path: Optional[Union[str | List[Path]]] = "/"
    description: Optional[Union[str | list[Description]]] = Field(
        "",
        description="Description of the role",
    )
    policy_document: Union[ManagedPolicyDocument | List[ManagedPolicyDocument]]
    tags: Optional[List[Tag]] = Field(
        [],
        description="List of tags attached to the role",
    )

    def _is_read_only(self, aws_account: AWSAccount):
        return aws_account.read_only or self.read_only or ctx.eval_only

    def _apply_resource_dict(self, aws_account: AWSAccount = None) -> dict:
        resource_dict = super()._apply_resource_dict(aws_account)
        resource_dict[
            "Arn"
        ] = f"arn:aws:iam::{aws_account.account_id}:policy{resource_dict['Path']}{resource_dict['PolicyName']}"
        return resource_dict

    async def _apply_to_account(self, aws_account: AWSAccount) -> AccountChangeDetails:
        boto3_session = aws_account.get_boto3_session()
        client = boto3_session.client("iam")
        account_policy = self.apply_resource_dict(aws_account)
        policy_name = account_policy["PolicyName"]
        account_change_details = AccountChangeDetails(
            account=str(aws_account),
            resource_id=policy_name,
            new_value=dict(**account_policy),
            proposed_changes=[],
        )
        log_params = dict(
            resource_type=self.resource_type,
            resource_id=policy_name,
            account=str(aws_account),
        )
        read_only = self._is_read_only(aws_account)
        policy_arn = account_policy.pop("Arn")
        current_policy = await get_managed_policy(policy_arn, client)
        if current_policy:
            account_change_details.current_value = {**current_policy}

        deleted = self.get_attribute_val_for_account(aws_account, "deleted", False)
        if isinstance(deleted, list):
            deleted = deleted[0].deleted

        if deleted:
            if current_policy:
                account_change_details.new_value = None
                account_change_details.proposed_changes.append(
                    ProposedChange(
                        change_type=ProposedChangeType.DELETE,
                        resource_id=policy_name,
                        resource_type=self.resource_type,
                    )
                )
                log_str = "Active resource found with deleted=false."
                if not read_only:
                    log_str = f"{log_str} Deleting resource..."
                log.info(log_str, **log_params)

                if not read_only:
                    await delete_managed_policy(policy_arn, client)

            return account_change_details

        if current_policy:
            changes_made = await asyncio.gather(
                update_managed_policy(
                    policy_arn,
                    client,
                    json.loads(account_policy["PolicyDocument"]),
                    current_policy["PolicyDocument"],
                    read_only,
                    log_params,
                ),
                apply_managed_policy_tags(
                    policy_arn,
                    client,
                    account_policy["Tags"],
                    current_policy["Tags"],
                    read_only,
                    log_params,
                ),
            )
            if any(changes_made):
                account_change_details.proposed_changes.extend(
                    list(chain.from_iterable(changes_made))
                )
        else:
            account_change_details.proposed_changes.append(
                ProposedChange(
                    change_type=ProposedChangeType.CREATE,
                    resource_id=policy_name,
                    resource_type=self.resource_type,
                )
            )
            log_str = "New resource found in code."
            if not read_only:
                log_str = f"{log_str} Creating resource..."
                await aio_wrapper(client.create_policy, **account_policy)
            log.info(log_str, **log_params)

        if not read_only:
            log.debug(
                "Successfully finished execution on account for resource",
                changes_made=bool(account_change_details.proposed_changes),
                **log_params,
            )
        else:
            log.debug(
                "Successfully finished scanning for drift on account for resource",
                requires_changes=bool(account_change_details.proposed_changes),
                **log_params,
            )

        return account_change_details

    @property
    def resource_type(self):
        return "aws:iam:managed_policy"

    @property
    def resource_id(self):
        return self.policy_name


class ManagedPolicyRef(AccessModel, ExpiryModel):
    policy_arn: constr(regex=ARN_RE)

    @property
    def resource_type(self):
        return "aws:iam:managed_policy"

    @property
    def resource_id(self):
        return self.policy_arn

iambic/aws/iam/policy/__init__.py METASEP

iambic/aws/iam/policy/template_generation.py METASEP

import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
    existing_template_map: dict,
):
    account_id_to_mp_map = {}
    num_of_accounts = len(managed_policy_refs)
    for managed_policy_ref in managed_policy_refs:
        async with aiofiles.open(managed_policy_ref["file_path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_mp_map[
                managed_policy_ref["account_id"]
            ] = normalize_boto3_resp(content_dict)

    # Generate the params used for attribute creation
    managed_policy_template_params = {"policy_name": managed_policy_name}
    path_resources = list()
    description_resources = list()
    policy_document_resources = list()
    tag_resources = list()
    for account_id, managed_policy_dict in account_id_to_mp_map.items():
        path_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["path"]}],
            }
        )
        policy_document_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["policy_document"]}],
            }
        )

        if tags := managed_policy_dict.get("tags"):
            tag_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": tag} for tag in tags],
                }
            )

        if description := managed_policy_dict.get("description"):
            description_resources.append(
                {"account_id": account_id, "resources": [{"resource_val": description}]}
            )

    if len(managed_policy_refs) != len(aws_account_map):
        managed_policy_template_params["included_accounts"] = [
            aws_account_map[managed_policy_ref["account_id"]].account_name
            for managed_policy_ref in managed_policy_refs
        ]

    path = await group_int_or_str_attribute(
        aws_account_map, num_of_accounts, path_resources, "path"
    )
    if path != "/":
        managed_policy_template_params["path"] = path

    managed_policy_template_params["policy_document"] = await group_dict_attribute(
        aws_account_map, num_of_accounts, policy_document_resources, True
    )

    if description_resources:
        managed_policy_template_params[
            "description"
        ] = await group_int_or_str_attribute(
            aws_account_map, num_of_accounts, description_resources, "description"
        )

    if tag_resources:
        tags = await group_dict_attribute(
            aws_account_map, num_of_accounts, tag_resources, True
        )
        if isinstance(tags, dict):
            tags = [tags]
        managed_policy_template_params["tags"] = tags

    try:
        managed_policy = ManagedPolicyTemplate(
            file_path=existing_template_map.get(
                managed_policy_name,
                get_templated_managed_policy_file_path(
                    managed_policy_dir, managed_policy_name
                ),
            ),
            **managed_policy_template_params,
        )
        managed_policy.write()
    except Exception as err:
        log.info(str(err), managed_policy_params=managed_policy_template_params)


async def generate_aws_managed_policy_templates(
    configs: list[Config], base_output_dir: str
):
    aws_account_map = get_aws_account_map(configs)
    existing_template_map = await get_existing_template_file_map(
        base_output_dir, "NOQ::IAM::ManagedPolicy"
    )
    resource_dir = get_managed_policy_dir(base_output_dir)
    generate_account_managed_policy_resource_files_semaphore = NoqSemaphore(
        generate_account_managed_policy_resource_files, 25
    )

    log.info("Generating AWS managed policy templates.")
    log.info(
        "Beginning to retrieve AWS IAM Managed Policies.",
        accounts=list(aws_account_map.keys()),
    )

    account_managed_policies = (
        await generate_account_managed_policy_resource_files_semaphore.process(
            [{"aws_account": aws_account} for aws_account in aws_account_map.values()]
        )
    )
    messages = []
    for account in account_managed_policies:
        for managed_policy in account["managed_policies"]:
            messages.append(
                {
                    "policy_name": managed_policy["policy_name"],
                    "arn": managed_policy["arn"],
                    "file_path": managed_policy["file_path"],
                    "aws_account": aws_account_map[managed_policy["account_id"]],
                }
            )

    log.info("Finished retrieving managed policy details")

    # Use these for testing `create_templated_managed_policy`
    # account_managed_policy_output = json.dumps(account_managed_policies)
    # with open("account_managed_policy_output.json", "w") as f:
    #     f.write(account_managed_policy_output)
    # with open("account_managed_policy_output.json") as f:
    #     account_managed_policies = json.loads(f.read())

    log.info("Grouping managed policies")
    # Move everything to required structure
    for account_mp_elem in range(len(account_managed_policies)):
        for mp_elem in range(
            len(account_managed_policies[account_mp_elem]["managed_policies"])
        ):
            policy_name = account_managed_policies[account_mp_elem]["managed_policies"][
                mp_elem
            ].pop("policy_name")
            account_managed_policies[account_mp_elem]["managed_policies"][mp_elem][
                "resource_val"
            ] = policy_name

        account_managed_policies[account_mp_elem][
            "resources"
        ] = account_managed_policies[account_mp_elem].pop("managed_policies", [])

    grouped_managed_policy_map = await base_group_str_attribute(
        aws_account_map, account_managed_policies
    )

    log.info("Writing templated roles")
    for policy_name, policy_refs in grouped_managed_policy_map.items():
infile
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
    existing_template_map: dict,
):
    account_id_to_mp_map = {}
    num_of_accounts = len(managed_policy_refs)
    for managed_policy_ref in managed_policy_refs:
        async with aiofiles.open(managed_policy_ref["file_path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_mp_map[
                managed_policy_ref["account_id"]
            ] = normalize_boto3_resp(content_dict)

    # Generate the params used for attribute creation
    managed_policy_template_params = {"policy_name": managed_policy_name}
    path_resources = list()
    description_resources = list()
    policy_document_resources = list()
    tag_resources = list()
    for account_id, managed_policy_dict in account_id_to_mp_map.items():
        path_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["path"]}],
            }
        )
        policy_document_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["policy_document"]}],
            }
        )

        if tags := managed_policy_dict.get("tags"):
            tag_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": tag} for tag in tags],
                }
            )

        if description := managed_policy_dict.get("description"):
            description_resources.append(
                {"account_id": account_id, "resources": [{"resource_val": description}]}
            )

    if len(managed_policy_refs) != len(aws_account_map):
        managed_policy_template_params["included_accounts"] = [
            aws_account_map[managed_policy_ref["account_id"]].account_name
            for managed_policy_ref in managed_policy_refs
        ]

    path = await group_int_or_str_attribute(
        aws_account_map, num_of_accounts, path_resources, "path"
    )
    if path != "/":
        managed_policy_template_params["path"] = path

    managed_policy_template_params["policy_document"] = await group_dict_attribute(
        aws_account_map, num_of_accounts, policy_document_resources, True
    )

    if description_resources:
        managed_policy_template_params[
            "description"
        ] = await group_int_or_str_attribute(
            aws_account_map, num_of_accounts, description_resources, "description"
        )

    if tag_resources:
        tags = await group_dict_attribute(
            aws_account_map, num_of_accounts, tag_resources, True
        )
        if isinstance(tags, dict):
            tags = [tags]
        managed_policy_template_params["tags"] = tags

    try:
        managed_policy = ManagedPolicyTemplate(
            file_path=existing_template_map.get(
                managed_policy_name,
                get_templated_managed_policy_file_path(
                    managed_policy_dir, managed_policy_name
                ),
            ),
            **managed_policy_template_params,
        )
        managed_policy.write()
    except Exception as err:
        log.info(str(err), managed_policy_params=managed_policy_template_params)


async def generate_aws_managed_policy_templates(
    configs: list[Config], base_output_dir: str
):
    aws_account_map = get_aws_account_map(configs)
    existing_template_map = await get_existing_template_file_map(
        base_output_dir, "NOQ::IAM::ManagedPolicy"
    )
infile
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
    existing_template_map: dict,
):
    account_id_to_mp_map = {}
    num_of_accounts = len(managed_policy_refs)
    for managed_policy_ref in managed_policy_refs:
        async with aiofiles.open(managed_policy_ref["file_path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_mp_map[
                managed_policy_ref["account_id"]
            ] = normalize_boto3_resp(content_dict)

    # Generate the params used for attribute creation
    managed_policy_template_params = {"policy_name": managed_policy_name}
    path_resources = list()
    description_resources = list()
    policy_document_resources = list()
    tag_resources = list()
    for account_id, managed_policy_dict in account_id_to_mp_map.items():
        path_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["path"]}],
            }
        )
        policy_document_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["policy_document"]}],
            }
        )

        if tags := managed_policy_dict.get("tags"):
            tag_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": tag} for tag in tags],
                }
            )

        if description := managed_policy_dict.get("description"):
            description_resources.append(
                {"account_id": account_id, "resources": [{"resource_val": description}]}
            )

    if len(managed_policy_refs) != len(aws_account_map):
        managed_policy_template_params["included_accounts"] = [
            aws_account_map[managed_policy_ref["account_id"]].account_name
            for managed_policy_ref in managed_policy_refs
        ]

    path = await group_int_or_str_attribute(
        aws_account_map, num_of_accounts, path_resources, "path"
    )
    if path != "/":
        managed_policy_template_params["path"] = path

    managed_policy_template_params["policy_document"] = await group_dict_attribute(
        aws_account_map, num_of_accounts, policy_document_resources, True
    )

    if description_resources:
        managed_policy_template_params[
            "description"
        ] = await group_int_or_str_attribute(
            aws_account_map, num_of_accounts, description_resources, "description"
        )

    if tag_resources:
        tags = await group_dict_attribute(
            aws_account_map, num_of_accounts, tag_resources, True
        )
        if isinstance(tags, dict):
            tags = [tags]
        managed_policy_template_params["tags"] = tags

    try:
        managed_policy = ManagedPolicyTemplate(
            file_path=existing_template_map.get(
                managed_policy_name,
                get_templated_managed_policy_file_path(
                    managed_policy_dir, managed_policy_name
                ),
            ),
            **managed_policy_template_params,
        )
        managed_policy.write()
    except Exception as err:
        log.info(str(err), managed_policy_params=managed_policy_template_params)


async def generate_aws_managed_policy_templates(
    configs: list[Config], base_output_dir: str
):
    aws_account_map = get_aws_account_map(configs)
    existing_template_map = await get_existing_template_file_map(
        base_output_dir, "NOQ::IAM::ManagedPolicy"
    )
    resource_dir = get_managed_policy_dir(base_output_dir)
    generate_account_managed_policy_resource_files_semaphore = NoqSemaphore(
infile
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


infile
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
infile
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
    existing_template_map: dict,
):
    account_id_to_mp_map = {}
    num_of_accounts = len(managed_policy_refs)
    for managed_policy_ref in managed_policy_refs:
        async with aiofiles.open(managed_policy_ref["file_path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_mp_map[
                managed_policy_ref["account_id"]
            ] = normalize_boto3_resp(content_dict)

    # Generate the params used for attribute creation
    managed_policy_template_params = {"policy_name": managed_policy_name}
    path_resources = list()
    description_resources = list()
    policy_document_resources = list()
    tag_resources = list()
    for account_id, managed_policy_dict in account_id_to_mp_map.items():
        path_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["path"]}],
            }
        )
        policy_document_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["policy_document"]}],
            }
        )

        if tags := managed_policy_dict.get("tags"):
            tag_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": tag} for tag in tags],
                }
            )

        if description := managed_policy_dict.get("description"):
            description_resources.append(
                {"account_id": account_id, "resources": [{"resource_val": description}]}
            )

    if len(managed_policy_refs) != len(aws_account_map):
        managed_policy_template_params["included_accounts"] = [
            aws_account_map[managed_policy_ref["account_id"]].account_name
            for managed_policy_ref in managed_policy_refs
        ]

    path = await group_int_or_str_attribute(
        aws_account_map, num_of_accounts, path_resources, "path"
    )
    if path != "/":
        managed_policy_template_params["path"] = path

    managed_policy_template_params["policy_document"] = await group_dict_attribute(
        aws_account_map, num_of_accounts, policy_document_resources, True
    )

    if description_resources:
        managed_policy_template_params[
            "description"
        ] = await group_int_or_str_attribute(
            aws_account_map, num_of_accounts, description_resources, "description"
        )

    if tag_resources:
        tags = await group_dict_attribute(
            aws_account_map, num_of_accounts, tag_resources, True
        )
        if isinstance(tags, dict):
            tags = [tags]
        managed_policy_template_params["tags"] = tags

    try:
        managed_policy = ManagedPolicyTemplate(
            file_path=existing_template_map.get(
                managed_policy_name,
infile
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
    existing_template_map: dict,
):
    account_id_to_mp_map = {}
    num_of_accounts = len(managed_policy_refs)
    for managed_policy_ref in managed_policy_refs:
        async with aiofiles.open(managed_policy_ref["file_path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_mp_map[
                managed_policy_ref["account_id"]
            ] = normalize_boto3_resp(content_dict)

    # Generate the params used for attribute creation
    managed_policy_template_params = {"policy_name": managed_policy_name}
    path_resources = list()
    description_resources = list()
    policy_document_resources = list()
    tag_resources = list()
    for account_id, managed_policy_dict in account_id_to_mp_map.items():
        path_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["path"]}],
            }
        )
        policy_document_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["policy_document"]}],
            }
        )

        if tags := managed_policy_dict.get("tags"):
            tag_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": tag} for tag in tags],
                }
            )

        if description := managed_policy_dict.get("description"):
            description_resources.append(
                {"account_id": account_id, "resources": [{"resource_val": description}]}
            )

    if len(managed_policy_refs) != len(aws_account_map):
        managed_policy_template_params["included_accounts"] = [
            aws_account_map[managed_policy_ref["account_id"]].account_name
            for managed_policy_ref in managed_policy_refs
        ]

    path = await group_int_or_str_attribute(
        aws_account_map, num_of_accounts, path_resources, "path"
    )
    if path != "/":
        managed_policy_template_params["path"] = path

    managed_policy_template_params["policy_document"] = await group_dict_attribute(
        aws_account_map, num_of_accounts, policy_document_resources, True
    )

    if description_resources:
        managed_policy_template_params[
            "description"
        ] = await group_int_or_str_attribute(
            aws_account_map, num_of_accounts, description_resources, "description"
        )

    if tag_resources:
        tags = await group_dict_attribute(
            aws_account_map, num_of_accounts, tag_resources, True
        )
        if isinstance(tags, dict):
            tags = [tags]
        managed_policy_template_params["tags"] = tags

    try:
        managed_policy = ManagedPolicyTemplate(
            file_path=existing_template_map.get(
                managed_policy_name,
                get_templated_managed_policy_file_path(
                    managed_policy_dir, managed_policy_name
                ),
            ),
            **managed_policy_template_params,
        )
        managed_policy.write()
    except Exception as err:
        log.info(str(err), managed_policy_params=managed_policy_template_params)


infile
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
    existing_template_map: dict,
):
    account_id_to_mp_map = {}
    num_of_accounts = len(managed_policy_refs)
    for managed_policy_ref in managed_policy_refs:
        async with aiofiles.open(managed_policy_ref["file_path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_mp_map[
                managed_policy_ref["account_id"]
            ] = normalize_boto3_resp(content_dict)

    # Generate the params used for attribute creation
    managed_policy_template_params = {"policy_name": managed_policy_name}
    path_resources = list()
    description_resources = list()
    policy_document_resources = list()
    tag_resources = list()
    for account_id, managed_policy_dict in account_id_to_mp_map.items():
        path_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["path"]}],
            }
        )
        policy_document_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["policy_document"]}],
            }
        )

        if tags := managed_policy_dict.get("tags"):
            tag_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": tag} for tag in tags],
                }
            )

        if description := managed_policy_dict.get("description"):
            description_resources.append(
                {"account_id": account_id, "resources": [{"resource_val": description}]}
            )

    if len(managed_policy_refs) != len(aws_account_map):
        managed_policy_template_params["included_accounts"] = [
            aws_account_map[managed_policy_ref["account_id"]].account_name
            for managed_policy_ref in managed_policy_refs
        ]

inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
    existing_template_map: dict,
):
    account_id_to_mp_map = {}
    num_of_accounts = len(managed_policy_refs)
    for managed_policy_ref in managed_policy_refs:
        async with aiofiles.open(managed_policy_ref["file_path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_mp_map[
                managed_policy_ref["account_id"]
            ] = normalize_boto3_resp(content_dict)

    # Generate the params used for attribute creation
    managed_policy_template_params = {"policy_name": managed_policy_name}
    path_resources = list()
    description_resources = list()
    policy_document_resources = list()
    tag_resources = list()
    for account_id, managed_policy_dict in account_id_to_mp_map.items():
        path_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["path"]}],
            }
        )
        policy_document_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["policy_document"]}],
            }
        )

        if tags := managed_policy_dict.get("tags"):
            tag_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": tag} for tag in tags],
                }
            )

        if description := managed_policy_dict.get("description"):
            description_resources.append(
                {"account_id": account_id, "resources": [{"resource_val": description}]}
            )

    if len(managed_policy_refs) != len(aws_account_map):
        managed_policy_template_params["included_accounts"] = [
            aws_account_map[managed_policy_ref["account_id"]].account_name
            for managed_policy_ref in managed_policy_refs
        ]

    path = await group_int_or_str_attribute(
        aws_account_map, num_of_accounts, path_resources, "path"
    )
    if path != "/":
        managed_policy_template_params["path"] = path

inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
    existing_template_map: dict,
):
    account_id_to_mp_map = {}
    num_of_accounts = len(managed_policy_refs)
    for managed_policy_ref in managed_policy_refs:
        async with aiofiles.open(managed_policy_ref["file_path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_mp_map[
                managed_policy_ref["account_id"]
            ] = normalize_boto3_resp(content_dict)

    # Generate the params used for attribute creation
    managed_policy_template_params = {"policy_name": managed_policy_name}
    path_resources = list()
    description_resources = list()
    policy_document_resources = list()
    tag_resources = list()
    for account_id, managed_policy_dict in account_id_to_mp_map.items():
        path_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["path"]}],
            }
        )
        policy_document_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["policy_document"]}],
            }
        )

        if tags := managed_policy_dict.get("tags"):
            tag_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": tag} for tag in tags],
                }
            )

        if description := managed_policy_dict.get("description"):
            description_resources.append(
                {"account_id": account_id, "resources": [{"resource_val": description}]}
            )

    if len(managed_policy_refs) != len(aws_account_map):
        managed_policy_template_params["included_accounts"] = [
            aws_account_map[managed_policy_ref["account_id"]].account_name
            for managed_policy_ref in managed_policy_refs
        ]

    path = await group_int_or_str_attribute(
        aws_account_map, num_of_accounts, path_resources, "path"
    )
    if path != "/":
        managed_policy_template_params["path"] = path

    managed_policy_template_params["policy_document"] = await group_dict_attribute(
        aws_account_map, num_of_accounts, policy_document_resources, True
    )

    if description_resources:
        managed_policy_template_params[
            "description"
inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
    existing_template_map: dict,
):
    account_id_to_mp_map = {}
    num_of_accounts = len(managed_policy_refs)
    for managed_policy_ref in managed_policy_refs:
        async with aiofiles.open(managed_policy_ref["file_path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_mp_map[
                managed_policy_ref["account_id"]
            ] = normalize_boto3_resp(content_dict)

    # Generate the params used for attribute creation
    managed_policy_template_params = {"policy_name": managed_policy_name}
    path_resources = list()
    description_resources = list()
    policy_document_resources = list()
    tag_resources = list()
    for account_id, managed_policy_dict in account_id_to_mp_map.items():
        path_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["path"]}],
            }
        )
        policy_document_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["policy_document"]}],
            }
        )

        if tags := managed_policy_dict.get("tags"):
            tag_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": tag} for tag in tags],
                }
            )

        if description := managed_policy_dict.get("description"):
            description_resources.append(
                {"account_id": account_id, "resources": [{"resource_val": description}]}
            )

    if len(managed_policy_refs) != len(aws_account_map):
        managed_policy_template_params["included_accounts"] = [
            aws_account_map[managed_policy_ref["account_id"]].account_name
            for managed_policy_ref in managed_policy_refs
        ]

    path = await group_int_or_str_attribute(
        aws_account_map, num_of_accounts, path_resources, "path"
    )
    if path != "/":
        managed_policy_template_params["path"] = path

    managed_policy_template_params["policy_document"] = await group_dict_attribute(
        aws_account_map, num_of_accounts, policy_document_resources, True
    )

    if description_resources:
        managed_policy_template_params[
            "description"
        ] = await group_int_or_str_attribute(
            aws_account_map, num_of_accounts, description_resources, "description"
        )

    if tag_resources:
inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
    existing_template_map: dict,
):
    account_id_to_mp_map = {}
    num_of_accounts = len(managed_policy_refs)
    for managed_policy_ref in managed_policy_refs:
        async with aiofiles.open(managed_policy_ref["file_path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_mp_map[
                managed_policy_ref["account_id"]
            ] = normalize_boto3_resp(content_dict)

    # Generate the params used for attribute creation
    managed_policy_template_params = {"policy_name": managed_policy_name}
    path_resources = list()
    description_resources = list()
    policy_document_resources = list()
    tag_resources = list()
    for account_id, managed_policy_dict in account_id_to_mp_map.items():
        path_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["path"]}],
            }
        )
        policy_document_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["policy_document"]}],
            }
        )

        if tags := managed_policy_dict.get("tags"):
            tag_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": tag} for tag in tags],
                }
            )

        if description := managed_policy_dict.get("description"):
            description_resources.append(
                {"account_id": account_id, "resources": [{"resource_val": description}]}
            )

    if len(managed_policy_refs) != len(aws_account_map):
        managed_policy_template_params["included_accounts"] = [
            aws_account_map[managed_policy_ref["account_id"]].account_name
            for managed_policy_ref in managed_policy_refs
        ]

    path = await group_int_or_str_attribute(
        aws_account_map, num_of_accounts, path_resources, "path"
    )
    if path != "/":
        managed_policy_template_params["path"] = path

    managed_policy_template_params["policy_document"] = await group_dict_attribute(
        aws_account_map, num_of_accounts, policy_document_resources, True
    )

    if description_resources:
        managed_policy_template_params[
            "description"
        ] = await group_int_or_str_attribute(
            aws_account_map, num_of_accounts, description_resources, "description"
        )

    if tag_resources:
        tags = await group_dict_attribute(
            aws_account_map, num_of_accounts, tag_resources, True
        )
inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
    existing_template_map: dict,
):
    account_id_to_mp_map = {}
    num_of_accounts = len(managed_policy_refs)
    for managed_policy_ref in managed_policy_refs:
        async with aiofiles.open(managed_policy_ref["file_path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_mp_map[
                managed_policy_ref["account_id"]
            ] = normalize_boto3_resp(content_dict)

    # Generate the params used for attribute creation
    managed_policy_template_params = {"policy_name": managed_policy_name}
    path_resources = list()
    description_resources = list()
    policy_document_resources = list()
    tag_resources = list()
    for account_id, managed_policy_dict in account_id_to_mp_map.items():
        path_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["path"]}],
            }
        )
        policy_document_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["policy_document"]}],
            }
        )

        if tags := managed_policy_dict.get("tags"):
            tag_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": tag} for tag in tags],
                }
            )

        if description := managed_policy_dict.get("description"):
            description_resources.append(
                {"account_id": account_id, "resources": [{"resource_val": description}]}
            )

    if len(managed_policy_refs) != len(aws_account_map):
        managed_policy_template_params["included_accounts"] = [
            aws_account_map[managed_policy_ref["account_id"]].account_name
            for managed_policy_ref in managed_policy_refs
        ]

    path = await group_int_or_str_attribute(
        aws_account_map, num_of_accounts, path_resources, "path"
    )
    if path != "/":
        managed_policy_template_params["path"] = path

    managed_policy_template_params["policy_document"] = await group_dict_attribute(
        aws_account_map, num_of_accounts, policy_document_resources, True
    )

    if description_resources:
        managed_policy_template_params[
            "description"
        ] = await group_int_or_str_attribute(
            aws_account_map, num_of_accounts, description_resources, "description"
        )

    if tag_resources:
        tags = await group_dict_attribute(
            aws_account_map, num_of_accounts, tag_resources, True
        )
        if isinstance(tags, dict):
            tags = [tags]
        managed_policy_template_params["tags"] = tags

    try:
inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
    existing_template_map: dict,
):
    account_id_to_mp_map = {}
    num_of_accounts = len(managed_policy_refs)
    for managed_policy_ref in managed_policy_refs:
        async with aiofiles.open(managed_policy_ref["file_path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_mp_map[
                managed_policy_ref["account_id"]
            ] = normalize_boto3_resp(content_dict)

    # Generate the params used for attribute creation
    managed_policy_template_params = {"policy_name": managed_policy_name}
    path_resources = list()
    description_resources = list()
    policy_document_resources = list()
    tag_resources = list()
    for account_id, managed_policy_dict in account_id_to_mp_map.items():
        path_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["path"]}],
            }
        )
        policy_document_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["policy_document"]}],
            }
        )

        if tags := managed_policy_dict.get("tags"):
            tag_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": tag} for tag in tags],
                }
            )

        if description := managed_policy_dict.get("description"):
            description_resources.append(
                {"account_id": account_id, "resources": [{"resource_val": description}]}
            )

    if len(managed_policy_refs) != len(aws_account_map):
        managed_policy_template_params["included_accounts"] = [
            aws_account_map[managed_policy_ref["account_id"]].account_name
            for managed_policy_ref in managed_policy_refs
        ]

    path = await group_int_or_str_attribute(
        aws_account_map, num_of_accounts, path_resources, "path"
    )
    if path != "/":
        managed_policy_template_params["path"] = path

    managed_policy_template_params["policy_document"] = await group_dict_attribute(
        aws_account_map, num_of_accounts, policy_document_resources, True
    )

    if description_resources:
        managed_policy_template_params[
            "description"
        ] = await group_int_or_str_attribute(
            aws_account_map, num_of_accounts, description_resources, "description"
        )

    if tag_resources:
        tags = await group_dict_attribute(
            aws_account_map, num_of_accounts, tag_resources, True
        )
        if isinstance(tags, dict):
            tags = [tags]
        managed_policy_template_params["tags"] = tags

    try:
        managed_policy = ManagedPolicyTemplate(
            file_path=existing_template_map.get(
                managed_policy_name,
                get_templated_managed_policy_file_path(
                    managed_policy_dir, managed_policy_name
                ),
            ),
            **managed_policy_template_params,
        )
inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
    existing_template_map: dict,
):
    account_id_to_mp_map = {}
    num_of_accounts = len(managed_policy_refs)
    for managed_policy_ref in managed_policy_refs:
        async with aiofiles.open(managed_policy_ref["file_path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_mp_map[
                managed_policy_ref["account_id"]
            ] = normalize_boto3_resp(content_dict)

    # Generate the params used for attribute creation
    managed_policy_template_params = {"policy_name": managed_policy_name}
    path_resources = list()
    description_resources = list()
    policy_document_resources = list()
    tag_resources = list()
    for account_id, managed_policy_dict in account_id_to_mp_map.items():
        path_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["path"]}],
            }
        )
        policy_document_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["policy_document"]}],
            }
        )

        if tags := managed_policy_dict.get("tags"):
            tag_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": tag} for tag in tags],
                }
            )

        if description := managed_policy_dict.get("description"):
            description_resources.append(
                {"account_id": account_id, "resources": [{"resource_val": description}]}
            )

    if len(managed_policy_refs) != len(aws_account_map):
        managed_policy_template_params["included_accounts"] = [
            aws_account_map[managed_policy_ref["account_id"]].account_name
            for managed_policy_ref in managed_policy_refs
        ]

    path = await group_int_or_str_attribute(
        aws_account_map, num_of_accounts, path_resources, "path"
    )
    if path != "/":
        managed_policy_template_params["path"] = path

    managed_policy_template_params["policy_document"] = await group_dict_attribute(
        aws_account_map, num_of_accounts, policy_document_resources, True
    )

    if description_resources:
        managed_policy_template_params[
            "description"
        ] = await group_int_or_str_attribute(
            aws_account_map, num_of_accounts, description_resources, "description"
        )

    if tag_resources:
        tags = await group_dict_attribute(
            aws_account_map, num_of_accounts, tag_resources, True
        )
        if isinstance(tags, dict):
            tags = [tags]
        managed_policy_template_params["tags"] = tags

    try:
        managed_policy = ManagedPolicyTemplate(
            file_path=existing_template_map.get(
                managed_policy_name,
                get_templated_managed_policy_file_path(
                    managed_policy_dir, managed_policy_name
                ),
            ),
            **managed_policy_template_params,
        )
        managed_policy.write()
    except Exception as err:
        log.info(str(err), managed_policy_params=managed_policy_template_params)


async def generate_aws_managed_policy_templates(
inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
    existing_template_map: dict,
):
    account_id_to_mp_map = {}
    num_of_accounts = len(managed_policy_refs)
    for managed_policy_ref in managed_policy_refs:
        async with aiofiles.open(managed_policy_ref["file_path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_mp_map[
                managed_policy_ref["account_id"]
            ] = normalize_boto3_resp(content_dict)

    # Generate the params used for attribute creation
    managed_policy_template_params = {"policy_name": managed_policy_name}
    path_resources = list()
    description_resources = list()
    policy_document_resources = list()
    tag_resources = list()
    for account_id, managed_policy_dict in account_id_to_mp_map.items():
        path_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["path"]}],
            }
        )
        policy_document_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["policy_document"]}],
            }
        )

        if tags := managed_policy_dict.get("tags"):
            tag_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": tag} for tag in tags],
                }
            )

        if description := managed_policy_dict.get("description"):
            description_resources.append(
                {"account_id": account_id, "resources": [{"resource_val": description}]}
            )

    if len(managed_policy_refs) != len(aws_account_map):
        managed_policy_template_params["included_accounts"] = [
            aws_account_map[managed_policy_ref["account_id"]].account_name
            for managed_policy_ref in managed_policy_refs
        ]

    path = await group_int_or_str_attribute(
        aws_account_map, num_of_accounts, path_resources, "path"
    )
    if path != "/":
        managed_policy_template_params["path"] = path

    managed_policy_template_params["policy_document"] = await group_dict_attribute(
        aws_account_map, num_of_accounts, policy_document_resources, True
    )

    if description_resources:
        managed_policy_template_params[
            "description"
        ] = await group_int_or_str_attribute(
            aws_account_map, num_of_accounts, description_resources, "description"
        )

    if tag_resources:
        tags = await group_dict_attribute(
            aws_account_map, num_of_accounts, tag_resources, True
        )
        if isinstance(tags, dict):
            tags = [tags]
        managed_policy_template_params["tags"] = tags

    try:
        managed_policy = ManagedPolicyTemplate(
            file_path=existing_template_map.get(
                managed_policy_name,
                get_templated_managed_policy_file_path(
                    managed_policy_dir, managed_policy_name
                ),
            ),
            **managed_policy_template_params,
        )
        managed_policy.write()
    except Exception as err:
        log.info(str(err), managed_policy_params=managed_policy_template_params)


async def generate_aws_managed_policy_templates(
    configs: list[Config], base_output_dir: str
):
inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
    existing_template_map: dict,
):
    account_id_to_mp_map = {}
    num_of_accounts = len(managed_policy_refs)
    for managed_policy_ref in managed_policy_refs:
        async with aiofiles.open(managed_policy_ref["file_path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_mp_map[
                managed_policy_ref["account_id"]
            ] = normalize_boto3_resp(content_dict)

    # Generate the params used for attribute creation
    managed_policy_template_params = {"policy_name": managed_policy_name}
    path_resources = list()
    description_resources = list()
    policy_document_resources = list()
    tag_resources = list()
    for account_id, managed_policy_dict in account_id_to_mp_map.items():
        path_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["path"]}],
            }
        )
        policy_document_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["policy_document"]}],
            }
        )

        if tags := managed_policy_dict.get("tags"):
            tag_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": tag} for tag in tags],
                }
            )

        if description := managed_policy_dict.get("description"):
            description_resources.append(
                {"account_id": account_id, "resources": [{"resource_val": description}]}
            )

    if len(managed_policy_refs) != len(aws_account_map):
        managed_policy_template_params["included_accounts"] = [
            aws_account_map[managed_policy_ref["account_id"]].account_name
            for managed_policy_ref in managed_policy_refs
        ]

    path = await group_int_or_str_attribute(
        aws_account_map, num_of_accounts, path_resources, "path"
    )
    if path != "/":
        managed_policy_template_params["path"] = path

    managed_policy_template_params["policy_document"] = await group_dict_attribute(
        aws_account_map, num_of_accounts, policy_document_resources, True
    )

    if description_resources:
        managed_policy_template_params[
            "description"
        ] = await group_int_or_str_attribute(
            aws_account_map, num_of_accounts, description_resources, "description"
        )

    if tag_resources:
        tags = await group_dict_attribute(
            aws_account_map, num_of_accounts, tag_resources, True
        )
        if isinstance(tags, dict):
            tags = [tags]
        managed_policy_template_params["tags"] = tags

    try:
        managed_policy = ManagedPolicyTemplate(
            file_path=existing_template_map.get(
                managed_policy_name,
                get_templated_managed_policy_file_path(
                    managed_policy_dir, managed_policy_name
                ),
            ),
            **managed_policy_template_params,
        )
        managed_policy.write()
    except Exception as err:
        log.info(str(err), managed_policy_params=managed_policy_template_params)


async def generate_aws_managed_policy_templates(
    configs: list[Config], base_output_dir: str
):
    aws_account_map = get_aws_account_map(configs)
inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
    existing_template_map: dict,
):
    account_id_to_mp_map = {}
    num_of_accounts = len(managed_policy_refs)
    for managed_policy_ref in managed_policy_refs:
        async with aiofiles.open(managed_policy_ref["file_path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_mp_map[
                managed_policy_ref["account_id"]
            ] = normalize_boto3_resp(content_dict)

    # Generate the params used for attribute creation
    managed_policy_template_params = {"policy_name": managed_policy_name}
    path_resources = list()
    description_resources = list()
    policy_document_resources = list()
    tag_resources = list()
    for account_id, managed_policy_dict in account_id_to_mp_map.items():
        path_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["path"]}],
            }
        )
        policy_document_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["policy_document"]}],
            }
        )

        if tags := managed_policy_dict.get("tags"):
            tag_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": tag} for tag in tags],
                }
            )

        if description := managed_policy_dict.get("description"):
            description_resources.append(
                {"account_id": account_id, "resources": [{"resource_val": description}]}
            )

    if len(managed_policy_refs) != len(aws_account_map):
        managed_policy_template_params["included_accounts"] = [
            aws_account_map[managed_policy_ref["account_id"]].account_name
            for managed_policy_ref in managed_policy_refs
        ]

    path = await group_int_or_str_attribute(
        aws_account_map, num_of_accounts, path_resources, "path"
    )
    if path != "/":
        managed_policy_template_params["path"] = path

    managed_policy_template_params["policy_document"] = await group_dict_attribute(
        aws_account_map, num_of_accounts, policy_document_resources, True
    )

    if description_resources:
        managed_policy_template_params[
            "description"
        ] = await group_int_or_str_attribute(
            aws_account_map, num_of_accounts, description_resources, "description"
        )

    if tag_resources:
        tags = await group_dict_attribute(
            aws_account_map, num_of_accounts, tag_resources, True
        )
        if isinstance(tags, dict):
            tags = [tags]
        managed_policy_template_params["tags"] = tags

    try:
        managed_policy = ManagedPolicyTemplate(
            file_path=existing_template_map.get(
                managed_policy_name,
                get_templated_managed_policy_file_path(
                    managed_policy_dir, managed_policy_name
                ),
            ),
            **managed_policy_template_params,
        )
        managed_policy.write()
    except Exception as err:
        log.info(str(err), managed_policy_params=managed_policy_template_params)


async def generate_aws_managed_policy_templates(
    configs: list[Config], base_output_dir: str
):
    aws_account_map = get_aws_account_map(configs)
    existing_template_map = await get_existing_template_file_map(
        base_output_dir, "NOQ::IAM::ManagedPolicy"
    )
    resource_dir = get_managed_policy_dir(base_output_dir)
inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
    existing_template_map: dict,
):
    account_id_to_mp_map = {}
    num_of_accounts = len(managed_policy_refs)
    for managed_policy_ref in managed_policy_refs:
        async with aiofiles.open(managed_policy_ref["file_path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_mp_map[
                managed_policy_ref["account_id"]
            ] = normalize_boto3_resp(content_dict)

    # Generate the params used for attribute creation
    managed_policy_template_params = {"policy_name": managed_policy_name}
    path_resources = list()
    description_resources = list()
    policy_document_resources = list()
    tag_resources = list()
    for account_id, managed_policy_dict in account_id_to_mp_map.items():
        path_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["path"]}],
            }
        )
        policy_document_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["policy_document"]}],
            }
        )

        if tags := managed_policy_dict.get("tags"):
            tag_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": tag} for tag in tags],
                }
            )

        if description := managed_policy_dict.get("description"):
            description_resources.append(
                {"account_id": account_id, "resources": [{"resource_val": description}]}
            )

    if len(managed_policy_refs) != len(aws_account_map):
        managed_policy_template_params["included_accounts"] = [
            aws_account_map[managed_policy_ref["account_id"]].account_name
            for managed_policy_ref in managed_policy_refs
        ]

    path = await group_int_or_str_attribute(
        aws_account_map, num_of_accounts, path_resources, "path"
    )
    if path != "/":
        managed_policy_template_params["path"] = path

    managed_policy_template_params["policy_document"] = await group_dict_attribute(
        aws_account_map, num_of_accounts, policy_document_resources, True
    )

    if description_resources:
        managed_policy_template_params[
            "description"
        ] = await group_int_or_str_attribute(
            aws_account_map, num_of_accounts, description_resources, "description"
        )

    if tag_resources:
        tags = await group_dict_attribute(
            aws_account_map, num_of_accounts, tag_resources, True
        )
        if isinstance(tags, dict):
            tags = [tags]
        managed_policy_template_params["tags"] = tags

    try:
        managed_policy = ManagedPolicyTemplate(
            file_path=existing_template_map.get(
                managed_policy_name,
                get_templated_managed_policy_file_path(
                    managed_policy_dir, managed_policy_name
                ),
            ),
            **managed_policy_template_params,
        )
        managed_policy.write()
    except Exception as err:
        log.info(str(err), managed_policy_params=managed_policy_template_params)


async def generate_aws_managed_policy_templates(
    configs: list[Config], base_output_dir: str
):
    aws_account_map = get_aws_account_map(configs)
    existing_template_map = await get_existing_template_file_map(
        base_output_dir, "NOQ::IAM::ManagedPolicy"
    )
    resource_dir = get_managed_policy_dir(base_output_dir)
    generate_account_managed_policy_resource_files_semaphore = NoqSemaphore(
        generate_account_managed_policy_resource_files, 25
    )

    log.info("Generating AWS managed policy templates.")
    log.info(
        "Beginning to retrieve AWS IAM Managed Policies.",
        accounts=list(aws_account_map.keys()),
    )

    account_managed_policies = (
inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
    existing_template_map: dict,
):
    account_id_to_mp_map = {}
    num_of_accounts = len(managed_policy_refs)
    for managed_policy_ref in managed_policy_refs:
        async with aiofiles.open(managed_policy_ref["file_path"], mode="r") as f:
inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
    existing_template_map: dict,
):
    account_id_to_mp_map = {}
    num_of_accounts = len(managed_policy_refs)
    for managed_policy_ref in managed_policy_refs:
        async with aiofiles.open(managed_policy_ref["file_path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_mp_map[
                managed_policy_ref["account_id"]
inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
    existing_template_map: dict,
):
    account_id_to_mp_map = {}
    num_of_accounts = len(managed_policy_refs)
    for managed_policy_ref in managed_policy_refs:
        async with aiofiles.open(managed_policy_ref["file_path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_mp_map[
                managed_policy_ref["account_id"]
            ] = normalize_boto3_resp(content_dict)

    # Generate the params used for attribute creation
    managed_policy_template_params = {"policy_name": managed_policy_name}
    path_resources = list()
    description_resources = list()
    policy_document_resources = list()
    tag_resources = list()
    for account_id, managed_policy_dict in account_id_to_mp_map.items():
        path_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["path"]}],
            }
        )
        policy_document_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["policy_document"]}],
            }
        )

        if tags := managed_policy_dict.get("tags"):
            tag_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": tag} for tag in tags],
                }
            )

        if description := managed_policy_dict.get("description"):
            description_resources.append(
                {"account_id": account_id, "resources": [{"resource_val": description}]}
            )

    if len(managed_policy_refs) != len(aws_account_map):
        managed_policy_template_params["included_accounts"] = [
            aws_account_map[managed_policy_ref["account_id"]].account_name
            for managed_policy_ref in managed_policy_refs
        ]

    path = await group_int_or_str_attribute(
        aws_account_map, num_of_accounts, path_resources, "path"
    )
    if path != "/":
        managed_policy_template_params["path"] = path

    managed_policy_template_params["policy_document"] = await group_dict_attribute(
        aws_account_map, num_of_accounts, policy_document_resources, True
    )

    if description_resources:
        managed_policy_template_params[
            "description"
        ] = await group_int_or_str_attribute(
            aws_account_map, num_of_accounts, description_resources, "description"
        )

    if tag_resources:
        tags = await group_dict_attribute(
            aws_account_map, num_of_accounts, tag_resources, True
        )
        if isinstance(tags, dict):
            tags = [tags]
        managed_policy_template_params["tags"] = tags

    try:
        managed_policy = ManagedPolicyTemplate(
            file_path=existing_template_map.get(
                managed_policy_name,
                get_templated_managed_policy_file_path(
                    managed_policy_dir, managed_policy_name
                ),
            ),
            **managed_policy_template_params,
        )
        managed_policy.write()
    except Exception as err:
        log.info(str(err), managed_policy_params=managed_policy_template_params)


async def generate_aws_managed_policy_templates(
    configs: list[Config], base_output_dir: str
):
    aws_account_map = get_aws_account_map(configs)
    existing_template_map = await get_existing_template_file_map(
        base_output_dir, "NOQ::IAM::ManagedPolicy"
    )
    resource_dir = get_managed_policy_dir(base_output_dir)
    generate_account_managed_policy_resource_files_semaphore = NoqSemaphore(
        generate_account_managed_policy_resource_files, 25
    )

    log.info("Generating AWS managed policy templates.")
    log.info(
        "Beginning to retrieve AWS IAM Managed Policies.",
        accounts=list(aws_account_map.keys()),
    )

    account_managed_policies = (
        await generate_account_managed_policy_resource_files_semaphore.process(
            [{"aws_account": aws_account} for aws_account in aws_account_map.values()]
        )
    )
    messages = []
    for account in account_managed_policies:
        for managed_policy in account["managed_policies"]:
            messages.append(
                {
                    "policy_name": managed_policy["policy_name"],
                    "arn": managed_policy["arn"],
                    "file_path": managed_policy["file_path"],
                    "aws_account": aws_account_map[managed_policy["account_id"]],
                }
            )

    log.info("Finished retrieving managed policy details")

    # Use these for testing `create_templated_managed_policy`
    # account_managed_policy_output = json.dumps(account_managed_policies)
    # with open("account_managed_policy_output.json", "w") as f:
    #     f.write(account_managed_policy_output)
    # with open("account_managed_policy_output.json") as f:
    #     account_managed_policies = json.loads(f.read())

    log.info("Grouping managed policies")
    # Move everything to required structure
    for account_mp_elem in range(len(account_managed_policies)):
        for mp_elem in range(
            len(account_managed_policies[account_mp_elem]["managed_policies"])
        ):
            policy_name = account_managed_policies[account_mp_elem]["managed_policies"][
                mp_elem
            ].pop("policy_name")
            account_managed_policies[account_mp_elem]["managed_policies"][mp_elem][
                "resource_val"
            ] = policy_name

        account_managed_policies[account_mp_elem][
            "resources"
        ] = account_managed_policies[account_mp_elem].pop("managed_policies", [])

inproject
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
    existing_template_map: dict,
):
    account_id_to_mp_map = {}
    num_of_accounts = len(managed_policy_refs)
    for managed_policy_ref in managed_policy_refs:
        async with aiofiles.open(managed_policy_ref["file_path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_mp_map[
                managed_policy_ref["account_id"]
            ] = normalize_boto3_resp(content_dict)

    # Generate the params used for attribute creation
    managed_policy_template_params = {"policy_name": managed_policy_name}
    path_resources = list()
    description_resources = list()
    policy_document_resources = list()
    tag_resources = list()
    for account_id, managed_policy_dict in account_id_to_mp_map.items():
        path_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["path"]}],
            }
        )
        policy_document_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["policy_document"]}],
            }
        )

        if tags := managed_policy_dict.get("tags"):
            tag_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": tag} for tag in tags],
                }
            )

        if description := managed_policy_dict.get("description"):
            description_resources.append(
                {"account_id": account_id, "resources": [{"resource_val": description}]}
            )

    if len(managed_policy_refs) != len(aws_account_map):
        managed_policy_template_params["included_accounts"] = [
            aws_account_map[managed_policy_ref["account_id"]].account_name
            for managed_policy_ref in managed_policy_refs
        ]

    path = await group_int_or_str_attribute(
        aws_account_map, num_of_accounts, path_resources, "path"
    )
    if path != "/":
        managed_policy_template_params["path"] = path

    managed_policy_template_params["policy_document"] = await group_dict_attribute(
        aws_account_map, num_of_accounts, policy_document_resources, True
    )

    if description_resources:
        managed_policy_template_params[
            "description"
        ] = await group_int_or_str_attribute(
            aws_account_map, num_of_accounts, description_resources, "description"
        )

    if tag_resources:
        tags = await group_dict_attribute(
            aws_account_map, num_of_accounts, tag_resources, True
        )
        if isinstance(tags, dict):
            tags = [tags]
        managed_policy_template_params["tags"] = tags

    try:
        managed_policy = ManagedPolicyTemplate(
common
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
    existing_template_map: dict,
):
    account_id_to_mp_map = {}
    num_of_accounts = len(managed_policy_refs)
    for managed_policy_ref in managed_policy_refs:
        async with aiofiles.open(managed_policy_ref["file_path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_mp_map[
                managed_policy_ref["account_id"]
            ] = normalize_boto3_resp(content_dict)

    # Generate the params used for attribute creation
    managed_policy_template_params = {"policy_name": managed_policy_name}
    path_resources = list()
    description_resources = list()
    policy_document_resources = list()
    tag_resources = list()
    for account_id, managed_policy_dict in account_id_to_mp_map.items():
        path_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["path"]}],
            }
        )
        policy_document_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["policy_document"]}],
            }
        )

common
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
    existing_template_map: dict,
):
    account_id_to_mp_map = {}
    num_of_accounts = len(managed_policy_refs)
    for managed_policy_ref in managed_policy_refs:
        async with aiofiles.open(managed_policy_ref["file_path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_mp_map[
                managed_policy_ref["account_id"]
            ] = normalize_boto3_resp(content_dict)

    # Generate the params used for attribute creation
    managed_policy_template_params = {"policy_name": managed_policy_name}
    path_resources = list()
    description_resources = list()
    policy_document_resources = list()
    tag_resources = list()
    for account_id, managed_policy_dict in account_id_to_mp_map.items():
        path_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["path"]}],
            }
        )
        policy_document_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["policy_document"]}],
            }
        )

        if tags := managed_policy_dict.get("tags"):
            tag_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": tag} for tag in tags],
                }
            )

common
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


non_informative
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
    existing_template_map: dict,
):
    account_id_to_mp_map = {}
    num_of_accounts = len(managed_policy_refs)
    for managed_policy_ref in managed_policy_refs:
        async with aiofiles.open(managed_policy_ref["file_path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_mp_map[
                managed_policy_ref["account_id"]
            ] = normalize_boto3_resp(content_dict)

    # Generate the params used for attribute creation
    managed_policy_template_params = {"policy_name": managed_policy_name}
    path_resources = list()
    description_resources = list()
    policy_document_resources = list()
    tag_resources = list()
    for account_id, managed_policy_dict in account_id_to_mp_map.items():
        path_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["path"]}],
            }
        )
        policy_document_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["policy_document"]}],
            }
        )

        if tags := managed_policy_dict.get("tags"):
            tag_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": tag} for tag in tags],
                }
            )

        if description := managed_policy_dict.get("description"):
            description_resources.append(
                {"account_id": account_id, "resources": [{"resource_val": description}]}
            )
non_informative
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
random
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
    existing_template_map: dict,
):
    account_id_to_mp_map = {}
    num_of_accounts = len(managed_policy_refs)
    for managed_policy_ref in managed_policy_refs:
        async with aiofiles.open(managed_policy_ref["file_path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_mp_map[
                managed_policy_ref["account_id"]
            ] = normalize_boto3_resp(content_dict)

    # Generate the params used for attribute creation
    managed_policy_template_params = {"policy_name": managed_policy_name}
    path_resources = list()
    description_resources = list()
    policy_document_resources = list()
    tag_resources = list()
    for account_id, managed_policy_dict in account_id_to_mp_map.items():
        path_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["path"]}],
            }
        )
        policy_document_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["policy_document"]}],
            }
        )

        if tags := managed_policy_dict.get("tags"):
            tag_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": tag} for tag in tags],
                }
            )

        if description := managed_policy_dict.get("description"):
            description_resources.append(
                {"account_id": account_id, "resources": [{"resource_val": description}]}
            )

    if len(managed_policy_refs) != len(aws_account_map):
        managed_policy_template_params["included_accounts"] = [
            aws_account_map[managed_policy_ref["account_id"]].account_name
            for managed_policy_ref in managed_policy_refs
        ]

    path = await group_int_or_str_attribute(
        aws_account_map, num_of_accounts, path_resources, "path"
    )
    if path != "/":
        managed_policy_template_params["path"] = path

    managed_policy_template_params["policy_document"] = await group_dict_attribute(
        aws_account_map, num_of_accounts, policy_document_resources, True
    )

    if description_resources:
        managed_policy_template_params[
            "description"
        ] = await group_int_or_str_attribute(
random
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
    existing_template_map: dict,
):
    account_id_to_mp_map = {}
    num_of_accounts = len(managed_policy_refs)
    for managed_policy_ref in managed_policy_refs:
        async with aiofiles.open(managed_policy_ref["file_path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_mp_map[
                managed_policy_ref["account_id"]
            ] = normalize_boto3_resp(content_dict)

    # Generate the params used for attribute creation
    managed_policy_template_params = {"policy_name": managed_policy_name}
    path_resources = list()
    description_resources = list()
    policy_document_resources = list()
    tag_resources = list()
    for account_id, managed_policy_dict in account_id_to_mp_map.items():
        path_resources.append(
            {
                "account_id": account_id,
random
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
    existing_template_map: dict,
):
    account_id_to_mp_map = {}
    num_of_accounts = len(managed_policy_refs)
    for managed_policy_ref in managed_policy_refs:
        async with aiofiles.open(managed_policy_ref["file_path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_mp_map[
                managed_policy_ref["account_id"]
            ] = normalize_boto3_resp(content_dict)

    # Generate the params used for attribute creation
    managed_policy_template_params = {"policy_name": managed_policy_name}
    path_resources = list()
    description_resources = list()
    policy_document_resources = list()
    tag_resources = list()
    for account_id, managed_policy_dict in account_id_to_mp_map.items():
        path_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["path"]}],
            }
        )
        policy_document_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["policy_document"]}],
            }
        )

        if tags := managed_policy_dict.get("tags"):
            tag_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": tag} for tag in tags],
                }
            )

        if description := managed_policy_dict.get("description"):
            description_resources.append(
                {"account_id": account_id, "resources": [{"resource_val": description}]}
            )

    if len(managed_policy_refs) != len(aws_account_map):
        managed_policy_template_params["included_accounts"] = [
            aws_account_map[managed_policy_ref["account_id"]].account_name
            for managed_policy_ref in managed_policy_refs
        ]

    path = await group_int_or_str_attribute(
        aws_account_map, num_of_accounts, path_resources, "path"
    )
    if path != "/":
        managed_policy_template_params["path"] = path

    managed_policy_template_params["policy_document"] = await group_dict_attribute(
        aws_account_map, num_of_accounts, policy_document_resources, True
    )

    if description_resources:
        managed_policy_template_params[
            "description"
        ] = await group_int_or_str_attribute(
            aws_account_map, num_of_accounts, description_resources, "description"
        )

    if tag_resources:
        tags = await group_dict_attribute(
            aws_account_map, num_of_accounts, tag_resources, True
        )
        if isinstance(tags, dict):
            tags = [tags]
        managed_policy_template_params["tags"] = tags

    try:
        managed_policy = ManagedPolicyTemplate(
            file_path=existing_template_map.get(
                managed_policy_name,
                get_templated_managed_policy_file_path(
                    managed_policy_dir, managed_policy_name
                ),
            ),
            **managed_policy_template_params,
        )
        managed_policy.write()
    except Exception as err:
        log.info(str(err), managed_policy_params=managed_policy_template_params)


async def generate_aws_managed_policy_templates(
    configs: list[Config], base_output_dir: str
):
    aws_account_map = get_aws_account_map(configs)
    existing_template_map = await get_existing_template_file_map(
        base_output_dir, "NOQ::IAM::ManagedPolicy"
    )
    resource_dir = get_managed_policy_dir(base_output_dir)
    generate_account_managed_policy_resource_files_semaphore = NoqSemaphore(
        generate_account_managed_policy_resource_files, 25
    )

    log.info("Generating AWS managed policy templates.")
    log.info(
        "Beginning to retrieve AWS IAM Managed Policies.",
        accounts=list(aws_account_map.keys()),
    )

    account_managed_policies = (
        await generate_account_managed_policy_resource_files_semaphore.process(
            [{"aws_account": aws_account} for aws_account in aws_account_map.values()]
        )
    )
    messages = []
    for account in account_managed_policies:
        for managed_policy in account["managed_policies"]:
            messages.append(
                {
                    "policy_name": managed_policy["policy_name"],
                    "arn": managed_policy["arn"],
                    "file_path": managed_policy["file_path"],
                    "aws_account": aws_account_map[managed_policy["account_id"]],
                }
            )

    log.info("Finished retrieving managed policy details")

    # Use these for testing `create_templated_managed_policy`
    # account_managed_policy_output = json.dumps(account_managed_policies)
    # with open("account_managed_policy_output.json", "w") as f:
    #     f.write(account_managed_policy_output)
    # with open("account_managed_policy_output.json") as f:
    #     account_managed_policies = json.loads(f.read())

    log.info("Grouping managed policies")
    # Move everything to required structure
    for account_mp_elem in range(len(account_managed_policies)):
        for mp_elem in range(
            len(account_managed_policies[account_mp_elem]["managed_policies"])
        ):
            policy_name = account_managed_policies[account_mp_elem]["managed_policies"][
                mp_elem
            ].pop("policy_name")
            account_managed_policies[account_mp_elem]["managed_policies"][mp_elem][
                "resource_val"
            ] = policy_name

        account_managed_policies[account_mp_elem][
            "resources"
        ] = account_managed_policies[account_mp_elem].pop("managed_policies", [])

    grouped_managed_policy_map = await base_group_str_attribute(
        aws_account_map, account_managed_policies
    )

random
import os
import pathlib

import aiofiles

from iambic.aws.iam.policy.models import ManagedPolicyTemplate
from iambic.aws.iam.policy.utils import list_managed_policies
from iambic.aws.utils import get_aws_account_map, normalize_boto3_resp
from iambic.config.models import AWSAccount, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_str_attribute,
    get_existing_template_file_map,
    group_dict_attribute,
    group_int_or_str_attribute,
)
from iambic.core.utils import NoqSemaphore, resource_file_upsert

MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".iambic", "resources", "aws", "managed_policies"
)


def get_managed_policy_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "managed_policies")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):
    file_name = (
        policy_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(managed_policy_dir, f"{file_name}.yaml"))


def get_account_managed_policy_resource_dir(account_id: str) -> str:
    account_resource_dir = os.path.join(MANAGED_POLICY_RESPONSE_DIR, account_id)
    os.makedirs(account_resource_dir, exist_ok=True)
    return account_resource_dir


async def generate_account_managed_policy_resource_files(
    aws_account: AWSAccount,
) -> dict:
    account_resource_dir = get_account_managed_policy_resource_dir(
        aws_account.account_id
    )
    resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=aws_account.account_id, managed_policies=[])
    iam_client = aws_account.get_boto3_client("iam")
    account_managed_policies = await list_managed_policies(iam_client)

    log.info(
        "Retrieved AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        account_name=aws_account.account_name,
        managed_policy_count=len(account_managed_policies),
    )

    for managed_policy in account_managed_policies:
        policy_path = os.path.join(
            account_resource_dir, f'{managed_policy["PolicyName"]}.json'
        )
        response["managed_policies"].append(
            {
                "file_path": policy_path,
                "policy_name": managed_policy["PolicyName"],
                "arn": managed_policy["Arn"],
                "account_id": aws_account.account_id,
            }
        )
        messages.append(
            dict(
                file_path=policy_path, content_as_dict=managed_policy, replace_file=True
            )
        )

    await resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Managed Policies.",
        account_id=aws_account.account_id,
        managed_policy_count=len(account_managed_policies),
    )

    return response


async def create_templated_managed_policy(  # noqa: C901
    aws_account_map: dict[str, AWSAccount],
    managed_policy_name: str,
    managed_policy_refs: list[dict],
    managed_policy_dir: str,
    existing_template_map: dict,
):
    account_id_to_mp_map = {}
    num_of_accounts = len(managed_policy_refs)
    for managed_policy_ref in managed_policy_refs:
        async with aiofiles.open(managed_policy_ref["file_path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_mp_map[
                managed_policy_ref["account_id"]
            ] = normalize_boto3_resp(content_dict)

    # Generate the params used for attribute creation
    managed_policy_template_params = {"policy_name": managed_policy_name}
    path_resources = list()
    description_resources = list()
    policy_document_resources = list()
    tag_resources = list()
    for account_id, managed_policy_dict in account_id_to_mp_map.items():
        path_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["path"]}],
            }
        )
        policy_document_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": managed_policy_dict["policy_document"]}],
            }
        )

        if tags := managed_policy_dict.get("tags"):
            tag_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": tag} for tag in tags],
                }
            )

        if description := managed_policy_dict.get("description"):
            description_resources.append(
                {"account_id": account_id, "resources": [{"resource_val": description}]}
            )

    if len(managed_policy_refs) != len(aws_account_map):
        managed_policy_template_params["included_accounts"] = [
            aws_account_map[managed_policy_ref["account_id"]].account_name
            for managed_policy_ref in managed_policy_refs
        ]

    path = await group_int_or_str_attribute(
        aws_account_map, num_of_accounts, path_resources, "path"
    )
    if path != "/":
        managed_policy_template_params["path"] = path

    managed_policy_template_params["policy_document"] = await group_dict_attribute(
        aws_account_map, num_of_accounts, policy_document_resources, True
    )

    if description_resources:
        managed_policy_template_params[
            "description"
        ] = await group_int_or_str_attribute(
            aws_account_map, num_of_accounts, description_resources, "description"
        )

    if tag_resources:
        tags = await group_dict_attribute(
            aws_account_map, num_of_accounts, tag_resources, True
        )
        if isinstance(tags, dict):
            tags = [tags]
        managed_policy_template_params["tags"] = tags

    try:
        managed_policy = ManagedPolicyTemplate(
            file_path=existing_template_map.get(
                managed_policy_name,
                get_templated_managed_policy_file_path(
                    managed_policy_dir, managed_policy_name
                ),
            ),
            **managed_policy_template_params,
        )
        managed_policy.write()
    except Exception as err:
        log.info(str(err), managed_policy_params=managed_policy_template_params)


async def generate_aws_managed_policy_templates(
    configs: list[Config], base_output_dir: str
):
    aws_account_map = get_aws_account_map(configs)
    existing_template_map = await get_existing_template_file_map(
        base_output_dir, "NOQ::IAM::ManagedPolicy"
    )
    resource_dir = get_managed_policy_dir(base_output_dir)
    generate_account_managed_policy_resource_files_semaphore = NoqSemaphore(
        generate_account_managed_policy_resource_files, 25
    )

    log.info("Generating AWS managed policy templates.")
random
        await create_templated_managed_policy(     resource_dir = get_managed_policy_dir(base_output_dir)         generate_account_managed_policy_resource_files, 25 async def generate_account_managed_policy_resource_files(     account_resource_dir = get_account_managed_policy_resource_dir(                 get_templated_managed_policy_file_path( async def generate_aws_managed_policy_templates(     base_group_str_attribute,     get_existing_template_file_map,     group_dict_attribute,     group_int_or_str_attribute, MANAGED_POLICY_RESPONSE_DIR = pathlib.Path.home().joinpath(     path = await group_int_or_str_attribute(     managed_policy_template_params["policy_document"] = await group_dict_attribute(         ] = await group_int_or_str_attribute(         tags = await group_dict_attribute(         if isinstance(tags, dict):     aws_account: AWSAccount, ) -> dict:         managed_policy = ManagedPolicyTemplate(     resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)     response = dict(account_id=aws_account.account_id, managed_policies=[])         managed_policy.write()     iam_client = aws_account.get_boto3_client("iam")     account_managed_policies = await list_managed_policies(iam_client)     configs: list[Config], base_output_dir: str     aws_account_map = get_aws_account_map(configs)     existing_template_map = await get_existing_template_file_map(     generate_account_managed_policy_resource_files_semaphore = NoqSemaphore(             dict(         await generate_account_managed_policy_resource_files_semaphore.process(     await resource_file_upsert_semaphore.process(messages)     aws_account_map: dict[str, AWSAccount],     managed_policy_refs: list[dict],     existing_template_map: dict,             content_dict = json.loads(await f.read())             ] = normalize_boto3_resp(content_dict)     grouped_managed_policy_map = await base_group_str_attribute(             file_path=existing_template_map.get(         if tags := managed_policy_dict.get("tags"):         if description := managed_policy_dict.get("description"): def get_templated_managed_policy_file_path(managed_policy_dir: str, policy_name: str):     return account_resource_dir             aws_account_map, num_of_accounts, description_resources, "description"                 "resources": [{"resource_val": managed_policy_dict["path"]}],     log.info("Writing templated roles")     log.info(