diff --git a/CHANGELOG.md b/CHANGELOG.md index 52de46c4b2..da39dadbb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,22 @@ -## 2.0.0 (Unreleased) +## 2.1.0 (Unreleased) + +BUG FIXES: + +* `data.azuread_client_config` - populate the `tenant_id` and `client_id` attributes when authenticating via Azure CLI [GH-539] +* `azuread_service_principal` - fix a bug that prevented creation of service principals in some cases due to `owners` being applied incorrectly [GH-539] +* `azuread_user` - fix a validation bug for the `password` property [GH-543] + +IMPROVEMENTS: + +* `azuread_application` - allow `redirect_uris` with a scheme of `ms-appx-web` [GH-540] + +## 2.0.1 (August 26, 2021) + +BUG FIXES: + +* `azuread_application` - fix a bug where unknown IDs or values for roles/scopes were incorrectly flagged as duplicates ([#528](https://github.com/terraform-providers/terraform-provider-azuread/issues/528)) + +## 2.0.0 (August 26, 2021) NOTES: @@ -7,131 +25,134 @@ NOTES: FEATURES: -* **Provider:** Client Certificate authentication now supports specifying an inline certificate [GH-490] -* **New Data Source:** `azuread_application_published_app_ids` [GH-481] -* **New Resource:** `application_pre_authorized` [GH-472] +* **Provider:** Client Certificate authentication now supports specifying an inline certificate ([#490](https://github.com/terraform-providers/terraform-provider-azuread/issues/490)) +* **New Data Source:** `azuread_application_published_app_ids` ([#481](https://github.com/terraform-providers/terraform-provider-azuread/issues/481)) +* **New Resource:** `application_pre_authorized` ([#472](https://github.com/terraform-providers/terraform-provider-azuread/issues/472)) IMPROVEMENTS: -* `data.azuread_application` - the `api` block now supports the `accept_mapped_claims`, `known_client_applications` and `requested_access_token_version` attributes [GH-474] -* `data.azuread_application` - the `implicit_grant` block now supports the `id_token_issuance_enabled` attribute [GH-461] -* `data.azuread_application` - the `optional_claims` block now supports the `saml2_token` attribute [GH-461] -* `data.azuread_application` - export the `disabled_by_microsoft` attribute [GH-474] -* `data.azuread_application` - export the `device_only_auth_enabled` and `oauth2_post_response_required` attributes [GH-474] -* `data.azuread_application` - export the `logo_url`, `marketing_url`, `privacy_statement_url` and `terms_of_service_url` attributes [GH-474] -* `data.azuread_application` - export the `publisher_domain` attribute [GH-474] -* `data.azuread_application` - export the `public_client` block [GH-474] -* `data.azuread_application` - export the `single_page_application` block [GH-474] -* `data.azuread_application` - export the `app_role_ids` and `oauth2_permission_scope_ids` attributes [GH-474] -* `data.azuread_domains` - export the `admin_managed`, `root` and `supported_services` attributes for each domain [GH-461] -* `data.azuread_domains` - support the `admin_managed`, `only_root` and `supports_services` properties [GH-461] -* `data.azuread_group` - export the `assignable_to_role`, `behaviors`, `mail_nickname`, `theme` and `visibility` attributes [GH-476] -* `data.azuread_group` - export the `mail`, `preferred_language` and `proxy_addresses` attributes [GH-476] -* `data.azuread_group` - export the `onpremises_domain_name`, `onpremises_netbios_name`, `onpremises_sam_account_name`, `onpremises_security_identifier` and `onpremises_sync_enabled` attributes [GH-476] -* `data.azuread_service_principal` - export the `account_enabled`, `login_url` and `preferred_single_sign_on_mode` attributes [GH-481] -* `data.azuread_service_principal` - export the `alternative_names`, `description`, `notes` and `notification_email_addresses` attributes [GH-481] -* `data.azuread_service_principal` - export the `app_role_ids` and `oauth2_permission_scope_ids` attributes [GH-481] -* `data.azuread_service_principal` - export the `application_tenant_id`, `display_name`, `service_principal_names`, `sign_in_audience` and `type` attributes [GH-481] -* `data.azuread_service_principal` - export the `homepage_url`, `logout_url`, `redirect_uris` and `saml_metadata_url` attributes [GH-481] -* `data.azuread_user` - export the `age_group` and `consent_provided_for_minor` attributes [GH-476] -* `data.azuread_user` - export the `business_phones`, `employee_id`, `fax_number` and `preferred_language` attributes [GH-476] -* `data.azuread_user` - export the `mail`, `other_mails` and `show_in_address_list` attributes [GH-476] -* `data.azuread_user` - export the `creation_type`, `external_user_state`, `im_addresses` and `proxy_addresses` attributes [GH-476] -* `data.azuread_user` - export the `onpremises_distinguished_name`, `onpremises_domain_name`, `onpremises_security_identifier` and `onpremises_sync_enabled` attributes [GH-476] -* `azuread_application` - the `api` block now supports the `accept_mapped_claims`, `known_client_applications` and `requested_access_token_version` properties [GH-474] -* `azuread_application` - the `implicit_grant` block now supports the `id_token_issuance_enabled` property [GH-461] -* `azuread_application` - the `optional_claims` block now supports the `saml2_token` block [GH-461] -* `azuread_application` - the `sign_in_audience` property now supports the `AzureADandPersonalMicrosoftAccount` and `PersonalMicrosoftAccount` values [GH-461] -* `azuread_application` - export the `disabled_by_microsoft` attribute [GH-474] -* `azuread_application` - export the `publisher_domain` attribute [GH-474] -* `azuread_application` - support the `device_only_auth_enabled` and `oauth2_post_response_required` properties [GH-474] -* `azuread_application` - support the `logo_url`, `marketing_url`, `privacy_statement_url` and `terms_of_service_url` properties [GH-474] -* `azuread_application` - support for the `public_client` block [GH-474] -* `azuread_application` - support for the `single_page_application` block [GH-474] -* `azuread_application` - export the `app_role_ids` and `oauth2_permission_scope_ids` attributes [GH-474] -* `azuread_application_password` - support the `keepers` property [GH-481] -* `azuread_group` - support for creating mail-enabled groups [GH-461] -* `azuread_group` - support for creating Microsoft 365 groups [GH-461] -* `azuread_group` - support for updating groups without recreating them [GH-461] -* `azuread_group` - support the `assignable_to_role`, `behaviors`, `mail_nickname`, `theme` and `visibility` properties [GH-476] -* `azuread_group` - export the `mail`, `preferred_language` and `proxy_addresses` attributes [GH-476] -* `azuread_group` - export the `onpremises_domain_name`, `onpremises_netbios_name`, `onpremises_sam_account_name`, `onpremises_security_identifier` and `onpremises_sync_enabled` attributes [GH-476] -* `azuread_service_principal` - support the `account_enabled`, `login_url` and `preferred_single_sign_on_mode` properties [GH-481] -* `azuread_service_principal` - support the `alternative_names`, `description`, `notes` and `notification_email_addresses` properties [GH-481] -* `azuread_service_principal` - support the `use_existing` property [GH-481] -* `azuread_service_principal` - export the `app_role_ids` and `oauth2_permission_scope_ids` attributes [GH-481] -* `azuread_service_principal` - export the `application_tenant_id`, `display_name`, `service_principal_names`, `sign_in_audience` and `type` attributes [GH-481] -* `azuread_service_principal` - export the `homepage_url`, `logout_url`, `redirect_uris` and `saml_metadata_url` attributes [GH-481] -* `azuread_service_principal_password` - support the `keepers` property [GH-481] -* `azuread_user` - support the `age_group` and `consent_provided_for_minor` properties [GH-476] -* `azuread_user` - support the `business_phones`, `employee_id`, `fax_number` and `preferred_language` properties [GH-476] -* `azuread_user` - support the `mail`, `other_mails` and `show_in_address_list` properties [GH-476] -* `azuread_user` - export the `creation_type`, `external_user_state`, `im_addresses` and `proxy_addresses` attributes [GH-476] -* `azuread_user` - export the `onpremises_distinguished_name`, `onpremises_domain_name`, `onpremises_security_identifier` and `onpremises_sync_enabled` attributes [GH-476] +* `data.azuread_application` - the `api` block now supports the `accept_mapped_claims`, `known_client_applications` and `requested_access_token_version` attributes ([#474](https://github.com/terraform-providers/terraform-provider-azuread/issues/474)) +* `data.azuread_application` - the `implicit_grant` block now supports the `id_token_issuance_enabled` attribute ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `data.azuread_application` - the `optional_claims` block now supports the `saml2_token` attribute ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `data.azuread_application` - export the `disabled_by_microsoft` attribute ([#474](https://github.com/terraform-providers/terraform-provider-azuread/issues/474)) +* `data.azuread_application` - export the `device_only_auth_enabled` and `oauth2_post_response_required` attributes ([#474](https://github.com/terraform-providers/terraform-provider-azuread/issues/474)) +* `data.azuread_application` - export the `logo_url`, `marketing_url`, `privacy_statement_url` and `terms_of_service_url` attributes ([#474](https://github.com/terraform-providers/terraform-provider-azuread/issues/474)) +* `data.azuread_application` - export the `publisher_domain` attribute ([#474](https://github.com/terraform-providers/terraform-provider-azuread/issues/474)) +* `data.azuread_application` - export the `public_client` block ([#474](https://github.com/terraform-providers/terraform-provider-azuread/issues/474)) +* `data.azuread_application` - export the `single_page_application` block ([#474](https://github.com/terraform-providers/terraform-provider-azuread/issues/474)) +* `data.azuread_application` - export the `app_role_ids` and `oauth2_permission_scope_ids` attributes ([#474](https://github.com/terraform-providers/terraform-provider-azuread/issues/474)) +* `data.azuread_domains` - export the `admin_managed`, `root` and `supported_services` attributes for each domain ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `data.azuread_domains` - support the `admin_managed`, `only_root` and `supports_services` properties ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `data.azuread_group` - export the `assignable_to_role`, `behaviors`, `mail_nickname`, `theme` and `visibility` attributes ([#476](https://github.com/terraform-providers/terraform-provider-azuread/issues/476)) +* `data.azuread_group` - export the `mail`, `preferred_language` and `proxy_addresses` attributes ([#476](https://github.com/terraform-providers/terraform-provider-azuread/issues/476)) +* `data.azuread_group` - export the `onpremises_domain_name`, `onpremises_netbios_name`, `onpremises_sam_account_name`, `onpremises_security_identifier` and `onpremises_sync_enabled` attributes ([#476](https://github.com/terraform-providers/terraform-provider-azuread/issues/476)) +* `data.azuread_service_principal` - export the `account_enabled`, `login_url` and `preferred_single_sign_on_mode` attributes ([#481](https://github.com/terraform-providers/terraform-provider-azuread/issues/481)) +* `data.azuread_service_principal` - export the `alternative_names`, `description`, `notes` and `notification_email_addresses` attributes ([#481](https://github.com/terraform-providers/terraform-provider-azuread/issues/481)) +* `data.azuread_service_principal` - export the `app_role_ids` and `oauth2_permission_scope_ids` attributes ([#481](https://github.com/terraform-providers/terraform-provider-azuread/issues/481)) +* `data.azuread_service_principal` - export the `application_tenant_id`, `display_name`, `service_principal_names`, `sign_in_audience` and `type` attributes ([#481](https://github.com/terraform-providers/terraform-provider-azuread/issues/481)) +* `data.azuread_service_principal` - export the `homepage_url`, `logout_url`, `redirect_uris` and `saml_metadata_url` attributes ([#481](https://github.com/terraform-providers/terraform-provider-azuread/issues/481)) +* `data.azuread_user` - export the `age_group` and `consent_provided_for_minor` attributes ([#476](https://github.com/terraform-providers/terraform-provider-azuread/issues/476)) +* `data.azuread_user` - export the `business_phones`, `employee_id`, `fax_number` and `preferred_language` attributes ([#476](https://github.com/terraform-providers/terraform-provider-azuread/issues/476)) +* `data.azuread_user` - export the `mail`, `other_mails` and `show_in_address_list` attributes ([#476](https://github.com/terraform-providers/terraform-provider-azuread/issues/476)) +* `data.azuread_user` - export the `creation_type`, `external_user_state`, `im_addresses` and `proxy_addresses` attributes ([#476](https://github.com/terraform-providers/terraform-provider-azuread/issues/476)) +* `data.azuread_user` - export the `onpremises_distinguished_name`, `onpremises_domain_name`, `onpremises_security_identifier` and `onpremises_sync_enabled` attributes ([#476](https://github.com/terraform-providers/terraform-provider-azuread/issues/476)) +* `azuread_application` - the `api` block now supports the `accept_mapped_claims`, `known_client_applications` and `requested_access_token_version` properties ([#474](https://github.com/terraform-providers/terraform-provider-azuread/issues/474)) +* `azuread_application` - the `implicit_grant` block now supports the `id_token_issuance_enabled` property ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - the `optional_claims` block now supports the `saml2_token` block ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - the `sign_in_audience` property now supports the `AzureADandPersonalMicrosoftAccount` and `PersonalMicrosoftAccount` values ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - export the `disabled_by_microsoft` attribute ([#474](https://github.com/terraform-providers/terraform-provider-azuread/issues/474)) +* `azuread_application` - export the `publisher_domain` attribute ([#474](https://github.com/terraform-providers/terraform-provider-azuread/issues/474)) +* `azuread_application` - support the `device_only_auth_enabled` and `oauth2_post_response_required` properties ([#474](https://github.com/terraform-providers/terraform-provider-azuread/issues/474)) +* `azuread_application` - support the `logo_url`, `marketing_url`, `privacy_statement_url` and `terms_of_service_url` properties ([#474](https://github.com/terraform-providers/terraform-provider-azuread/issues/474)) +* `azuread_application` - support for the `public_client` block ([#474](https://github.com/terraform-providers/terraform-provider-azuread/issues/474)) +* `azuread_application` - support for the `single_page_application` block ([#474](https://github.com/terraform-providers/terraform-provider-azuread/issues/474)) +* `azuread_application` - export the `app_role_ids` and `oauth2_permission_scope_ids` attributes ([#474](https://github.com/terraform-providers/terraform-provider-azuread/issues/474)) +* `azuread_application_password` - support the `keepers` property ([#481](https://github.com/terraform-providers/terraform-provider-azuread/issues/481)) +* `azuread_group` - support for creating mail-enabled groups ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_group` - support for creating Microsoft 365 groups ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_group` - support for updating groups without recreating them ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_group` - support the `assignable_to_role`, `behaviors`, `mail_nickname`, `theme` and `visibility` properties ([#476](https://github.com/terraform-providers/terraform-provider-azuread/issues/476)) +* `azuread_group` - export the `mail`, `preferred_language` and `proxy_addresses` attributes ([#476](https://github.com/terraform-providers/terraform-provider-azuread/issues/476)) +* `azuread_group` - export the `onpremises_domain_name`, `onpremises_netbios_name`, `onpremises_sam_account_name`, `onpremises_security_identifier` and `onpremises_sync_enabled` attributes ([#476](https://github.com/terraform-providers/terraform-provider-azuread/issues/476)) +* `azuread_service_principal` - support the `account_enabled`, `login_url` and `preferred_single_sign_on_mode` properties ([#481](https://github.com/terraform-providers/terraform-provider-azuread/issues/481)) +* `azuread_service_principal` - support the `alternative_names`, `description`, `notes` and `notification_email_addresses` properties ([#481](https://github.com/terraform-providers/terraform-provider-azuread/issues/481)) +* `azuread_service_principal` - support the `owners` property ([#519](https://github.com/terraform-providers/terraform-provider-azuread/issues/519)) +* `azuread_service_principal` - support the `use_existing` property ([#481](https://github.com/terraform-providers/terraform-provider-azuread/issues/481)) +* `azuread_service_principal` - export the `app_role_ids` and `oauth2_permission_scope_ids` attributes ([#481](https://github.com/terraform-providers/terraform-provider-azuread/issues/481)) +* `azuread_service_principal` - export the `application_tenant_id`, `display_name`, `service_principal_names`, `sign_in_audience` and `type` attributes ([#481](https://github.com/terraform-providers/terraform-provider-azuread/issues/481)) +* `azuread_service_principal` - export the `homepage_url`, `logout_url`, `redirect_uris` and `saml_metadata_url` attributes ([#481](https://github.com/terraform-providers/terraform-provider-azuread/issues/481)) +* `azuread_service_principal_password` - support the `keepers` property ([#481](https://github.com/terraform-providers/terraform-provider-azuread/issues/481)) +* `azuread_user` - support the `age_group` and `consent_provided_for_minor` properties ([#476](https://github.com/terraform-providers/terraform-provider-azuread/issues/476)) +* `azuread_user` - support the `business_phones`, `employee_id`, `fax_number` and `preferred_language` properties ([#476](https://github.com/terraform-providers/terraform-provider-azuread/issues/476)) +* `azuread_user` - support the `mail`, `other_mails` and `show_in_address_list` properties ([#476](https://github.com/terraform-providers/terraform-provider-azuread/issues/476)) +* `azuread_user` - export the `creation_type`, `external_user_state`, `im_addresses` and `proxy_addresses` attributes ([#476](https://github.com/terraform-providers/terraform-provider-azuread/issues/476)) +* `azuread_user` - export the `onpremises_distinguished_name`, `onpremises_domain_name`, `onpremises_security_identifier` and `onpremises_sync_enabled` attributes ([#476](https://github.com/terraform-providers/terraform-provider-azuread/issues/476)) BUG FIXES: -* `azuread_application` - resolved an issue where `identifier_uris` could be reordered and cause a persistent diff [GH-461] -* `azuread_application` - the `identifier_uris` property can now be set for all applications regardless of target platform [GH-461] -* `azuread_application` - fixed a bug where app roles could be duplicated or left in a disabled state [GH-461] -* `azuread_application` - fixed a bug where app roles could not be removed from an application [GH-461] -* `azuread_application` - fixed a bug where the `enabled` property of app roles could be ignored [GH-461] -* `azuread_application` - fixed a bug where the `id` property of app roles could be undesirably changed [GH-461] -* `azuread_application` - resolved an issue where the default scope could not be removed from an application [GH-461] -* `azuread_application` - resolved an issue where multiple `group_membership_claims` could not be specified [GH-461] -* `azuread_application_password` - the `display_name` / `description` properties are no longer stored using the `customKeyIdentifier` API field, lifting the 32 byte limit [GH-461] -* `azuread_user` - resolved an issue where importing users would inadvertently reset their password [GH-461] +* `azuread_application` - resolved an issue where `identifier_uris` could be reordered and cause a persistent diff ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - the `identifier_uris` property can now be set for all applications regardless of target platform ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - fixed a bug where app roles could be duplicated or left in a disabled state ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - fixed a bug where app roles could not be removed from an application ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - fixed a bug where the `enabled` property of app roles could be ignored ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - fixed a bug where the `id` property of app roles could be undesirably changed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - resolved an issue where the default scope could not be removed from an application ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - resolved an issue where multiple `group_membership_claims` could not be specified ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application_password` - the `display_name` / `description` properties are no longer stored using the `customKeyIdentifier` API field, lifting the 32 byte limit ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_group` - fix a bug where `owners` or `members` would sometimes not be updated ([#519](https://github.com/terraform-providers/terraform-provider-azuread/issues/519)) +* `azuread_group` - fix some ownership-related bugs where groups could sometimes not be created or updated ([#519](https://github.com/terraform-providers/terraform-provider-azuread/issues/519)) +* `azuread_user` - resolved an issue where importing users would inadvertently reset their password ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) BREAKING CHANGES: -* `data.azuread_domains` - the `is_` prefix has been dropped from all exported attributes [GH-461] -* `data.azuread_application` - the `display_name` property is now matched case-insensitively which mirrors the behaviour of Azure Active Directory [GH-492] -* `data.azuread_application` - the deprecated property `name` has been removed [GH-461] -* `data.azuread_application` - the deprecated attribute `available_to_other_tenants` has been removed [GH-461] -* `data.azuread_application` - the `group_membership_claims` attribute has changed from a string to a list of strings [GH-461] -* `data.azuread_application` - the deprecated attribute `homepage` has been removed [GH-461] -* `data.azuread_application` - the deprecated attribute `logout_url` has been removed [GH-461] -* `data.azuread_application` - the deprecated attribute `oauth2_allow_implicit_flow` has been removed [GH-461] -* `data.azuread_application` - the deprecated attribute `oauth2_permissions` has been removed [GH-461] -* `data.azuread_application` - the `public_client` attribute is now a block containing public client settings [GH-461] -* `data.azuread_application` - the deprecated attribute `reply_urls` has been removed [GH-461] -* `data.azuread_application` - the deprecated attribute `type` has been removed [GH-461] -* `data.azuread_group` - the deprecated property `name` has been removed [GH-461] -* `data.azuread_groups` - the deprecated property `names` has been removed [GH-461] -* `data.azuread_service_principal` - the deprecated attribute `oauth2_permissions` has been removed [GH-461] -* `data.azuread_user` - the deprecated attribute `immutable_id` has been removed [GH-461] -* `data.azuread_user` - the deprecated attribute `physical_delivery_office_name` has been removed [GH-461] -* `data.azuread_user` - the deprecated attribute `mobile` has been removed [GH-461] -* `data.azuread_users` - the deprecated attribute `immutable_id` in the `users` block has been removed [GH-461] -* `azuread_application` - the deprecated property `name` has been removed [GH-461] -* `azuread_application` - the `api` block is no longer Computed, omitting this block will cause it to be removed from your configuration [GH-461] -* `azuread_application` - the `app_role` block is no longer Computed, omitting this block will cause it to be removed from your configuration [GH-461] -* `azuread_application` - the `id` property in the `app_role` block is now Required [GH-461] -* `azuread_application` - the deprecated property `available_to_other_tenants` has been removed [GH-461] -* `azuread_application` - the `fallback_public_client_enabled` property is no longer Computed, omitting this property will cause the default value to be applied [GH-461] -* `azuread_application` - the `group_membership_claims` property has changed from a string to a set of strings [GH-461] -* `azuread_application` - the deprecated property `homepage` has been removed [GH-461] -* `azuread_application` - the `identifier_uris` property is no longer Computed, omitting this property will cause it to be removed from your configuration [GH-461] -* `azuread_application` - the `identifier_uris` property has changed from a List to a Set to resolve an API ordering issue [GH-481] -* `azuread_application` - the deprecated property `logout_url` has been removed [GH-461] -* `azuread_application` - the deprecated property `oauth2_allow_implicit_flow` has been removed [GH-461] -* `azuread_application` - the `oauth2_permission_scope` block is no longer Computed, omitting this block will cause it to be removed from your configuration [GH-461] -* `azuread_application` - the deprecated block `oauth2_permissions` has been removed [GH-461] -* `azuread_application` - the `owners` property is no longer Computed, omitting this property will cause it to be removed from your configuration [GH-461] -* `azuread_application` - the `public_client` property is now a block containing public client settings [GH-461] -* `azuread_application` - the deprecated property `reply_urls` has been removed [GH-461] -* `azuread_application` - the `sign_in_audience` property is no longer Computed, omitting this property will cause the default value to be applied [GH-461] -* `azuread_application` - the deprecated property `type` has been removed [GH-461] -* `azuread_application` - the `web` block is no longer Computed, omitting this block will cause it to be removed from your configuration [GH-461] -* `azuread_application_password` - the `key_id` and `value` properties are now Computed, due to API changes it is no longer possible to specify these values [GH-461] -* `azuread_group` - the deprecated property `name` has been removed [GH-461] -* `azuread_group` - at least one of the `mail_enabled` or `security_enabled` properties are now Required [GH-461] -* `azuread_service_principal` - the deprecated attribute `oauth2_permissions` has been removed [GH-461] -* `azuread_service_principal_password` - the `key_id` and `value` properties are now Computed, due to API changes it is no longer possible to specify these values [GH-461] -* `azuread_service_principal_password` - the `start_date` and `end_date` properties are now Computed, due to an API issue it is no longer possible to specify these values [GH-461] -* `azuread_user` - the deprecated property `immutable_id` has been removed [GH-461] -* `azuread_user` - the deprecated property `physical_delivery_office_name` has been removed [GH-461] -* `azuread_user` - the deprecated property `mobile` has been removed [GH-461] +* `data.azuread_domains` - the `is_` prefix has been dropped from all exported attributes ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `data.azuread_application` - the `display_name` property is now matched case-insensitively which mirrors the behaviour of Azure Active Directory ([#492](https://github.com/terraform-providers/terraform-provider-azuread/issues/492)) +* `data.azuread_application` - the deprecated property `name` has been removed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `data.azuread_application` - the deprecated attribute `available_to_other_tenants` has been removed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `data.azuread_application` - the `group_membership_claims` attribute has changed from a string to a list of strings ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `data.azuread_application` - the deprecated attribute `homepage` has been removed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `data.azuread_application` - the deprecated attribute `logout_url` has been removed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `data.azuread_application` - the deprecated attribute `oauth2_allow_implicit_flow` has been removed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `data.azuread_application` - the deprecated attribute `oauth2_permissions` has been removed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `data.azuread_application` - the `public_client` attribute is now a block containing public client settings ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `data.azuread_application` - the deprecated attribute `reply_urls` has been removed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `data.azuread_application` - the deprecated attribute `type` has been removed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `data.azuread_group` - the deprecated property `name` has been removed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `data.azuread_groups` - the deprecated property `names` has been removed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `data.azuread_service_principal` - the deprecated attribute `oauth2_permissions` has been removed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `data.azuread_user` - the deprecated attribute `immutable_id` has been removed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `data.azuread_user` - the deprecated attribute `physical_delivery_office_name` has been removed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `data.azuread_user` - the deprecated attribute `mobile` has been removed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `data.azuread_users` - the deprecated attribute `immutable_id` in the `users` block has been removed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - the deprecated property `name` has been removed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - the `api` block is no longer Computed, omitting this block will cause it to be removed from your configuration ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - the `app_role` block is no longer Computed, omitting this block will cause it to be removed from your configuration ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - the `id` property in the `app_role` block is now Required ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - the deprecated property `available_to_other_tenants` has been removed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - the `fallback_public_client_enabled` property is no longer Computed, omitting this property will cause the default value to be applied ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - the `group_membership_claims` property has changed from a string to a set of strings ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - the deprecated property `homepage` has been removed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - the `identifier_uris` property is no longer Computed, omitting this property will cause it to be removed from your configuration ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - the `identifier_uris` property has changed from a List to a Set to resolve an API ordering issue ([#481](https://github.com/terraform-providers/terraform-provider-azuread/issues/481)) +* `azuread_application` - the deprecated property `logout_url` has been removed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - the deprecated property `oauth2_allow_implicit_flow` has been removed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - the `oauth2_permission_scope` block is no longer Computed, omitting this block will cause it to be removed from your configuration ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - the deprecated block `oauth2_permissions` has been removed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - the `owners` property is no longer Computed, omitting this property will cause it to be removed from your configuration ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - the `public_client` property is now a block containing public client settings ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - the deprecated property `reply_urls` has been removed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - the `sign_in_audience` property is no longer Computed, omitting this property will cause the default value to be applied ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - the deprecated property `type` has been removed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application` - the `web` block is no longer Computed, omitting this block will cause it to be removed from your configuration ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_application_password` - the `key_id` and `value` properties are now Computed, due to API changes it is no longer possible to specify these values ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_group` - the deprecated property `name` has been removed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_group` - at least one of the `mail_enabled` or `security_enabled` properties are now Required ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_service_principal` - the deprecated attribute `oauth2_permissions` has been removed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_service_principal_password` - the `key_id` and `value` properties are now Computed, due to API changes it is no longer possible to specify these values ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_service_principal_password` - the `start_date` and `end_date` properties are now Computed, due to an API issue it is no longer possible to specify these values ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_user` - the deprecated property `immutable_id` has been removed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_user` - the deprecated property `physical_delivery_office_name` has been removed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) +* `azuread_user` - the deprecated property `mobile` has been removed ([#461](https://github.com/terraform-providers/terraform-provider-azuread/issues/461)) ## 1.6.0 (June 24, 2021) diff --git a/README.md b/README.md index 14b8f74992..7dbba0ba1c 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ - [Terraform Website](https://www.terraform.io) - [AzureAD Provider Documentation](https://terraform.io/docs/providers/azuread/) - [AzureAD Provider Usage Examples](https://github.com/hashicorp/terraform-provider-azuread/tree/main/examples) +- [Learn Tutorial](https://learn.hashicorp.com/tutorials/terraform/azure-ad) - [Slack Workspace for Contributors](https://terraform-azure.slack.com) ([Request Invite](https://join.slack.com/t/terraform-azure/shared_invite/enQtNDMzNjQ5NzcxMDc3LWNiY2ZhNThhNDgzNmY0MTM0N2MwZjE4ZGU0MjcxYjUyMzRmN2E5NjZhZmQ0ZTA1OTExMGNjYzA4ZDkwZDYxNDE)) diff --git a/docs/data-sources/application.md b/docs/data-sources/application.md index 740dbd9de5..448bb35ea9 100644 --- a/docs/data-sources/application.md +++ b/docs/data-sources/application.md @@ -6,6 +6,14 @@ subcategory: "Applications" Use this data source to access information about an existing Application within Azure Active Directory. +## API Permissions + +The following API permissions are required in order to use this data source. + +When authenticated with a service principal, this data source requires one of the following application roles: `Application.Read.All` or `Directory.Read.All` + +When authenticated with a user principal, this data source does not require any additional roles. + ## Example Usage ```terraform @@ -13,18 +21,20 @@ data "azuread_application" "example" { display_name = "My First AzureAD Application" } -output "azure_ad_object_id" { +output "application_object_id" { value = data.azuread_application.example.id } ``` ## Argument Reference +The following arguments are supported: + * `application_id` - (Optional) Specifies the Application ID (also called Client ID). * `display_name` - (Optional) Specifies the display name of the application. * `object_id` - (Optional) Specifies the Object ID of the application. -~> **NOTE:** One of `object_id`, `application_id` or `display_name` must be specified. +~> One of `object_id`, `application_id` or `display_name` must be specified. ## Attributes Reference diff --git a/docs/data-sources/client_config.md b/docs/data-sources/client_config.md index 7d31b761c4..1502080991 100644 --- a/docs/data-sources/client_config.md +++ b/docs/data-sources/client_config.md @@ -6,23 +6,28 @@ subcategory: "Base" Use this data source to access the configuration of the AzureAD provider. +## API Permissions + +No additional roles are required to use this data source. + ## Example Usage ```hcl -data "azuread_client_config" "current" { -} +data "azuread_client_config" "current" {} -output "account_id" { - value = data.azuread_client_config.current.client_id +output "object_id" { + value = data.azuread_client_config.current.object_id } ``` ## Argument Reference -There are no arguments available for this data source. +This data source does not have any arguments. ## Attributes Reference +The following attributes are exported: + * `client_id` - The client ID (application ID) linked to the authenticated principal, or the application used for delegated authentication. * `object_id` - The object ID of the authenticated principal. * `tenant_id` - The tenant ID of the authenticated principal. diff --git a/docs/data-sources/domains.md b/docs/data-sources/domains.md index 34ca6dfd09..9b28161414 100644 --- a/docs/data-sources/domains.md +++ b/docs/data-sources/domains.md @@ -6,20 +6,28 @@ subcategory: "Domains" Use this data source to access information about existing Domains within Azure Active Directory. --> **NOTE:** If you're authenticating using a Service Principal then it must have permissions to `Directory.Read.All` within the `Windows Azure Active Directory` API. +## API Permissions + +The following API permissions are required in order to use this data source. + +When authenticated with a service principal, this data source requires one of the following application roles: `Domain.Read.All` or `Directory.Read.All` + +When authenticated with a user principal, this data source does not require any additional roles. ## Example Usage ```terraform data "azuread_domains" "aad_domains" {} -output "domains" { - value = data.azuread_domains.aad_domains.domains +output "domain_names" { + value = data.azuread_domains.aad_domains.domains.*.domain_name } ``` ## Argument Reference +The following arguments are supported: + * `admin_managed` - (Optional) Set to `true` to only return domains whose DNS is managed by Microsoft 365. Defaults to `false`. * `include_unverified` - (Optional) Set to `true` if unverified Azure AD domains should be included. Defaults to `false`. * `only_default` - (Optional) Set to `true` to only return the default domain. @@ -27,12 +35,16 @@ output "domains" { * `only_root` - (Optional) Set to `true` to only return verified root domains. Excludes subdomains and unverified domains. * `supports_services` - (Optional) A list of supported services that must be supported by a domain. Possible values include `Email`, `Sharepoint`, `EmailInternalRelayOnly`, `OfficeCommunicationsOnline`, `SharePointDefaultDomain`, `FullRedelegation`, `SharePointPublic`, `OrgIdAuthentication`, `Yammer` and `Intune`. -~> **NOTE:** If `include_unverified` is set to `true` you cannot specify `only_default` or `only_initial`. Additionally, you cannot combine `only_default` with `only_initial`. +-> **Note on filters** If `include_unverified` is set to `true`, you cannot specify `only_default` or `only_initial`. Additionally, you cannot combine `only_default` with `only_initial`. ## Attributes Reference +In addition to all arguments above, the following attributes are exported: + * `domains` - A list of tenant domains. Each `domain` object provides the attributes documented below. +--- + `domain` object exports the following: * `admin_managed` - Whether the DNS for the domain is managed by Microsoft 365. @@ -42,4 +54,4 @@ output "domains" { * `initial` - Whether this is the initial domain created by Azure Active Directory. * `root` - Whether the domain is a verified root domain (not a subdomain). * `verified` - Whether the domain has completed domain ownership verification. -* `supported_services` - A list of capabilities / services supported by the domain. Possible values include `Email`, `Sharepoint`, `EmailInternalRelayOnly`, `OfficeCommunicationsOnline`, `SharePointDefaultDomain`, `FullRedelegation`, `SharePointPublic`, `OrgIdAuthentication`, `Yammer` and `Intune`. \ No newline at end of file +* `supported_services` - A list of capabilities / services supported by the domain. Possible values include `Email`, `Sharepoint`, `EmailInternalRelayOnly`, `OfficeCommunicationsOnline`, `SharePointDefaultDomain`, `FullRedelegation`, `SharePointPublic`, `OrgIdAuthentication`, `Yammer` and `Intune`. diff --git a/docs/data-sources/group.md b/docs/data-sources/group.md index caebbb57ac..9f452a7c89 100644 --- a/docs/data-sources/group.md +++ b/docs/data-sources/group.md @@ -6,6 +6,14 @@ subcategory: "Groups" Gets information about an Azure Active Directory group. +## API Permissions + +The following API permissions are required in order to use this data source. + +When authenticated with a service principal, this data source requires one of the following application roles: `Group.Read.All` or `Directory.Read.All` + +When authenticated with a user principal, this data source does not require any additional roles. + ## Example Usage (by Group Display Name) ```terraform @@ -24,7 +32,7 @@ The following arguments are supported: * `object_id` - (Optional) Specifies the object ID of the group. * `security_enabled` - (Optional) Whether the group is a security group. -~> **NOTE:** One of `display_name` or `object_id` must be specified. +~> One of `display_name` or `object_id` must be specified. ## Attributes Reference diff --git a/docs/data-sources/groups.md b/docs/data-sources/groups.md index 7c1db1b3f4..511096d8e5 100644 --- a/docs/data-sources/groups.md +++ b/docs/data-sources/groups.md @@ -6,6 +6,14 @@ subcategory: "Groups" Gets Object IDs or Display Names for multiple Azure Active Directory groups. +## API Permissions + +The following API permissions are required in order to use this data source. + +When authenticated with a service principal, this data source requires one of the following application roles: `Group.Read.All` or `Directory.Read.All` + +When authenticated with a user principal, this data source does not require any additional roles. + ## Example Usage ```terraform @@ -28,7 +36,7 @@ The following arguments are supported: * `object_ids` - (Optional) The object IDs of the groups. * `show_all` - (Optional) A flag to denote if all groups should be fetched and returned. -~> **NOTE:** Either `display_names`, `object_ids` or `show_all` should be specified. Either of the first two _may_ be specified as an empty list, in which case no results will be returned. +~> One of `display_names`, `object_ids` or `show_all` should be specified. Either of the first two _may_ be specified as an empty list, in which case no results will be returned. ## Attributes Reference diff --git a/docs/data-sources/service_principal.md b/docs/data-sources/service_principal.md index 4afef5fefa..dc1287cb2f 100644 --- a/docs/data-sources/service_principal.md +++ b/docs/data-sources/service_principal.md @@ -6,9 +6,17 @@ subcategory: "Service Principals" Gets information about an existing service principal associated with an application within Azure Active Directory. +## API Permissions + +The following API permissions are required in order to use this data source. + +When authenticated with a service principal, this data source requires one of the following application roles: `Application.Read.All` or `Directory.Read.All` + +When authenticated with a user principal, this data source does not require any additional roles. + ## Example Usage -**Look up by application display name** +*Look up by application display name* ```terraform data "azuread_service_principal" "example" { @@ -16,7 +24,7 @@ data "azuread_service_principal" "example" { } ``` -**Look up by application ID** +*Look up by application ID (client ID)* ```terraform data "azuread_service_principal" "example" { @@ -24,7 +32,7 @@ data "azuread_service_principal" "example" { } ``` -**Look up by service principal object ID** +*Look up by service principal object ID* ```terraform data "azuread_service_principal" "example" { @@ -40,7 +48,7 @@ The following arguments are supported: * `display_name` - (Optional) The display name of the application associated with this service principal. * `object_id` - (Optional) The object ID of the service principal. -~> **NOTE:** At least one of `application_id`, `display_name` or `object_id` must be specified. +~> One of `application_id`, `display_name` or `object_id` must be specified. ## Attributes Reference @@ -48,19 +56,22 @@ The following attributes are exported: * `account_enabled` - - Whether or not the service principal account is enabled. * `alternative_names` - A list of alternative names, used to retrieve service principals by subscription, identify resource group and full resource ids for managed identities. +* `application_id` - The application ID (client ID) of the application associated with this service principal. * `app_role_assignment_required` - Whether this service principal requires an app role assignment to a user or group before Azure AD will issue a user or access token to the application. * `app_role_ids` - A mapping of app role values to app role IDs, as published by the associated application, intended to be useful when referencing app roles in other resources in your configuration. * `app_roles` - A list of app roles published by the associated application, as documented below. For more information [official documentation](https://docs.microsoft.com/en-us/azure/architecture/multitenant-identity/app-roles). * `application_tenant_id` - The tenant ID where the associated application is registered. * `description` - A description of the service principal provided for internal end-users. +* `display_name` - The display name of the application associated with this service principal. * `homepage_url` - Home page or landing page of the associated application. * `login_url` - The URL where the service provider redirects the user to Azure AD to authenticate. Azure AD uses the URL to launch the application from Microsoft 365 or the Azure AD My Apps. * `logout_url` - The URL that will be used by Microsoft's authorization service to logout an user using OpenId Connect front-channel, back-channel or SAML logout protocols, taken from the associated application. * `notes` - A free text field to capture information about the service principal, typically used for operational purposes. * `notification_email_addresses` - A list of email addresses where Azure AD sends a notification when the active certificate is near the expiration date. This is only for the certificates used to sign the SAML token issued for Azure AD Gallery applications. -* `object_id` - The object ID for the service principal. +* `object_id` - The object ID of the service principal. * `oauth2_permission_scope_ids` - A mapping of OAuth2.0 permission scope values to scope IDs, as exposed by the associated application, intended to be useful when referencing permission scopes in other resources in your configuration. * `oauth2_permission_scopes` - A collection of OAuth 2.0 delegated permissions exposed by the associated application. Each permission is covered by an `oauth2_permission_scopes` block as documented below. +* `preferred_single_sign_on_mode` - The single sign-on mode configured for this application. Azure AD uses the preferred single sign-on mode to launch the application from Microsoft 365 or the Azure AD My Apps. * `redirect_uris` - A list of URLs where user tokens are sent for sign-in with the associated application, or the redirect URIs where OAuth 2.0 authorization codes and access tokens are sent for the associated application. * `saml_metadata_url` - The URL where the service exposes SAML metadata for federation. * `service_principal_names` - A list of identifier URI(s), copied over from the associated application. diff --git a/docs/data-sources/user.md b/docs/data-sources/user.md index 76e3b2598b..dc63ea212b 100644 --- a/docs/data-sources/user.md +++ b/docs/data-sources/user.md @@ -6,6 +6,14 @@ subcategory: "Users" Gets information about an Azure Active Directory user. +## API Permissions + +The following API permissions are required in order to use this data source. + +When authenticated with a service principal, this data source requires one of the following application roles: `User.Read.All` or `Directory.Read.All` + +When authenticated with a user principal, this data source does not require any additional roles. + ## Example Usage ```terraform @@ -22,7 +30,7 @@ The following arguments are supported: * `object_id` - (Optional) The object ID of the user. * `user_principal_name` - (Optional) The user principal name (UPN) of the user. -~> **NOTE:** One of `user_principal_name`, `object_id` or `mail_nickname` must be specified. +~> One of `user_principal_name`, `object_id` or `mail_nickname` must be specified. ## Attributes Reference @@ -44,9 +52,10 @@ The following attributes are exported: * `given_name` - The given name (first name) of the user. * `im_addresses` - A list of instant message voice over IP (VOIP) session initiation protocol (SIP) addresses for the user. * `job_title` - The user’s job title. -* `mail_nickname` - The email alias of the user. * `mail` - The SMTP address for the user. +* `mail_nickname` - The email alias of the user. * `mobile_phone` - The primary cellular telephone number for the user. +* `object_id` - The object ID of the user. * `office_location` - The office location in the user's place of business. * `onpremises_distinguished_name` - The on-premises distinguished name (DN) of the user, synchronised from the on-premises directory when Azure AD Connect is used. * `onpremises_domain_name` - The on-premises FQDN, also called dnsDomainName, synchronised from the on-premises directory when Azure AD Connect is used. diff --git a/docs/data-sources/users.md b/docs/data-sources/users.md index 19de021919..b9ddab5915 100644 --- a/docs/data-sources/users.md +++ b/docs/data-sources/users.md @@ -6,6 +6,14 @@ subcategory: "Users" Gets object IDs or user principal names for multiple Azure Active Directory users. +## API Permissions + +The following API permissions are required in order to use this data source. + +When authenticated with a service principal, this data source requires one of the following application roles: `User.Read.All` or `Directory.Read.All` + +When authenticated with a user principal, this data source does not require any additional roles. + ## Example Usage ```terraform @@ -23,7 +31,7 @@ The following arguments are supported: * `object_ids` - (Optional) The object IDs of the users. * `user_principal_names` - (Optional) The user principal names (UPNs) of the users. -~> **NOTE:** One of `user_principal_names`, `object_ids` or `mail_nicknames` must be specified. These _may_ be specified as an empty list, in which case no results will be returned. +~> One of `user_principal_names`, `object_ids` or `mail_nicknames` must be specified. These _may_ be specified as an empty list, in which case no results will be returned. ## Attributes Reference diff --git a/docs/guides/azure_cli.md b/docs/guides/azure_cli.md index 012a048729..1c1501b5de 100644 --- a/docs/guides/azure_cli.md +++ b/docs/guides/azure_cli.md @@ -25,17 +25,11 @@ We recommend using either a Service Principal or Managed Identity when running T ## Logging into the Azure CLI -~> **Using other clouds** If you're using the **China**, **German** or **Government** Azure Clouds - you'll need to first configure the Azure CLI to work with that Cloud, so that the correct authentication service is used. You can do this by running: - -```shell -$ az cloud set --name AzureChinaCloud|AzureGermanCloud|AzureUSGovernment -``` - ---- +-> **Using other clouds** If you're using the **China**, **German** or **Government** Azure Clouds - you'll need to first configure the Azure CLI to work with that Cloud, so that the correct authentication service is used. You can do this by running:

`$ az cloud set --name AzureChinaCloud|AzureGermanCloud|AzureUSGovernment` Firstly, login to the Azure CLI using: -```shell +```shell-session $ az login --allow-no-subscriptions ``` @@ -43,53 +37,48 @@ The `--allow-no-subscriptions` argument enables access to tenants that have no l Once logged in - it's possible to list the Subscriptions and Tenants associated with the account via: -```shell -$ az account list +```shell-session +$ az account list -o table --all --query "[].{TenantID: tenantId, Subscription: name, Default: isDefault}" ``` The output (similar to below) will display one or more Tenants and/or Subscriptions. -```json -[ - { - "cloudName": "AzureCloud", - "id": "00000000-0000-0000-0000-000000000000", - "isDefault": true, - "name": "PAYG Subscription", - "state": "Enabled", - "tenantId": "00000000-0000-0000-0000-000000000000", - "user": { - "name": "user@example.com", - "type": "user" - } - } -] +``` +TenantID Subscription Default +------------------------------------ ----------------------------------- --------- +00000000-0000-1111-1111-111111111111 N/A(tenant level account) False +00000000-0000-2222-2222-222222222222 N/A(tenant level account) False +00000000-0000-1111-1111-111111111111 My Subscription True +00000000-0000-1111-1111-111111111111 My Other Subscription False ``` -Each entry shown is referred to as an `Azure CLI account`, which represents either a subscription with its linked tenant, or a tenant without any accessible subscriptions. The provider will select the tenant ID from your default Azure CLI account. If you have more than one tenant listed in the output of `az account list`, for example if you are a guest user in other tenants, you can specify the tenant to use. +Each entry shown is referred to as an `Azure CLI account`, which represents either a subscription with its linked tenant, or a tenant without any accessible subscriptions (Azure CLI does not show tenant names or domains). The provider will select the tenant ID from your default Azure CLI account. If you have more than one tenant listed in the output of `az account list`, for example if you are a guest user in other tenants, you can specify the tenant to use. -```shell +```shell-session # sh -export ARM_TENANT_ID=00000000-0000-0000-0000-000000000000 - +export ARM_TENANT_ID=00000000-0000-2222-2222-222222222222 +``` +```powershell # PowerShell -$env:ARM_TENANT_ID = 00000000-0000-0000-0000-000000000000 +$env:ARM_TENANT_ID = 00000000-0000-2222-2222-222222222222 ``` You can also configure the tenant ID from within the provider block. ```hcl provider "azuread" { - tenant_id = "00000000-0000-0000-0000-000000000000" + tenant_id = "00000000-0000-2222-2222-222222222222" } ``` Alternatively, you can configure the Azure CLI to default to the tenant you are managing with Terraform. -```bash +```shell-session $ az login --allow-no-subscriptions --tenant "TENANT_ID_OR_DOMAIN" ``` +
+ -> **Tenants and Subscriptions** The AzureAD provider operates on tenants and not on subscriptions. We recommend always specifying `az login --allow-no-subscriptions` as it will force the Azure CLI to report tenants with no associated subscriptions, or where your user account does not have any roles assigned for a subscription. --- @@ -100,7 +89,7 @@ No specific configuration is required for the provider to use Azure CLI authenti ```hcl provider "azuread" { - tenant_id = "10000000-2000-3000-4000-500000000000" + tenant_id = "00000000-0000-1111-1111-111111111111" } ``` diff --git a/docs/guides/managed_service_identity.md b/docs/guides/managed_service_identity.md index 2a7c692d6c..3cfa364738 100644 --- a/docs/guides/managed_service_identity.md +++ b/docs/guides/managed_service_identity.md @@ -34,11 +34,11 @@ When using a managed identity, you can only manage resources in the tenant where The (simplified) Terraform configuration below configures a Virtual Machine with a system-assigned identity, and then outputs the Object ID of the corresponding Service Principal: -```hcl +```terraform data "azurerm_subscription" "current" {} -resource "azurerm_linux_virtual_machine" "example" { - name = "test-vm" +resource "azurerm_linux_virtual_machine" "management_host" { + name = "management-vm" # ... @@ -47,8 +47,8 @@ resource "azurerm_linux_virtual_machine" "example" { } } -output "example_msi_object_id" { - value = azurerm_linux_virtual_machine.test.identity.0.principal_id +output "management_host_identity_object_id" { + value = azurerm_linux_virtual_machine.management_host.identity.0.principal_id } ``` @@ -56,6 +56,25 @@ Refer to the [azurerm_linux_virtual_machine][azurerm_linux_virtual_machine] and The implicitly created Service Principal should have the same or similar name as your virtual machine. At this point you will need to assign permissions to access Azure Active Directory to create and modify Azure Active Directory objects such as users and groups. See the [Configuring a Service Principal for managing Azure Active Directory][azuread-service-principal-permissions] guide for more information. +## Using a user-assigned identity + +As an alternative to using a system-assigned managed identity, you can create a user-assigned identity that can be allocated to one or more resources such as virtual machines. + +```terraform +resource "azurerm_user_assigned_identity" "terraform" { + name = "terraform" + # ... +} + +output "terraform_identity_object_id" { + value = azurerm_user_assigned_identity.terraform.principal_id +} +``` + +Refer to the [azurerm_user_assigned_identity][azurerm_user_assigned_identity] documentation for more information on how to configure this resource. + +The implicitly created Service Principal should have the same or similar name as the user assigned identity. At this point you will need to assign permissions to access Azure Active Directory to create and modify Azure Active Directory objects such as users and groups. See the [Configuring a Service Principal for managing Azure Active Directory][azuread-service-principal-permissions] guide for more information. + ## Configuring Managed Identity in Terraform At this point we assume that managed identity is configured on the resource (e.g. virtual machine) being used, that permissions have been granted, and that you are running Terraform on that resource. @@ -108,3 +127,4 @@ Next you should follow the [Configuring a Service Principal for managing Azure A [azuread-service-principal-permissions]: https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/guides/service_principal_configuration#method-1-api-roles-recommended-for-service-principals [azurerm_linux_virtual_machine]: https://www.terraform.io/docs/providers/azurerm/r/linux_virtual_machine.html [azurerm_windows_virtual_machine]: https://www.terraform.io/docs/providers/azurerm/r/windows_virtual_machine.html +[azurerm_user_assigned_identity]: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/user_assigned_identity diff --git a/docs/guides/microsoft-graph.md b/docs/guides/microsoft-graph.md index 94dcf88a0c..44ed0f23c2 100644 --- a/docs/guides/microsoft-graph.md +++ b/docs/guides/microsoft-graph.md @@ -210,7 +210,9 @@ The deprecated field `description` has been replaced by the `display_name` field -> The following also applies when the Microsoft Graph beta is enabled in version 1.5 or later -The `key_id` field has become read-only as Azure Active Directory no longer allows user-specified key IDs for passwords. This also means that the `azuread_application_password` resource no longer supports importing in version 2.0 of the provider. +The `azuread_application_password` resource no longer supports importing. + +The `key_id` field has become read-only as Azure Active Directory no longer allows user-specified key IDs for passwords. The `value` field has become read-only as Azure Active Directory no longer accepts user-supplied password values. Passwords are instead auto-generated by Azure and exported with the `value` attribute by the resource. @@ -226,9 +228,11 @@ The deprecated field `description` has been replaced by the `display_name` field -> The following also applies when the Microsoft Graph beta is enabled in version 1.5 or later +The `azuread_service_principal_password` resource no longer supports importing. + The `display_name`, `start_date` and `end_date` fields are no longer respected by the API and have been made read-only. Accordingly the `end_date_relative` field has been removed. -The `key_id` field has become read-only as Azure Active Directory no longer allows user-specified key IDs for passwords. This also means that the `azuread_service_principal_password` resource no longer supports importing in version 2.0 of the provider. +The `key_id` field has become read-only as Azure Active Directory no longer allows user-specified key IDs for passwords. The `value` field has become read-only as Azure Active Directory no longer accepts user-supplied password values. Passwords are instead auto-generated by Azure and exported with the `value` attribute by the resource. diff --git a/docs/guides/service_principal_client_certificate.md b/docs/guides/service_principal_client_certificate.md index 3b2755fc3d..7ebce126b7 100644 --- a/docs/guides/service_principal_client_certificate.md +++ b/docs/guides/service_principal_client_certificate.md @@ -78,13 +78,14 @@ The provider can be configured to read the certificate bundle from the .pfx file Our recommended approach is storing the credentials as Environment Variables, for example: *Reading the certificate bundle from the filesystem* -```bash +```shell-session # sh $ export ARM_CLIENT_ID="00000000-0000-0000-0000-000000000000" $ export ARM_CLIENT_CERTIFICATE_PATH="/path/to/my/client/certificate.pfx" $ export ARM_CLIENT_CERTIFICATE_PASSWORD="Pa55w0rd123" $ export ARM_TENANT_ID="10000000-2000-3000-4000-500000000000" - +``` +```powershell # PowerShell > $env:ARM_CLIENT_ID = "00000000-0000-0000-0000-000000000000" > $env:ARM_CLIENT_CERTIFICATE_PATH = "/path/to/my/client/certificate.pfx" @@ -93,13 +94,14 @@ $ export ARM_TENANT_ID="10000000-2000-3000-4000-500000000000" ``` *Passing the encoded certificate bundle directly* -```bash +```shell-session # sh $ export ARM_CLIENT_ID="00000000-0000-0000-0000-000000000000" $ export ARM_CLIENT_CERTIFICATE="$(base64 /path/to/my/client/certificate.pfx)" $ export ARM_CLIENT_CERTIFICATE_PASSWORD="Pa55w0rd123" $ export ARM_TENANT_ID="10000000-2000-3000-4000-500000000000" - +``` +```powershell # PowerShell > $env:ARM_CLIENT_ID = "00000000-0000-0000-0000-000000000000" > $env:ARM_CLIENT_CERTIFICATE = [Convert]::ToBase64String([System.IO.File]::ReadAllBytes("/path/to/my/client/certificate.pfx")) @@ -115,7 +117,7 @@ Next you should follow the [Configuring a Service Principal for managing Azure A It's also possible to configure these variables either directly, or from variables, in your provider block, like so: -~> We recommend not defining these variables in-line since they could easily be checked into Source Control. +~> **Caution** We recommend not defining these variables in-line since they could easily be checked into Source Control. *Reading the certificate bundle from the filesystem* ```hcl diff --git a/docs/guides/service_principal_client_secret.md b/docs/guides/service_principal_client_secret.md index b54311500f..12dc7562ec 100644 --- a/docs/guides/service_principal_client_secret.md +++ b/docs/guides/service_principal_client_secret.md @@ -56,16 +56,17 @@ Now we have obtained the necessary credentials, it's possible to configure Terra Our recommended approach is storing the credentials as Environment Variables, for example: -```bash +```shell-session # sh $ export ARM_CLIENT_ID="00000000-0000-0000-0000-000000000000" $ export ARM_CLIENT_SECRET="MyCl1eNtSeCr3t" $ export ARM_TENANT_ID="10000000-2000-3000-4000-500000000000" - +``` +```powershell # PowerShell -$env:ARM_CLIENT_ID = 00000000-0000-0000-0000-000000000000 -$env:ARM_CLIENT_SECRET = 03MWPmH2W3i008UVcucO1E1vifY_bR -$env:ARM_TENANT_ID = 10000000-2000-3000-4000-500000000000 +$env:ARM_CLIENT_ID = "00000000-0000-0000-0000-000000000000" +$env:ARM_CLIENT_SECRET = "MyCl1eNtSeCr3t" +$env:ARM_TENANT_ID = "10000000-2000-3000-4000-500000000000" ``` At this point running either `terraform plan` or `terraform apply` should allow Terraform to authenticate using the Client Secret. @@ -76,9 +77,9 @@ Next you should follow the [Configuring a Service Principal for managing Azure A It's also possible to configure these variables either directly, or from variables, in your provider block, like so: -~> We recommend not defining these variables in-line since they could easily be checked into Source Control. +~> **Caution** We recommend not defining these variables in-line since they could easily be checked into Source Control. -```hcl +```terraform variable "client_secret" {} provider "azuread" { diff --git a/docs/guides/service_principal_configuration.md b/docs/guides/service_principal_configuration.md index 9d6b4fe576..80fbff7926 100644 --- a/docs/guides/service_principal_configuration.md +++ b/docs/guides/service_principal_configuration.md @@ -20,7 +20,7 @@ We recommend using either a Service Principal or Managed Identity when running T A Service Principal represents an application within Azure Active Directory whose properties and authentication tokens can be used as the `tenant_id`, `client_id` and `client_secret` fields needed by Terraform. -Depending on how the service principal authenticates to Azure it can be created and configured in a number of different ways: +Depending on how the service principal authenticates to Azure it can be created and configured in a number of different ways: * [Authenticating to Azure using a Service Principal and a Client Certificate](service_principal_client_certificate.html) * [Authenticating to Azure using a Service Principal and a Client Secret](service_principal_client_secret.html) @@ -50,6 +50,8 @@ Resource(s) | Role Name(s) `azuread_group`
`azuread_group_member` | Group.ReadWrite.All `azuread_user` | User.ReadWrite.All +
+ -> **Permissions for other resources** If the resource you are using is not shown in the above table, consult the documentation page for the resource for a guide to the required permissions. Depending on the configuration of your AAD tenant, you may also need to grant the Directory.Read.All and/or Directory.ReadWrite.All roles. If a resource you are using is not shown in the table above, consult the resource documentation. diff --git a/docs/index.md b/docs/index.md index a2e16bc96a..786693903d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,6 +4,10 @@ The Azure Provider can be used to configure infrastructure in [Azure Active Dire Interested in the provider's latest features, or want to make sure you're up to date? Check out the [changelog](https://github.com/hashicorp/terraform-provider-azuread/blob/main/CHANGELOG.md) for version information and release notes. +## Getting Started + +If you're new to the AzureAD provider, check out our [Learn tutorial](https://learn.hashicorp.com/tutorials/terraform/azure-ad), which guides practitioners through learning the Terraform configuration language and the AzureAD provider, with an example workflow for managing users and groups. + ## Example Usage ```hcl diff --git a/docs/resources/application.md b/docs/resources/application.md index 2d980d045a..c2a8f900ad 100644 --- a/docs/resources/application.md +++ b/docs/resources/application.md @@ -6,6 +6,16 @@ subcategory: "Applications" Manages an application registration within Azure Active Directory. +## API Permissions + +The following API permissions are required in order to use this resource. + +When authenticated with a service principal, this resource requires one of the following application roles: `Application.ReadWrite.All` or `Directory.ReadWrite.All` + +-> It's possible to use this resource with the `Application.ReadWrite.OwnedBy` application role, provided the principal being used to run Terraform is included in the `owners` property. + +When authenticated with a user principal, this resource requires one of the following directory roles: `Application Administrator` or `Global Administrator` + ## Example Usage ```terraform @@ -51,7 +61,8 @@ resource "azuread_application" "example" { allowed_member_types = ["User", "Application"] description = "Admins can manage roles and perform all task actions" display_name = "Admin" - is_enabled = true + enabled = true + id = "1b19509b-32b1-4e9f-b71d-4992aa991967" value = "admin" } @@ -60,7 +71,7 @@ resource "azuread_application" "example" { description = "ReadOnly roles have limited query access" display_name = "ReadOnly" enabled = true - id = "%[6]s" + id = "497406e4-012a-4267-bf18-45a1cb148a01" value = "User" } @@ -135,7 +146,10 @@ The following arguments are supported: * `marketing_url` - (Optional) URL of the application's marketing page. * `oauth2_post_response_required` - (Optional) Specifies whether, as part of OAuth 2.0 token requests, Azure AD allows POST requests, as opposed to GET requests. Defaults to `false`, which specifies that only GET requests are allowed. * `optional_claims` - (Optional) An `optional_claims` block as documented below. -* `owners` - (Optional) A list of object IDs of principals that will be granted ownership of the application. It's recommended to specify the object ID of the authenticated principal running Terraform, to ensure sufficient permissions that the application can be subsequently updated. +* `owners` - (Optional) A set of object IDs of principals that will be granted ownership of the application. Supported object types are users or service principals. By default, no owners are assigned. + +-> **Ownership of Applications** It's recommended to always specify one or more application owners, including the principal being used to execute Terraform, such as in the example above. + * `prevent_duplicate_names` - (Optional) If `true`, will return an error if an existing application is found with the same name. Defaults to `false`. * `privacy_statement_url` - (Optional) URL of the application's privacy statement. * `public_client` - (Optional) A `public_client` block as documented below, which configures non-web app or non-web API application settings, for example mobile or other public clients such as an installed application running on a desktop device. @@ -264,16 +278,11 @@ In addition to all arguments above, the following attributes are exported: * `app_role_ids` - A mapping of app role values to app role IDs, intended to be useful when referencing app roles in other resources in your configuration. * `application_id` - The Application ID (also called Client ID). * `disabled_by_microsoft` - Whether Microsoft has disabled the registered application. If the application is disabled, this will be a string indicating the status/reason, e.g. `DisabledDueToViolationOfServicesAgreement` +* `logo_url` - CDN URL to the application's logo. * `oauth2_permission_scope_ids` - A mapping of OAuth2.0 permission scope values to scope IDs, intended to be useful when referencing permission scopes in other resources in your configuration. * `object_id` - The application's object ID. * `publisher_domain` - The verified publisher domain for the application. ---- - -`info` block exports the following: - -* `logo_url` - CDN URL to the application's logo. - ## Import Applications can be imported using their object ID, e.g. diff --git a/docs/resources/application_certificate.md b/docs/resources/application_certificate.md index e38ffe42ae..644afd2b0c 100644 --- a/docs/resources/application_certificate.md +++ b/docs/resources/application_certificate.md @@ -6,6 +6,16 @@ subcategory: "Applications" Manages a certificate associated with an application within Azure Active Directory. These are also referred to as client certificates during authentication. +## API Permissions + +The following API permissions are required in order to use this resource. + +When authenticated with a service principal, this resource requires one of the following application roles: `Application.ReadWrite.All` or `Directory.ReadWrite.All` + +-> It's possible to use this resource with the `Application.ReadWrite.OwnedBy` application role, provided the principal being used to run Terraform is included in the `owners` property. + +When authenticated with a user principal, this resource requires one of the following directory roles: `Application Administrator` or `Global Administrator` + ## Example Usage *Using a PEM certificate* @@ -113,12 +123,12 @@ The following arguments are supported: * `application_object_id` - (Required) The object ID of the application for which this certificate should be created. Changing this field forces a new resource to be created. * `encoding` - (Optional) Specifies the encoding used for the supplied certificate data. Must be one of `pem`, `base64` or `hex`. Defaults to `pem`. --> **NOTE:** The `hex` encoding option is useful for consuming certificate data from the [azurerm_key_vault_certificate](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault_certificate) resource. +-> **Tip for Azure Key Vault** The `hex` encoding option is useful for consuming certificate data from the [azurerm_key_vault_certificate](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault_certificate) resource. * `end_date` - (Optional) The end date until which the certificate is valid, formatted as an RFC3339 date string (e.g. `2018-01-01T01:02:03Z`). If omitted, the API will decide a suitable expiry date, which is typically around 2 years from the start date. Changing this field forces a new resource to be created. * `end_date_relative` - (Optional) A relative duration for which the certificate is valid until, for example `240h` (10 days) or `2400h30m`. Changing this field forces a new resource to be created. -~> **NOTE:** One of `end_date` or `end_date_relative` must be set. The maximum duration is enforced by Azure AD. +~> One of `end_date` or `end_date_relative` must be set. The maximum allowed duration is determined by Azure AD. * `key_id` - (Optional) A UUID used to uniquely identify this certificate. If omitted, a random UUID will be automatically generated. Changing this field forces a new resource to be created. * `start_date` - (Optional) The start date from which the certificate is valid, formatted as an RFC3339 date string (e.g. `2018-01-01T01:02:03Z`). If this isn't specified, the current date and time are used. Changing this field forces a new resource to be created. @@ -127,9 +137,7 @@ The following arguments are supported: ## Attributes Reference -In addition to all arguments above, the following attributes are exported: - -*No additional attributes are exported* +No additional attributes are exported. ## Import @@ -139,4 +147,4 @@ Certificates can be imported using the object ID of the associated application a terraform import azuread_application_certificate.test 00000000-0000-0000-0000-000000000000/certificate/11111111-1111-1111-1111-111111111111 ``` --> **NOTE:** This ID format is unique to Terraform and is composed of the application's object ID, the string "certificate" and the certificate's key ID in the format `{ObjectId}/certificate/{CertificateKeyId}`. +-> This ID format is unique to Terraform and is composed of the application's object ID, the string "certificate" and the certificate's key ID in the format `{ObjectId}/certificate/{CertificateKeyId}`. diff --git a/docs/resources/application_password.md b/docs/resources/application_password.md index a35e6d66e7..c4dcb1dd09 100644 --- a/docs/resources/application_password.md +++ b/docs/resources/application_password.md @@ -6,6 +6,16 @@ subcategory: "Applications" Manages a password credential associated with an application within Azure Active Directory. These are also referred to as client secrets during authentication. +## API Permissions + +The following API permissions are required in order to use this resource. + +When authenticated with a service principal, this resource requires one of the following application roles: `Application.ReadWrite.All` or `Directory.ReadWrite.All` + +-> It's possible to use this resource with the `Application.ReadWrite.OwnedBy` application role, provided the principal being used to run Terraform is included in the `owners` property. + +When authenticated with a user principal, this resource requires one of the following directory roles: `Application Administrator` or `Global Administrator` + ## Example Usage *Basic example* diff --git a/docs/resources/application_pre_authorized.md b/docs/resources/application_pre_authorized.md index fac746dc32..e61512efa1 100644 --- a/docs/resources/application_pre_authorized.md +++ b/docs/resources/application_pre_authorized.md @@ -6,6 +6,16 @@ subcategory: "Applications" Manages client applications that are pre-authorized with the specified permissions to access an application's APIs without requiring user consent. +## API Permissions + +The following API permissions are required in order to use this resource. + +When authenticated with a service principal, this resource requires one of the following application roles: `Application.ReadWrite.All` or `Directory.ReadWrite.All` + +-> It's possible to use this resource with the `Application.ReadWrite.OwnedBy` application role, provided the principal being used to run Terraform is included in the `owners` property. + +When authenticated with a user principal, this resource requires one of the following directory roles: `Application Administrator` or `Global Administrator` + ## Example Usage ```terraform @@ -56,9 +66,7 @@ The following arguments are supported: ## Attributes Reference -In addition to all arguments above, the following attributes are exported: - -*No additional attributes are exported* +No additional attributes are exported. ## Import @@ -68,4 +76,4 @@ Pre-authorized applications can be imported using the object ID of the authorizi terraform import azuread_application_pre_authorized.example 00000000-0000-0000-0000-000000000000/preAuthorizedApplication/11111111-1111-1111-1111-111111111111 ``` --> **NOTE:** This ID format is unique to Terraform and is composed of the authorizing application's object ID, the string "preAuthorizedApplication" and the authorized application's application ID (client ID) in the format `{ObjectId}/preAuthorizedApplication/{ApplicationId}`. +-> This ID format is unique to Terraform and is composed of the authorizing application's object ID, the string "preAuthorizedApplication" and the authorized application's application ID (client ID) in the format `{ObjectId}/preAuthorizedApplication/{ApplicationId}`. diff --git a/docs/resources/group.md b/docs/resources/group.md index 0f2c8ca7ef..919f6a9ed3 100644 --- a/docs/resources/group.md +++ b/docs/resources/group.md @@ -6,13 +6,24 @@ subcategory: "Groups" Manages a group within Azure Active Directory. +## API Permissions + +The following API permissions are required in order to use this resource. + +When authenticated with a service principal, this resource requires one of the following application roles: `Group.ReadWrite.All` or `Directory.ReadWrite.All` + +When authenticated with a user principal, this resource requires one of the following directory roles: `Groups Administrator`, `User Administrator` or `Global Administrator` + ## Example Usage *Basic example* ```terraform +data "azuread_client_config" "current" {} + resource "azuread_group" "example" { display_name = "example" + owners = [data.azuread_client_config.current.object_id] security_enabled = true } ``` @@ -20,26 +31,44 @@ resource "azuread_group" "example" { *Microsoft 365 group* ```terraform +data "azuread_client_config" "current" {} + +resource "azuread_user" "group_owner" { + user_principal_name = "example-group-owner@hashicorp.com" + display_name = "Group Owner" + mail_nickname = "example-group-owner" + password = "SecretP@sswd99!" +} + resource "azuread_group" "example" { display_name = "example" mail_enabled = true mail_nickname = "ExampleGroup" security_enabled = true types = ["Unified"] + + owners = [ + data.azuread_client_config.current.object_id, + azuread_user.group_owner.object_id, + ] } ``` *Group with members* ```terraform +data "azuread_client_config" "current" {} + resource "azuread_user" "example" { display_name = "J Doe" + owners = [data.azuread_client_config.current.object_id] password = "notSecure123" user_principal_name = "jdoe@hashicorp.com" } resource "azuread_group" "example" { display_name = "MyGroup" + owners = [data.azuread_client_config.current.object_id] security_enabled = true members = [ @@ -57,26 +86,28 @@ The following arguments are supported: * `behaviors` - (Optional) A set of behaviors for a Microsoft 365 group. Possible values are `AllowOnlyMembersToPost`, `HideGroupInOutlook`, `SubscribeNewGroupMembers` and `WelcomeEmailDisabled`. See [official documentation](https://docs.microsoft.com/en-us/graph/group-set-options) for more details. Changing this forces a new resource to be created. * `description` - (Optional) The description for the group. * `display_name` - (Required) The display name for the group. -* `mail_enabled` - (Optional) Whether the group is a mail enabled, with a shared group mailbox. At least one of `mail_enabled` or `security_enabled` must be specified. A group can be mail enabled _and_ security enabled. +* `mail_enabled` - (Optional) Whether the group is a mail enabled, with a shared group mailbox. At least one of `mail_enabled` or `security_enabled` must be specified. Only Microsoft 365 groups can be mail enabled (see the `types` property). * `mail_nickname` - (Optional) The mail alias for the group, unique in the organisation. Required for mail-enabled groups. Changing this forces a new resource to be created. * `members` - (Optional) A set of members who should be present in this group. Supported object types are Users, Groups or Service Principals. -* `owners` - (Optional) A set of owners who own this group. Supported object types are Users or Service Principals. -~> **Group Ownership and Permissions** Terraform always adds its own principal as a group owner to ensure that groups can continue to be managed. If using a user principal to execute Terraform, we recommend assigning the directory role `Groups Administrator` (or a role with the same effective permissions) to that user, in order to help prevent scenarios where groups may become unmanageable without administrative intervention. +!> **Warning** Do not use the `members` property at the same time as the [azuread_group_member](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/group_member) resource for the same group. Doing so will cause a conflict and group members will be removed. + +* `owners` - (Optional) A set of object IDs of principals that will be granted ownership of the group. Supported object types are users or service principals. By default, the principal being used to execute Terraform is assigned as the sole owner. Groups cannot be created with no owners or have all their owners removed. --> **Ownership of Microsoft 365 Groups** Microsoft 365 groups are required to have at least one owner which _must be a user_ (i.e. not a service principal). If you are running Terraform with an Azure AD user principal, you do not need to specify any owners for a group, although we suggest always specifying at least one user as an owner in your configuration. +-> **Group Ownership** It's recommended to always specify one or more group owners, including the principal being used to execute Terraform, such as in the example above. When removing group owners, if a user principal has been assigned ownership, the last user cannot be removed as an owner. Microsoft 365 groups are required to always have at least one owner which _must be a user_ (i.e. not a service principal). * `prevent_duplicate_names` - (Optional) If `true`, will return an error if an existing group is found with the same name. Defaults to `false`. * `provisioning_options` - (Optional) A set of provisioning options for a Microsoft 365 group. The only supported value is `Team`. See [official documentation](https://docs.microsoft.com/en-us/graph/group-set-options) for details. Changing this forces a new resource to be created. -* `security_enabled` - (Optional) Whether the group is a security group for controlling access to in-app resources. At least one of `security_enabled` or `mail_enabled` must be specified. A group can be security enabled _and_ mail enabled. +* `security_enabled` - (Optional) Whether the group is a security group for controlling access to in-app resources. At least one of `security_enabled` or `mail_enabled` must be specified. A Microsoft 365 group can be security enabled _and_ mail enabled (see the `types` property). * `theme` - (Optional) The colour theme for a Microsoft 365 group. Possible values are `Blue`, `Green`, `Orange`, `Pink`, `Purple`, `Red` or `Teal`. By default, no theme is set. * `types` - (Optional) A set of group types to configure for the group. The only supported type is `Unified`, which specifies a Microsoft 365 group. Required when `mail_enabled` is true. Changing this forces a new resource to be created. + +-> **Supported Group Types** At present, only security groups and Microsoft 365 groups can be created or managed with this resource. Distribution groups and mail-enabled security groups are not supported. Microsoft 365 groups can be security-enabled. + * `visibility` - (Optional) The group join policy and group content visibility. Possible values are `Private`, `Public`, or `Hiddenmembership`. Only Microsoft 365 groups can have `Hiddenmembership` visibility and this value must be set when the group is created. By default, security groups will receive `Private` visibility and Microsoft 365 groups will receive `Public` visibility. -> **Group Name Uniqueness** Group names are not unique within Azure Active Directory. Use the `prevent_duplicate_names` argument to check for existing groups if you want to avoid name collisions. -!> **Warning** Do not use the `azuread_group_member` resource at the same time as the `members` argument. - ## Attributes Reference In addition to all arguments above, the following attributes are exported: diff --git a/docs/resources/group_member.md b/docs/resources/group_member.md index 911460b407..e33832a927 100644 --- a/docs/resources/group_member.md +++ b/docs/resources/group_member.md @@ -6,7 +6,15 @@ subcategory: "Groups" Manages a single group membership within Azure Active Directory. --> **Warning** Do not use this resource at the same time as the `members` property of the `azuread_group` resource. +~> **Warning** Do not use this resource at the same time as the `members` property of the `azuread_group` resource for the same group. Doing so will cause a conflict and group members will be removed. + +## API Permissions + +The following API permissions are required in order to use this resource. + +When authenticated with a service principal, this resource requires one of the following application roles: `Group.ReadWrite.All` or `Directory.ReadWrite.All` + +When authenticated with a user principal, this resource requires one of the following directory roles: `Groups Administrator`, `User Administrator` or `Global Administrator` ## Example Usage @@ -48,4 +56,4 @@ Group members can be imported using the object ID of the group and the object ID terraform import azuread_group_member.test 00000000-0000-0000-0000-000000000000/member/11111111-1111-1111-1111-111111111111 ``` --> **NOTE:** This ID format is unique to Terraform and is composed of the Azure AD Group Object ID and the target Member Object ID in the format `{GroupObjectID}/member/{MemberObjectID}`. +-> This ID format is unique to Terraform and is composed of the Azure AD Group Object ID and the target Member Object ID in the format `{GroupObjectID}/member/{MemberObjectID}`. diff --git a/docs/resources/service_principal.md b/docs/resources/service_principal.md index 850cdfb6f4..5bf567b3a9 100644 --- a/docs/resources/service_principal.md +++ b/docs/resources/service_principal.md @@ -6,16 +6,28 @@ subcategory: "Service Principals" Manages a service principal associated with an application within Azure Active Directory. +## API Permissions + +The following API permissions are required in order to use this resource. + +When authenticated with a service principal, this resource requires one of the following application roles: `Application.ReadWrite.All` or `Directory.ReadWrite.All` + +When authenticated with a user principal, this resource requires one of the following directory roles: `Application Administrator` or `Global Administrator` + ## Example Usage ```terraform +data "azuread_client_config" "current" {} + resource "azuread_application" "example" { display_name = "example" + owners = [data.azuread_client_config.current.object_id] } resource "azuread_service_principal" "example" { application_id = azuread_application.example.application_id app_role_assignment_required = false + owners = [data.azuread_client_config.current.object_id] tags = ["example", "tags", "here"] } @@ -33,6 +45,10 @@ The following arguments are supported: * `login_url` - (Optional) The URL where the service provider redirects the user to Azure AD to authenticate. Azure AD uses the URL to launch the application from Microsoft 365 or the Azure AD My Apps. When blank, Azure AD performs IdP-initiated sign-on for applications configured with SAML-based single sign-on. * `notes` - (Optional) A free text field to capture information about the service principal, typically used for operational purposes. * `notification_email_addresses` - (Optional) A set of email addresses where Azure AD sends a notification when the active certificate is near the expiration date. This is only for the certificates used to sign the SAML token issued for Azure AD Gallery applications. +* `owners` - (Optional) A set of object IDs of principals that will be granted ownership of the service principal. Supported object types are users or service principals. By default, no owners are assigned. + +-> **Ownership of Service Principals** It's recommended to always specify one or more service principal owners, including the principal being used to execute Terraform, such as in the example above. + * `preferred_single_sign_on_mode` - (Optional) The single sign-on mode configured for this application. Azure AD uses the preferred single sign-on mode to launch the application from Microsoft 365 or the Azure AD My Apps. Supported values are `oidc`, `password`, `saml` or `notSupported`. Omit this property or specify a blank string to unset. * `tags` - (Optional) A set of tags to apply to the service principal. * `use_existing` - (Optional) When true, any existing service principal linked to the same application will be automatically imported. When false, an import error will be raised for any pre-existing service principal. diff --git a/docs/resources/service_principal_certificate.md b/docs/resources/service_principal_certificate.md index 0e39108000..f5ac587522 100644 --- a/docs/resources/service_principal_certificate.md +++ b/docs/resources/service_principal_certificate.md @@ -6,6 +6,14 @@ subcategory: "Service Principals" Manages a certificate associated with a service principal within Azure Active Directory. +## API Permissions + +The following API permissions are required in order to use this resource. + +When authenticated with a service principal, this resource requires one of the following application roles: `Application.ReadWrite.All` or `Directory.ReadWrite.All` + +When authenticated with a user principal, this resource requires one of the following directory roles: `Application Administrator` or `Global Administrator` + ## Example Usage *Using a PEM certificate* @@ -53,12 +61,12 @@ The following arguments are supported: * `encoding` - (Optional) Specifies the encoding used for the supplied certificate data. Must be one of `pem`, `base64` or `hex`. Defaults to `pem`. --> **NOTE:** The `hex` encoding option is useful for consuming certificate data from the [azurerm_key_vault_certificate](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault_certificate) resource. +-> **Tip for Azure Key Vault** The `hex` encoding option is useful for consuming certificate data from the [azurerm_key_vault_certificate](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault_certificate) resource. * `end_date` - (Optional) The end date until which the certificate is valid, formatted as an RFC3339 date string (e.g. `2018-01-01T01:02:03Z`). Changing this field forces a new resource to be created. * `end_date_relative` - (Optional) A relative duration for which the certificate is valid until, for example `240h` (10 days) or `2400h30m`. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". Changing this field forces a new resource to be created. -~> **NOTE:** One of `end_date` or `end_date_relative` must be set. The maximum duration is enforced by Azure AD. +~> One of `end_date` or `end_date_relative` must be set. The maximum duration is determined by Azure AD. * `key_id` - (Optional) A UUID used to uniquely identify this certificate. If not specified a UUID will be automatically generated. Changing this field forces a new resource to be created. * `service_principal_id` - (Required) The object ID of the service principal for which this certificate should be created. Changing this field forces a new resource to be created. @@ -68,9 +76,7 @@ The following arguments are supported: ## Attributes Reference -In addition to all arguments above, the following attributes are exported: - -*No additional attributes are exported* +No additional attributes are exported. ## Import @@ -80,4 +86,4 @@ Certificates can be imported using the object ID of the associated service princ terraform import azuread_service_principal_certificate.test 00000000-0000-0000-0000-000000000000/certificate/11111111-1111-1111-1111-111111111111 ``` --> **NOTE:** This ID format is unique to Terraform and is composed of the service principal's object ID, the string "certificate" and the certificate's key ID in the format `{ServicePrincipalObjectId}/certificate/{CertificateKeyId}`. +-> This ID format is unique to Terraform and is composed of the service principal's object ID, the string "certificate" and the certificate's key ID in the format `{ServicePrincipalObjectId}/certificate/{CertificateKeyId}`. diff --git a/docs/resources/service_principal_password.md b/docs/resources/service_principal_password.md index 100042461a..877e9ad297 100644 --- a/docs/resources/service_principal_password.md +++ b/docs/resources/service_principal_password.md @@ -6,6 +6,14 @@ subcategory: "Service Principals" Manages a password credential associated with a service principal within Azure Active Directory. See also the [azuread_application_password resource](application_password.html). +## API Permissions + +The following API permissions are required in order to use this resource. + +When authenticated with a service principal, this resource requires one of the following application roles: `Application.ReadWrite.All` or `Directory.ReadWrite.All` + +When authenticated with a user principal, this resource requires one of the following directory roles: `Application Administrator` or `Global Administrator` + ## Example Usage *Basic example* diff --git a/docs/resources/user.md b/docs/resources/user.md index a2689215f1..4c54bfc248 100644 --- a/docs/resources/user.md +++ b/docs/resources/user.md @@ -6,6 +6,14 @@ subcategory: "Users" Manages a user within Azure Active Directory. +## API Permissions + +The following API permissions are required in order to use this resource. + +When authenticated with a service principal, this resource requires one of the following application roles: `User.ReadWrite.All` or `Directory.ReadWrite.All` + +When authenticated with a user principal, this resource requires one of the following directory roles: `User Administrator` or `Global Administrator` + ## Example Usage ```terraform @@ -42,6 +50,9 @@ The following arguments are supported: * `onpremises_immutable_id` - (Optional) The value used to associate an on-premise Active Directory user account with their Azure AD user object. This must be specified if you are using a federated domain for the user's `user_principal_name` property when creating a new user account. * `other_mails` - (Optional) A list of additional email addresses for the user. * `password` - (Optional) The password for the user. The password must satisfy minimum requirements as specified by the password policy. The maximum length is 256 characters. This property is required when creating a new user. + +-> **Passwords and importing users** Passwords can be changed but not cleared. Removing the `password` property for an existing user resource, or setting the password value to a blank string, will not remove the password. When importing a user, Terraform will not reset the password unless the value is subsequently changed in your configuration. + * `postal_code` - (Optional) The postal code for the user's postal address. The postal code is specific to the user's country/region. In the United States of America, this attribute contains the ZIP code. * `preferred_language` - (Optional) The user's preferred language, in ISO 639-1 notation. * `show_in_address_list` - (Optional) Whether or not the Outlook global address list should include this user. Defaults to `true`. diff --git a/go.mod b/go.mod index 64ff432a3e..ba428ab450 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/hashicorp/terraform-plugin-sdk/v2 v2.7.0 github.com/hashicorp/yamux v0.0.0-20210316155119-a95892c5f864 // indirect github.com/klauspost/compress v1.12.2 // indirect - github.com/manicminer/hamilton v0.23.1 + github.com/manicminer/hamilton v0.26.0 github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.4.1 // indirect diff --git a/go.sum b/go.sum index 628d2da13d..bebef8c4e8 100644 --- a/go.sum +++ b/go.sum @@ -285,8 +285,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/manicminer/hamilton v0.23.1 h1:7p5WviHLT0VKG7DKs/PggsUeapA1w6fd8B19ah62+LY= -github.com/manicminer/hamilton v0.23.1/go.mod h1:4bnCX1oYiQuNa9CQnmT+WMHRNAEF4mT7ygW9Q/D3o+Y= +github.com/manicminer/hamilton v0.26.0 h1:AJ8RrSAG8xkTBKC+hOeUijgVFXiXaqPBDs7oRP3O14o= +github.com/manicminer/hamilton v0.26.0/go.mod h1:QryxpD/4+cdKuXNi0UjLDvgxYdP0LLmYz7dYU7DAX4U= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= diff --git a/internal/acceptance/testcase.go b/internal/acceptance/testcase.go index a510df9cd0..14b02371f6 100644 --- a/internal/acceptance/testcase.go +++ b/internal/acceptance/testcase.go @@ -45,6 +45,12 @@ func (td TestData) ResourceTestIgnoreDangling(t *testing.T, _ types.TestResource } func (td TestData) runAcceptanceTest(t *testing.T, testCase resource.TestCase) { + testCase.ExternalProviders = map[string]resource.ExternalProvider{ + "random": { + Source: "hashicorp/random", + VersionConstraint: ">= 3.0.0", + }, + } testCase.ProviderFactories = map[string]func() (*schema.Provider, error){ "azuread": func() (*schema.Provider, error) { return AzureADProvider, nil diff --git a/internal/clients/builder.go b/internal/clients/builder.go index a4c036a57a..323cfbd79b 100644 --- a/internal/clients/builder.go +++ b/internal/clients/builder.go @@ -4,9 +4,8 @@ import ( "context" "fmt" - "github.com/manicminer/hamilton/environments" - "github.com/manicminer/hamilton/auth" + "github.com/manicminer/hamilton/environments" "github.com/hashicorp/terraform-provider-azuread/internal/common" ) @@ -35,17 +34,6 @@ func (b *ClientBuilder) Build(ctx context.Context) (*Client, error) { return nil, err } - // Obtain the tenant ID from Azure CLI - if cli, ok := authorizer.(*auth.AzureCliAuthorizer); ok { - if cli.TenantID == "" { - return nil, fmt.Errorf("azure-cli could not determine tenant ID to use") - } - client.TenantID = cli.TenantID - if clientId, ok := environments.PublishedApis["MicrosoftAzureCli"]; ok && clientId != "" { - client.ClientID = clientId - } - } - client.Environment = b.AuthConfig.Environment o := &common.ClientOptions{ @@ -57,6 +45,21 @@ func (b *ClientBuilder) Build(ctx context.Context) (*Client, error) { TerraformVersion: client.TerraformVersion, } + // Obtain the tenant ID from Azure CLI + realAuthorizer := authorizer + if cache, ok := authorizer.(*auth.CachedAuthorizer); ok { + realAuthorizer = cache.Source + } + if cli, ok := realAuthorizer.(*auth.AzureCliAuthorizer); ok { + if cli.TenantID == "" { + return nil, fmt.Errorf("azure-cli could not determine tenant ID to use") + } + client.TenantID = cli.TenantID + if clientId, ok := environments.PublishedApis["MicrosoftAzureCli"]; ok && clientId != "" { + client.ClientID = clientId + } + } + if err := client.build(ctx, o); err != nil { return nil, fmt.Errorf("building client: %+v", err) } diff --git a/internal/common/client_options.go b/internal/common/client_options.go index 17aec8a433..5c32efa75b 100644 --- a/internal/common/client_options.go +++ b/internal/common/client_options.go @@ -39,6 +39,9 @@ func (o ClientOptions) ConfigureClient(c *msgraph.Client) { } *c.RequestMiddlewares = append(*c.RequestMiddlewares, o.requestLogger) *c.ResponseMiddlewares = append(*c.ResponseMiddlewares, o.responseLogger) + + // Default retry limit, can be overridden from within a resource + c.RetryableClient.RetryMax = 8 } func (o ClientOptions) requestLogger(req *http.Request) (*http.Request, error) { diff --git a/internal/services/applications/application_certificate_resource.go b/internal/services/applications/application_certificate_resource.go index 67e518ac7c..afe368ec73 100644 --- a/internal/services/applications/application_certificate_resource.go +++ b/internal/services/applications/application_certificate_resource.go @@ -163,7 +163,9 @@ func applicationCertificateResourceCreate(ctx context.Context, d *schema.Resourc newCredentials = append(newCredentials, *credential) properties := msgraph.Application{ - ID: &id.ObjectId, + DirectoryObject: msgraph.DirectoryObject{ + ID: &id.ObjectId, + }, KeyCredentials: &newCredentials, } if _, err := client.Update(ctx, properties); err != nil { @@ -257,7 +259,9 @@ func applicationCertificateResourceDelete(ctx context.Context, d *schema.Resourc } properties := msgraph.Application{ - ID: &id.ObjectId, + DirectoryObject: msgraph.DirectoryObject{ + ID: &id.ObjectId, + }, KeyCredentials: &newCredentials, } if _, err := client.Update(ctx, properties); err != nil { diff --git a/internal/services/applications/application_data_source.go b/internal/services/applications/application_data_source.go index bd2fe02cba..4dae8ea964 100644 --- a/internal/services/applications/application_data_source.go +++ b/internal/services/applications/application_data_source.go @@ -433,6 +433,7 @@ func applicationDataSource() *schema.Resource { func applicationDataSourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).Applications.ApplicationsClient + client.BaseClient.DisableRetries = true var app *msgraph.Application diff --git a/internal/services/applications/application_password_resource.go b/internal/services/applications/application_password_resource.go index dee1d3262a..05502a48b6 100644 --- a/internal/services/applications/application_password_resource.go +++ b/internal/services/applications/application_password_resource.go @@ -2,7 +2,7 @@ package applications import ( "context" - b64 "encoding/base64" + "encoding/base64" "errors" "log" "net/http" @@ -206,7 +206,7 @@ func applicationPasswordResourceRead(ctx context.Context, d *schema.ResourceData if credential.DisplayName != nil { tf.Set(d, "display_name", credential.DisplayName) } else if credential.CustomKeyIdentifier != nil { - displayName, err := b64.StdEncoding.DecodeString(*credential.CustomKeyIdentifier) + displayName, err := base64.StdEncoding.DecodeString(*credential.CustomKeyIdentifier) if err != nil { return tf.ErrorDiagPathF(err, "display_name", "Parsing CustomKeyIdentifier") } diff --git a/internal/services/applications/application_pre_authorized_resource.go b/internal/services/applications/application_pre_authorized_resource.go index dfcd7bce3c..2a51ff7e1a 100644 --- a/internal/services/applications/application_pre_authorized_resource.go +++ b/internal/services/applications/application_pre_authorized_resource.go @@ -104,7 +104,9 @@ func applicationPreAuthorizedResourceCreate(ctx context.Context, d *schema.Resou }) properties := msgraph.Application{ - ID: app.ID, + DirectoryObject: msgraph.DirectoryObject{ + ID: app.ID, + }, Api: &msgraph.ApplicationApi{ PreAuthorizedApplications: &newPreAuthorizedApps, }, @@ -157,7 +159,9 @@ func applicationPreAuthorizedResourceUpdate(ctx context.Context, d *schema.Resou } properties := msgraph.Application{ - ID: app.ID, + DirectoryObject: msgraph.DirectoryObject{ + ID: app.ID, + }, Api: &msgraph.ApplicationApi{ PreAuthorizedApplications: &newPreAuthorizedApps, }, @@ -245,7 +249,9 @@ func applicationPreAuthorizedResourceDelete(ctx context.Context, d *schema.Resou } properties := msgraph.Application{ - ID: app.ID, + DirectoryObject: msgraph.DirectoryObject{ + ID: app.ID, + }, Api: &msgraph.ApplicationApi{ PreAuthorizedApplications: &newPreAuthorizedApps, }, diff --git a/internal/services/applications/application_resource.go b/internal/services/applications/application_resource.go index de71075bad..6d16d59891 100644 --- a/internal/services/applications/application_resource.go +++ b/internal/services/applications/application_resource.go @@ -38,9 +38,9 @@ func applicationResource() *schema.Resource { CustomizeDiff: applicationResourceCustomizeDiff, Timeouts: &schema.ResourceTimeout{ - Create: schema.DefaultTimeout(5 * time.Minute), + Create: schema.DefaultTimeout(10 * time.Minute), Read: schema.DefaultTimeout(5 * time.Minute), - Update: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(10 * time.Minute), Delete: schema.DefaultTimeout(5 * time.Minute), }, @@ -332,12 +332,14 @@ func applicationResource() *schema.Resource { }, "owners": { - Description: "A list of object IDs of principals that will be granted ownership of the application. It's recommended to specify the object ID of the authenticated principal running Terraform, to ensure sufficient permissions that the application can be subsequently updated", + Description: "A list of object IDs of principals that will be granted ownership of the application", Type: schema.TypeSet, Optional: true, + Set: schema.HashString, + MaxItems: 100, Elem: &schema.Schema{ Type: schema.TypeString, - ValidateDiagFunc: validate.NoEmptyStrings, + ValidateDiagFunc: validate.UUID, }, }, @@ -806,6 +808,8 @@ func applicationDiffSuppress(k, old, new string, d *schema.ResourceData) bool { func applicationResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).Applications.ApplicationsClient + directoryObjectsClient := meta.(*clients.Client).Applications.DirectoryObjectsClient + callerId := meta.(*clients.Client).Claims.ObjectId displayName := d.Get("display_name").(string) // Perform this check at apply time to catch any duplicate names created during the same apply @@ -846,6 +850,51 @@ func applicationResourceCreate(ctx context.Context, d *schema.ResourceData, meta Web: expandApplicationWeb(d.Get("web").([]interface{})), } + // Sort the owners into two slices, the first containing up to 20 and the rest overflowing to the second slice + // The calling principal should always be in the first slice of owners + callerObject, _, err := directoryObjectsClient.Get(ctx, callerId, odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve calling principal object %q", callerId) + } + if callerObject == nil { + return tf.ErrorDiagF(errors.New("returned callerObject was nil"), "Could not retrieve calling principal object %q", callerId) + } + ownersFirst20 := msgraph.Owners{*callerObject} + var ownersExtra msgraph.Owners + + // Track whether we need to remove the calling principal later on + removeCallerOwner := true + + // Retrieve and set the initial owners, which can be up to 20 in total when creating the application + if v, ok := d.GetOk("owners"); ok { + ownerCount := 0 + for _, id := range v.(*schema.Set).List() { + if strings.EqualFold(id.(string), callerId) { + removeCallerOwner = false + continue + } + ownerObject, _, err := directoryObjectsClient.Get(ctx, id.(string), odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve owner principal object %q", id) + } + if ownerObject == nil { + return tf.ErrorDiagF(errors.New("ownerObject was nil"), "Could not retrieve owner principal object %q", id) + } + if ownerObject.ODataId == nil { + return tf.ErrorDiagF(errors.New("ODataId was nil"), "Could not retrieve owner principal object %q", id) + } + if ownerCount < 19 { + ownersFirst20 = append(ownersFirst20, *ownerObject) + } else { + ownersExtra = append(ownersExtra, *ownerObject) + } + ownerCount++ + } + } + + // Set the initial owners, which should include the calling principal plus up to 19 of owners specified in configuration + properties.Owners = &ownersFirst20 + app, _, err := client.Create(ctx, properties) if err != nil { return tf.ErrorDiagF(err, "Could not create application") @@ -857,9 +906,19 @@ func applicationResourceCreate(ctx context.Context, d *schema.ResourceData, meta d.SetId(*app.ID) - owners := *tf.ExpandStringSlicePtr(d.Get("owners").(*schema.Set).List()) - if err := applicationSetOwners(ctx, client, app, owners); err != nil { - return tf.ErrorDiagPathF(err, "owners", "Could not set owners for application with object ID: %q", *app.ID) + if len(ownersExtra) > 0 { + // Add any remaining owners after the application is created + app.Owners = &ownersExtra + if _, err := client.AddOwners(ctx, app); err != nil { + return tf.ErrorDiagF(err, "Could not add owners to application with object ID: %q", d.Id()) + } + } + + // If the calling principal was not included in configuration, remove it now + if removeCallerOwner { + if _, err = client.RemoveOwners(ctx, d.Id(), &[]string{callerId}); err != nil { + return tf.ErrorDiagF(err, "Could not remove initial owner from application with object ID: %q", d.Id()) + } } return applicationResourceRead(ctx, d, meta) @@ -867,6 +926,7 @@ func applicationResourceCreate(ctx context.Context, d *schema.ResourceData, meta func applicationResourceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).Applications.ApplicationsClient + directoryObjectsClient := meta.(*clients.Client).Applications.DirectoryObjectsClient applicationId := d.Id() displayName := d.Get("display_name").(string) @@ -890,7 +950,9 @@ func applicationResourceUpdate(ctx context.Context, d *schema.ResourceData, meta } properties := msgraph.Application{ - ID: utils.String(applicationId), + DirectoryObject: msgraph.DirectoryObject{ + ID: utils.String(applicationId), + }, Api: expandApplicationApi(d.Get("api").([]interface{})), AppRoles: expandApplicationAppRoles(d.Get("app_role").(*schema.Set).List()), DisplayName: utils.String(displayName), @@ -922,12 +984,44 @@ func applicationResourceUpdate(ctx context.Context, d *schema.ResourceData, meta } if _, err := client.Update(ctx, properties); err != nil { - return tf.ErrorDiagF(err, "Could not update application with ID: %q", d.Id()) + return tf.ErrorDiagF(err, "Could not update application with object ID: %q", d.Id()) } - owners := *tf.ExpandStringSlicePtr(d.Get("owners").(*schema.Set).List()) - if err := applicationSetOwners(ctx, client, &properties, owners); err != nil { - return tf.ErrorDiagPathF(err, "owners", "Could not set owners for application with object ID: %q", d.Id()) + if v, ok := d.GetOk("owners"); ok && d.HasChange("owners") { + owners, _, err := client.ListOwners(ctx, applicationId) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve owners for application with object ID: %q", d.Id()) + } + + desiredOwners := *tf.ExpandStringSlicePtr(v.(*schema.Set).List()) + existingOwners := *owners + ownersForRemoval := utils.Difference(existingOwners, desiredOwners) + ownersToAdd := utils.Difference(desiredOwners, existingOwners) + + if len(ownersToAdd) > 0 { + newOwners := make(msgraph.Owners, 0) + for _, m := range ownersToAdd { + ownerObject, _, err := directoryObjectsClient.Get(ctx, m, odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve owner principal object %q", m) + } + if ownerObject == nil { + return tf.ErrorDiagF(errors.New("returned ownerObject was nil"), "Could not retrieve owner principal object %q", m) + } + newOwners = append(newOwners, *ownerObject) + } + + properties.Owners = &newOwners + if _, err := client.AddOwners(ctx, &properties); err != nil { + return tf.ErrorDiagF(err, "Could not add owners to application with object ID: %q", d.Id()) + } + } + + if len(ownersForRemoval) > 0 { + if _, err = client.RemoveOwners(ctx, d.Id(), &ownersForRemoval); err != nil { + return tf.ErrorDiagF(err, "Could not remove owners from application with object ID: %q", d.Id()) + } + } } return applicationResourceRead(ctx, d, meta) diff --git a/internal/services/applications/application_resource_test.go b/internal/services/applications/application_resource_test.go index 19ebefb3b9..dd4ecf8f80 100644 --- a/internal/services/applications/application_resource_test.go +++ b/internal/services/applications/application_resource_test.go @@ -146,6 +146,23 @@ func TestAccApplication_appRoles(t *testing.T) { }) } +func TestAccApplication_duplicateAppRolesOauth2PermissionsIdsUnknown(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_application", "test") + r := ApplicationResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.duplicateAppRolesOauth2PermissionsIdsUnknown(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).Key("app_role.#").HasValue("1"), + check.That(data.ResourceName).Key("app_role_ids.%").HasValue("1"), + check.That(data.ResourceName).Key("api.0.oauth2_permission_scope.#").HasValue("1"), + check.That(data.ResourceName).Key("oauth2_permission_scope_ids.%").HasValue("1"), + ), + }, + }) +} + func TestAccApplication_duplicateAppRolesOauth2PermissionsValues(t *testing.T) { data := acceptance.BuildTestData(t, "azuread_application", "test") r := ApplicationResource{} @@ -266,6 +283,22 @@ func TestAccApplication_owners(t *testing.T) { ), }, data.ImportStep(), + { + Config: r.noOwners(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("0"), + ), + }, + data.ImportStep(), + { + Config: r.singleOwner(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("1"), + ), + }, + data.ImportStep(), { Config: r.threeOwners(data), Check: resource.ComposeTestCheckFunc( @@ -285,6 +318,38 @@ func TestAccApplication_owners(t *testing.T) { }) } +func TestAccApplication_createWithNoOwners(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_application", "test") + r := ApplicationResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.noOwners(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("0"), + ), + }, + data.ImportStep(), + }) +} + +func TestAccApplication_manyOwners(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_application", "test") + r := ApplicationResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.manyOwners(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("45"), + ), + }, + data.ImportStep(), + }) +} + func TestAccApplication_preventDuplicateNamesPass(t *testing.T) { data := acceptance.BuildTestData(t, "azuread_application", "test") r := ApplicationResource{} @@ -485,6 +550,7 @@ resource "azuread_application" "test" { redirect_uris = [ "https://login.microsoftonline.com/common/oauth2/nativeclient", "https://login.live.com/oauth20_desktop.srf", + "ms-appx-web://Microsoft.AAD.BrokerPlugin/00000000-1111-1111-1111-222222222222", ] } @@ -829,6 +895,41 @@ resource "azuread_application" "test" { `, data.RandomInteger, uuids[0], uuids[1], uuids[2], uuids[3]) } +func (ApplicationResource) duplicateAppRolesOauth2PermissionsIdsUnknown(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azuread" {} +provider "random" {} + +resource "random_uuid" "test" { + count = 2 +} + +resource "azuread_application" "test" { + display_name = "acctest-APP-%[1]d" + + api { + oauth2_permission_scope { + admin_consent_description = "Administer the application" + admin_consent_display_name = "Administer" + enabled = true + id = random_uuid.test[0].id + type = "Admin" + value = "administer" + } + } + + app_role { + allowed_member_types = ["User"] + description = "Admins can manage roles and perform all task actions" + display_name = "Admin" + enabled = true + id = random_uuid.test[1].id + value = "administrate" + } +} +`, data.RandomInteger, data.UUID(), data.UUID()) +} + func (ApplicationResource) duplicateAppRolesOauth2PermissionsValues(data acceptance.TestData) string { return fmt.Sprintf(` provider "azuread" {} @@ -888,6 +989,17 @@ resource "azuread_user" "testC" { `, data.RandomInteger, data.RandomPassword) } +func (ApplicationResource) noOwners(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azuread" {} + +resource "azuread_application" "test" { + display_name = "acctest-APP-%[1]d" + owners = [] +} +`, data.RandomInteger) +} + func (r ApplicationResource) singleOwner(data acceptance.TestData) string { return fmt.Sprintf(` %[1]s @@ -915,3 +1027,40 @@ resource "azuread_application" "test" { } `, r.templateThreeUsers(data), data.RandomInteger) } + +func (r ApplicationResource) manyOwners(data acceptance.TestData) string { + return fmt.Sprintf(` +data "azuread_client_config" "test" {} + +data "azuread_domains" "test" { + only_initial = true +} + +resource "azuread_application" "owner" { + count = 27 + display_name = "acctestApplicationOwner${count.index}-%[1]d" +} + +resource "azuread_service_principal" "owner" { + count = 27 + application_id = azuread_application.owner[count.index].application_id +} + +resource "azuread_user" "owner" { + count = 17 + user_principal_name = "acctestApplicationOwner${count.index}-%[1]d@${data.azuread_domains.test.domains.0.domain_name}" + display_name = "acctestApplicationOwner${count.index}-%[1]d" + password = "Qwer5678!@#" +} + +resource "azuread_application" "test" { + display_name = "acctest-APP-%[1]d" + + owners = flatten([ + data.azuread_client_config.test.object_id, + azuread_service_principal.owner.*.object_id, + azuread_user.owner.*.object_id, + ]) +} +`, data.RandomInteger) +} diff --git a/internal/services/applications/applications.go b/internal/services/applications/applications.go index 04d940be61..e599a134b4 100644 --- a/internal/services/applications/applications.go +++ b/internal/services/applications/applications.go @@ -80,7 +80,9 @@ func applicationDisableAppRoles(ctx context.Context, client *msgraph.Application if disable { // Disable any changed or removed roles properties := msgraph.Application{ - ID: application.ID, + DirectoryObject: msgraph.DirectoryObject{ + ID: application.ID, + }, AppRoles: &existingRoles, } if _, err := client.Update(ctx, properties); err != nil { @@ -193,7 +195,9 @@ func applicationDisableOauth2PermissionScopes(ctx context.Context, client *msgra if disable { // Disable any changed or removed scopes properties := msgraph.Application{ - ID: application.ID, + DirectoryObject: msgraph.DirectoryObject{ + ID: application.ID, + }, Api: &msgraph.ApplicationApi{ OAuth2PermissionScopes: &existingScopes, }, @@ -302,58 +306,25 @@ func applicationFindByName(ctx context.Context, client *msgraph.ApplicationsClie return &result, nil } -func applicationSetOwners(ctx context.Context, client *msgraph.ApplicationsClient, application *msgraph.Application, desiredOwners []string) error { - if application.ID == nil { - return fmt.Errorf("Cannot use Application model with nil ID") - } - - owners, _, err := client.ListOwners(ctx, *application.ID) - if err != nil { - return fmt.Errorf("retrieving owners for Application with object ID %q: %+v", *application.ID, err) - } - - existingOwners := *owners - ownersForRemoval := utils.Difference(existingOwners, desiredOwners) - ownersToAdd := utils.Difference(desiredOwners, existingOwners) - - if ownersToAdd != nil { - for _, m := range ownersToAdd { - application.AppendOwner(client.BaseClient.Endpoint, client.BaseClient.ApiVersion, m) - } - - if _, err := client.AddOwners(ctx, application); err != nil { - return fmt.Errorf("adding owners to Application with object ID %q: %+v", *application.ID, err) - } - } - - if ownersForRemoval != nil { - if _, err = client.RemoveOwners(ctx, *application.ID, &ownersForRemoval); err != nil { - return fmt.Errorf("removing owner from Application with object ID %q: %+v", *application.ID, err) - } - } - - return nil -} - func applicationValidateRolesScopes(appRoles, oauth2Permissions []interface{}) error { var ids, values []string for _, roleRaw := range appRoles { role := roleRaw.(map[string]interface{}) - if id := role["id"].(string); id != "" { + if id := role["id"].(string); id != "" && id != tf.PluginSdkUnknownValue { ids = append(ids, id) } - if val := role["value"].(string); val != "" { + if val := role["value"].(string); val != "" && val != tf.PluginSdkUnknownValue { values = append(values, val) } } for _, scopeRaw := range oauth2Permissions { scope := scopeRaw.(map[string]interface{}) - if id := scope["id"].(string); id != "" { + if id := scope["id"].(string); id != "" && id != tf.PluginSdkUnknownValue { ids = append(ids, id) } - if val := scope["value"].(string); val != "" { + if val := scope["value"].(string); val != "" && val != tf.PluginSdkUnknownValue { values = append(values, val) } } diff --git a/internal/services/applications/client/client.go b/internal/services/applications/client/client.go index e4a74b3d6c..7fa39b0477 100644 --- a/internal/services/applications/client/client.go +++ b/internal/services/applications/client/client.go @@ -7,14 +7,19 @@ import ( ) type Client struct { - ApplicationsClient *msgraph.ApplicationsClient + ApplicationsClient *msgraph.ApplicationsClient + DirectoryObjectsClient *msgraph.DirectoryObjectsClient } func NewClient(o *common.ClientOptions) *Client { - msClient := msgraph.NewApplicationsClient(o.TenantID) - o.ConfigureClient(&msClient.BaseClient) + applicationsClient := msgraph.NewApplicationsClient(o.TenantID) + o.ConfigureClient(&applicationsClient.BaseClient) + + directoryObjectsClient := msgraph.NewDirectoryObjectsClient(o.TenantID) + o.ConfigureClient(&directoryObjectsClient.BaseClient) return &Client{ - ApplicationsClient: msClient, + ApplicationsClient: applicationsClient, + DirectoryObjectsClient: directoryObjectsClient, } } diff --git a/internal/services/domains/domains_data_source.go b/internal/services/domains/domains_data_source.go index f824f619bd..95425c5e63 100644 --- a/internal/services/domains/domains_data_source.go +++ b/internal/services/domains/domains_data_source.go @@ -133,6 +133,7 @@ func domainsDataSource() *schema.Resource { func domainsDataSourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).Domains.DomainsClient + client.BaseClient.DisableRetries = true adminManaged := d.Get("admin_managed").(bool) onlyDefault := d.Get("only_default").(bool) diff --git a/internal/services/groups/client/client.go b/internal/services/groups/client/client.go index 3481c284e4..6311a0a72d 100644 --- a/internal/services/groups/client/client.go +++ b/internal/services/groups/client/client.go @@ -7,14 +7,19 @@ import ( ) type Client struct { - GroupsClient *msgraph.GroupsClient + DirectoryObjectsClient *msgraph.DirectoryObjectsClient + GroupsClient *msgraph.GroupsClient } func NewClient(o *common.ClientOptions) *Client { - msClient := msgraph.NewGroupsClient(o.TenantID) - o.ConfigureClient(&msClient.BaseClient) + directoryObjectsClient := msgraph.NewDirectoryObjectsClient(o.TenantID) + o.ConfigureClient(&directoryObjectsClient.BaseClient) + + groupsClient := msgraph.NewGroupsClient(o.TenantID) + o.ConfigureClient(&groupsClient.BaseClient) return &Client{ - GroupsClient: msClient, + DirectoryObjectsClient: directoryObjectsClient, + GroupsClient: groupsClient, } } diff --git a/internal/services/groups/group_data_source.go b/internal/services/groups/group_data_source.go index a57838ee22..4031ea446e 100644 --- a/internal/services/groups/group_data_source.go +++ b/internal/services/groups/group_data_source.go @@ -190,6 +190,7 @@ func groupDataSource() *schema.Resource { func groupDataSourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).Groups.GroupsClient + client.BaseClient.DisableRetries = true var group msgraph.Group var displayName string diff --git a/internal/services/groups/group_member_resource.go b/internal/services/groups/group_member_resource.go index d6971a17d4..7093c7b5c4 100644 --- a/internal/services/groups/group_member_resource.go +++ b/internal/services/groups/group_member_resource.go @@ -2,6 +2,7 @@ package groups import ( "context" + "errors" "log" "net/http" "strings" @@ -9,6 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/manicminer/hamilton/msgraph" "github.com/manicminer/hamilton/odata" "github.com/hashicorp/terraform-provider-azuread/internal/clients" @@ -57,6 +59,7 @@ func groupMemberResource() *schema.Resource { func groupMemberResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).Groups.GroupsClient + directoryObjectsClient := meta.(*clients.Client).Groups.DirectoryObjectsClient groupId := d.Get("group_object_id").(string) memberId := d.Get("member_object_id").(string) @@ -85,7 +88,17 @@ func groupMemberResourceCreate(ctx context.Context, d *schema.ResourceData, meta } } - group.AppendMember(client.BaseClient.Endpoint, client.BaseClient.ApiVersion, memberId) + memberObject, _, err := directoryObjectsClient.Get(ctx, memberId, odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve principal object %q", memberId) + } + if memberObject == nil { + return tf.ErrorDiagF(errors.New("returned memberObject was nil"), "Could not retrieve member principal object %q", memberId) + } + if memberObject.ODataId == nil { + return tf.ErrorDiagF(errors.New("ODataId was nil"), "Could not retrieve member principal object %q", memberId) + } + group.Members = &msgraph.Members{*memberObject} if _, err := client.AddMembers(ctx, group); err != nil { return tf.ErrorDiagF(err, "Adding group member %q to group %q", memberId, groupId) diff --git a/internal/services/groups/group_resource.go b/internal/services/groups/group_resource.go index 491bf22d28..e1906dad0e 100644 --- a/internal/services/groups/group_resource.go +++ b/internal/services/groups/group_resource.go @@ -33,9 +33,9 @@ func groupResource() *schema.Resource { CustomizeDiff: groupResourceCustomizeDiff, Timeouts: &schema.ResourceTimeout{ - Create: schema.DefaultTimeout(5 * time.Minute), + Create: schema.DefaultTimeout(20 * time.Minute), Read: schema.DefaultTimeout(5 * time.Minute), - Update: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(20 * time.Minute), Delete: schema.DefaultTimeout(5 * time.Minute), }, @@ -116,6 +116,8 @@ func groupResource() *schema.Resource { Type: schema.TypeSet, Optional: true, Computed: true, + MinItems: 1, + MaxItems: 100, Set: schema.HashString, Elem: &schema.Schema{ Type: schema.TypeString, @@ -253,17 +255,6 @@ func groupResource() *schema.Resource { func groupResourceCustomizeDiff(ctx context.Context, diff *schema.ResourceDiff, meta interface{}) error { client := meta.(*clients.Client).Groups.GroupsClient - callerId := meta.(*clients.Client).Claims.ObjectId - - // Suppress the diff when the only change is to remove the calling principal as a group owner - // as we always want to retain such ownership in order to avoid orphaning the group - existingOwnersRaw, newOwnersRaw := diff.GetChange("owners") - existingOwners := tf.ExpandStringSlice(existingOwnersRaw.(*schema.Set).List()) - newOwners := tf.ExpandStringSlice(newOwnersRaw.(*schema.Set).List()) - ownersToRemove := utils.Difference(existingOwners, newOwners) - if len(ownersToRemove) == 1 && ownersToRemove[0] == callerId { - diff.Clear("owners") - } // Check for duplicate names oldDisplayName, newDisplayName := diff.GetChange("display_name") @@ -346,7 +337,9 @@ func groupResourceCustomizeDiff(ctx context.Context, diff *schema.ResourceDiff, func groupResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).Groups.GroupsClient + directoryObjectsClient := meta.(*clients.Client).Groups.DirectoryObjectsClient callerId := meta.(*clients.Client).Claims.ObjectId + displayName := d.Get("display_name").(string) // Perform this check at apply time to catch any duplicate names created during the same apply @@ -408,8 +401,88 @@ func groupResourceCreate(ctx context.Context, d *schema.ResourceData, meta inter properties.Visibility = utils.String(visibility) } - // The calling principal is the initial owner (retained after other owners are also set) - properties.AppendOwner(client.BaseClient.Endpoint, client.BaseClient.ApiVersion, callerId) + // Sort the owners into two slices, the first containing up to 20 and the rest overflowing to the second slice + var ownersFirst20, ownersExtra msgraph.Owners + + // getOwnerObject retrieves and validates a DirectoryObject for a given object ID + getOwnerObject := func(ctx context.Context, id string) (*msgraph.DirectoryObject, error) { + ownerObject, _, err := directoryObjectsClient.Get(ctx, id, odata.Query{}) + if err != nil { + return nil, err + } + if ownerObject == nil { + return nil, errors.New("ownerObject was nil") + } + if ownerObject.ID == nil { + return nil, errors.New("ownerObject ID was nil") + } + if ownerObject.ODataId == nil { + return nil, errors.New("ODataId was nil") + } + if ownerObject.ODataType == nil { + return nil, errors.New("ownerObject ODataType was nil") + } + return ownerObject, nil + } + + // Retrieve and set the initial owners, which can be up to 20 in total when creating the group. + // First look for the calling principal, then prefer users, followed by service principals, to try and avoid + // ownership-related API validation errors for Microsoft 365 groups. + if v, ok := d.GetOk("owners"); ok { + owners := v.(*schema.Set).List() + ownerCount := 0 + + // First look for the calling principal in the specified owners; it should always be included in the initial + // owners to avoid orphaning a group when the caller doesn't have the Groups.ReadWrite.All scope. + for _, id := range owners { + ownerObject, err := getOwnerObject(ctx, id.(string)) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve owner principal object %q", id) + } + if *ownerObject.ID == callerId { + if ownerObject.ODataId == nil { + return tf.ErrorDiagF(errors.New("ODataId was nil"), "Could not retrieve owner principal object %q", id) + } + if ownerCount < 20 { + ownersFirst20 = append(ownersFirst20, *ownerObject) + } else { + ownersExtra = append(ownersExtra, *ownerObject) + } + ownerCount++ + } + } + + // Then look for users, and finally service principals + for _, t := range []odata.Type{odata.TypeUser, odata.TypeServicePrincipal} { + for _, id := range owners { + ownerObject, err := getOwnerObject(ctx, id.(string)) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve owner principal object %q", id) + } + if *ownerObject.ODataType == t { + if ownerCount < 20 { + ownersFirst20 = append(ownersFirst20, *ownerObject) + } else { + ownersExtra = append(ownersExtra, *ownerObject) + } + ownerCount++ + } + } + } + } + + if len(ownersFirst20) == 0 { + // The calling principal is the default owner if no others are specified. This is the default API behaviour, so + // we're being explicit about this in order to minimise confusion and avoid inconsistent API behaviours. + callerObject, err := getOwnerObject(ctx, callerId) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve calling principal object %q", callerId) + } + ownersFirst20 = msgraph.Owners{*callerObject} + } + + // Set the initial owners, which either be the calling principal, or up to 20 of the owners specified in configuration + properties.Owners = &ownersFirst20 group, _, err := client.Create(ctx, properties) if err != nil { @@ -422,25 +495,35 @@ func groupResourceCreate(ctx context.Context, d *schema.ResourceData, meta inter d.SetId(*group.ID) - // Configure owners after the group is created, so they can be set one-by-one - if v, ok := d.GetOk("owners"); ok { - owners := v.(*schema.Set).List() - for _, o := range owners { - group.AppendOwner(client.BaseClient.Endpoint, client.BaseClient.ApiVersion, o.(string)) - } + // Add any remaining owners after the group is created + if len(ownersExtra) > 0 { + group.Owners = &ownersExtra if _, err := client.AddOwners(ctx, group); err != nil { - return tf.ErrorDiagF(err, "Could not add owners to group with ID: %q", d.Id()) + return tf.ErrorDiagF(err, "Could not add owners to group with object ID: %q", d.Id()) } } - // Configure members after the group is created, so they can be reliably batched + // Add members after the group is created + members := make(msgraph.Members, 0) if v, ok := d.GetOk("members"); ok { - members := v.(*schema.Set).List() - for _, o := range members { - group.AppendMember(client.BaseClient.Endpoint, client.BaseClient.ApiVersion, o.(string)) + for _, id := range v.(*schema.Set).List() { + memberObject, _, err := directoryObjectsClient.Get(ctx, id.(string), odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve member principal object %q", id) + } + if memberObject == nil { + return tf.ErrorDiagF(errors.New("memberObject was nil"), "Could not retrieve member principal object %q", id) + } + if memberObject.ODataId == nil { + return tf.ErrorDiagF(errors.New("ODataId was nil"), "Could not retrieve member principal object %q", id) + } + members = append(members, *memberObject) } + } + if len(members) > 0 { + group.Members = &members if _, err := client.AddMembers(ctx, group); err != nil { - return tf.ErrorDiagF(err, "Could not add members to group with ID: %q", d.Id()) + return tf.ErrorDiagF(err, "Could not add members to group with object ID: %q", d.Id()) } } @@ -449,7 +532,9 @@ func groupResourceCreate(ctx context.Context, d *schema.ResourceData, meta inter func groupResourceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).Groups.GroupsClient + directoryObjectsClient := meta.(*clients.Client).Groups.DirectoryObjectsClient callerId := meta.(*clients.Client).Claims.ObjectId + groupId := d.Id() displayName := d.Get("display_name").(string) @@ -476,7 +561,9 @@ func groupResourceUpdate(ctx context.Context, d *schema.ResourceData, meta inter } group := msgraph.Group{ - ID: utils.String(groupId), + DirectoryObject: msgraph.DirectoryObject{ + ID: utils.String(groupId), + }, Description: utils.NullableString(d.Get("description").(string)), DisplayName: utils.String(displayName), MailEnabled: utils.Bool(d.Get("mail_enabled").(bool)), @@ -498,7 +585,7 @@ func groupResourceUpdate(ctx context.Context, d *schema.ResourceData, meta inter if v, ok := d.GetOk("members"); ok && d.HasChange("members") { members, _, err := client.ListMembers(ctx, *group.ID) if err != nil { - return tf.ErrorDiagF(err, "Could not retrieve members for group with ID: %q", d.Id()) + return tf.ErrorDiagF(err, "Could not retrieve members for group with object ID: %q", d.Id()) } existingMembers := *members @@ -506,19 +593,28 @@ func groupResourceUpdate(ctx context.Context, d *schema.ResourceData, meta inter membersForRemoval := utils.Difference(existingMembers, desiredMembers) membersToAdd := utils.Difference(desiredMembers, existingMembers) - if membersForRemoval != nil { + if len(membersForRemoval) > 0 { if _, err = client.RemoveMembers(ctx, d.Id(), &membersForRemoval); err != nil { - return tf.ErrorDiagF(err, "Could not remove members from group with ID: %q", d.Id()) + return tf.ErrorDiagF(err, "Could not remove members from group with object ID: %q", d.Id()) } } - if membersToAdd != nil { + if len(membersToAdd) > 0 { + newMembers := make(msgraph.Members, 0) for _, m := range membersToAdd { - group.AppendMember(client.BaseClient.Endpoint, client.BaseClient.ApiVersion, m) + memberObject, _, err := directoryObjectsClient.Get(ctx, m, odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve principal object %q", m) + } + if memberObject == nil { + return tf.ErrorDiagF(errors.New("returned memberObject was nil"), "Could not retrieve member principal object %q", m) + } + newMembers = append(newMembers, *memberObject) } + group.Members = &newMembers if _, err := client.AddMembers(ctx, &group); err != nil { - return tf.ErrorDiagF(err, "Could not add members to group with ID: %q", d.Id()) + return tf.ErrorDiagF(err, "Could not add members to group with object ID: %q", d.Id()) } } } @@ -526,29 +622,43 @@ func groupResourceUpdate(ctx context.Context, d *schema.ResourceData, meta inter if v, ok := d.GetOk("owners"); ok && d.HasChange("owners") { owners, _, err := client.ListOwners(ctx, *group.ID) if err != nil { - return tf.ErrorDiagF(err, "Could not retrieve owners for group with ID: %q", d.Id()) + return tf.ErrorDiagF(err, "Could not retrieve owners for group with object ID: %q", d.Id()) } - // The calling principal should always be an owner, regardless of the owners property - desiredOwners := utils.EnsureStringInSlice(*tf.ExpandStringSlicePtr(v.(*schema.Set).List()), callerId) + // If all owners are removed, restore the calling principal as the sole owner, in order to meet API + // restrictions about removing all owners, and maintain consistency with the Create behaviour. + // In theory this path should never be reached, since the property is Computed and has MinItems: 1, but we handle it anyway. + desiredOwners := tf.ExpandStringSlice(v.(*schema.Set).List()) + if len(desiredOwners) == 0 { + desiredOwners = []string{callerId} + } existingOwners := *owners ownersForRemoval := utils.Difference(existingOwners, desiredOwners) ownersToAdd := utils.Difference(desiredOwners, existingOwners) - if ownersToAdd != nil { + if len(ownersToAdd) > 0 { + newOwners := make(msgraph.Owners, 0) for _, m := range ownersToAdd { - group.AppendOwner(client.BaseClient.Endpoint, client.BaseClient.ApiVersion, m) + ownerObject, _, err := directoryObjectsClient.Get(ctx, m, odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve owner principal object %q", m) + } + if ownerObject == nil { + return tf.ErrorDiagF(errors.New("returned ownerObject was nil"), "Could not retrieve owner principal object %q", m) + } + newOwners = append(newOwners, *ownerObject) } + group.Owners = &newOwners if _, err := client.AddOwners(ctx, &group); err != nil { - return tf.ErrorDiagF(err, "Could not add owners to group with ID: %q", d.Id()) + return tf.ErrorDiagF(err, "Could not add owners to group with object ID: %q", d.Id()) } } - if ownersForRemoval != nil { + if len(ownersForRemoval) > 0 { if _, err = client.RemoveOwners(ctx, d.Id(), &ownersForRemoval); err != nil { - return tf.ErrorDiagF(err, "Could not remove owners from group with ID: %q", d.Id()) + return tf.ErrorDiagF(err, "Could not remove owners from group with object ID: %q", d.Id()) } } } diff --git a/internal/services/groups/group_resource_test.go b/internal/services/groups/group_resource_test.go index caaf630d5e..46dcca94d6 100644 --- a/internal/services/groups/group_resource_test.go +++ b/internal/services/groups/group_resource_test.go @@ -114,91 +114,65 @@ func TestAccGroup_owners(t *testing.T) { data.ResourceTest(t, r, []resource.TestStep{ { - Config: r.withThreeOwners(data), + Config: r.basic(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("1"), ), }, data.ImportStep(), - }) -} - -func TestAccGroup_members(t *testing.T) { - data := acceptance.BuildTestData(t, "azuread_group", "test") - r := GroupResource{} - - data.ResourceTest(t, r, []resource.TestStep{ { - Config: r.withThreeMembers(data), + Config: r.withOneOwner(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("1"), ), }, data.ImportStep(), - }) -} - -func TestAccGroup_membersAndOwners(t *testing.T) { - data := acceptance.BuildTestData(t, "azuread_group", "test") - r := GroupResource{} - - data.ResourceTest(t, r, []resource.TestStep{ { - Config: r.withOwnersAndMembers(data), + Config: r.withThreeOwners(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("3"), ), }, data.ImportStep(), - }) -} - -func TestAccGroup_manyMembersAndOwners(t *testing.T) { - data := acceptance.BuildTestData(t, "azuread_group", "test") - r := GroupResource{} - - data.ResourceTest(t, r, []resource.TestStep{ { - Config: r.withManyOwnersAndMembers(data), + Config: r.withOneOwner(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("1"), ), }, data.ImportStep(), - }) -} - -func TestAccGroup_membersDiverse(t *testing.T) { - data := acceptance.BuildTestData(t, "azuread_group", "test") - r := GroupResource{} - - data.ResourceTest(t, r, []resource.TestStep{ { - Config: r.withDiverseMembers(data), + Config: r.withServicePrincipalOwner(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("1"), ), }, data.ImportStep(), - }) -} - -func TestAccGroup_ownersDiverse(t *testing.T) { - data := acceptance.BuildTestData(t, "azuread_group", "test") - r := GroupResource{} - - data.ResourceTest(t, r, []resource.TestStep{ { Config: r.withDiverseOwners(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("2"), + ), + }, + data.ImportStep(), + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("2"), ), }, data.ImportStep(), }) } -func TestAccGroup_membersUpdate(t *testing.T) { +func TestAccGroup_members(t *testing.T) { data := acceptance.BuildTestData(t, "azuread_group", "test") r := GroupResource{} @@ -207,20 +181,23 @@ func TestAccGroup_membersUpdate(t *testing.T) { Config: r.basic(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("members.#").HasValue("0"), ), }, data.ImportStep(), { - Config: r.withOneMember(data), + Config: r.withThreeMembers(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("members.#").HasValue("3"), ), }, data.ImportStep(), { - Config: r.withThreeMembers(data), + Config: r.withOneMember(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("members.#").HasValue("1"), ), }, data.ImportStep(), @@ -228,49 +205,66 @@ func TestAccGroup_membersUpdate(t *testing.T) { Config: r.withServicePrincipalMember(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("members.#").HasValue("1"), + ), + }, + data.ImportStep(), + { + Config: r.withDiverseMembers(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("members.#").HasValue("3"), ), }, data.ImportStep(), { - Config: r.noMembers(data), + Config: r.withNoMembers(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("members.#").HasValue("0"), ), }, data.ImportStep(), }) } -func TestAccGroup_ownersUpdate(t *testing.T) { +func TestAccGroup_membersAndOwners(t *testing.T) { data := acceptance.BuildTestData(t, "azuread_group", "test") r := GroupResource{} data.ResourceTest(t, r, []resource.TestStep{ { - Config: r.basic(data), - Check: resource.ComposeTestCheckFunc( - check.That(data.ResourceName).ExistsInAzure(r), - ), - }, - data.ImportStep(), - { - Config: r.withThreeOwners(data), + Config: r.withOwnersAndMembers(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("members.#").HasValue("2"), + check.That(data.ResourceName).Key("owners.#").HasValue("1"), ), }, data.ImportStep(), + }) +} + +func TestAccGroup_manyMembersAndOwners(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_group", "test") + r := GroupResource{} + + data.ResourceTest(t, r, []resource.TestStep{ { - Config: r.withOneOwner(data), + Config: r.withManyOwnersAndMembers(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("members.#").HasValue("66"), + check.That(data.ResourceName).Key("owners.#").HasValue("45"), ), }, data.ImportStep(), { - Config: r.withServicePrincipalOwner(data), + Config: r.withOneOwnerAndNoMembers(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("members.#").HasValue("0"), + check.That(data.ResourceName).Key("owners.#").HasValue("1"), ), }, data.ImportStep(), @@ -507,33 +501,52 @@ resource "azuread_group" "test" { `, data.RandomInteger, visibility) } -func (GroupResource) noMembers(data acceptance.TestData) string { +func (r GroupResource) withOneOwner(data acceptance.TestData) string { return fmt.Sprintf(` +%[1]s + +resource "azuread_group" "test" { + display_name = "acctestGroup-%[2]d" + security_enabled = true + owners = [azuread_user.testA.object_id] +} +`, r.templateThreeUsers(data), data.RandomInteger) +} + +func (GroupResource) withServicePrincipalOwner(data acceptance.TestData) string { + return fmt.Sprintf(` +resource "azuread_application" "test" { + display_name = "acctestGroup-%[1]d" +} + +resource "azuread_service_principal" "test" { + application_id = azuread_application.test.application_id +} + resource "azuread_group" "test" { display_name = "acctestGroup-%[1]d" security_enabled = true - members = [] + owners = [azuread_service_principal.test.object_id] } `, data.RandomInteger) } -func (r GroupResource) withDiverseMembers(data acceptance.TestData) string { +func (r GroupResource) withDiverseOwners(data acceptance.TestData) string { return fmt.Sprintf(` %[1]s resource "azuread_group" "test" { display_name = "acctestGroup-%[2]d" security_enabled = true - members = [ + owners = [ + azuread_service_principal.test.object_id, azuread_user.test.object_id, - azuread_group.member.object_id, - azuread_service_principal.test.object_id ] } `, r.templateDiverseDirectoryObjects(data), data.RandomInteger) } -func (r GroupResource) withDiverseOwners(data acceptance.TestData) string { +func (r GroupResource) withThreeOwners(data acceptance.TestData) string { return fmt.Sprintf(` %[1]s @@ -541,11 +554,22 @@ resource "azuread_group" "test" { display_name = "acctestGroup-%[2]d" security_enabled = true owners = [ - azuread_user.test.object_id, - azuread_service_principal.test.object_id + azuread_user.testA.object_id, + azuread_user.testB.object_id, + azuread_user.testC.object_id ] } -`, r.templateDiverseDirectoryObjects(data), data.RandomInteger) +`, r.templateThreeUsers(data), data.RandomInteger) +} + +func (GroupResource) withNoMembers(data acceptance.TestData) string { + return fmt.Sprintf(` +resource "azuread_group" "test" { + display_name = "acctestGroup-%[1]d" + security_enabled = true + members = [] +} +`, data.RandomInteger) } func (r GroupResource) withOneMember(data acceptance.TestData) string { @@ -560,19 +584,25 @@ resource "azuread_group" "test" { `, r.templateThreeUsers(data), data.RandomInteger) } -func (r GroupResource) withOneOwner(data acceptance.TestData) string { +func (GroupResource) withServicePrincipalMember(data acceptance.TestData) string { return fmt.Sprintf(` -%[1]s +resource "azuread_application" "test" { + display_name = "acctestGroup-%[1]d" +} + +resource "azuread_service_principal" "test" { + application_id = azuread_application.test.application_id +} resource "azuread_group" "test" { - display_name = "acctestGroup-%[2]d" + display_name = "acctestGroup-%[1]d" security_enabled = true - owners = [azuread_user.testA.object_id] + members = [azuread_service_principal.test.object_id] } -`, r.templateThreeUsers(data), data.RandomInteger) +`, data.RandomInteger) } -func (r GroupResource) withThreeMembers(data acceptance.TestData) string { +func (r GroupResource) withDiverseMembers(data acceptance.TestData) string { return fmt.Sprintf(` %[1]s @@ -580,22 +610,22 @@ resource "azuread_group" "test" { display_name = "acctestGroup-%[2]d" security_enabled = true members = [ - azuread_user.testA.object_id, - azuread_user.testB.object_id, - azuread_user.testC.object_id + azuread_user.test.object_id, + azuread_group.member.object_id, + azuread_service_principal.test.object_id ] } -`, r.templateThreeUsers(data), data.RandomInteger) +`, r.templateDiverseDirectoryObjects(data), data.RandomInteger) } -func (r GroupResource) withThreeOwners(data acceptance.TestData) string { +func (r GroupResource) withThreeMembers(data acceptance.TestData) string { return fmt.Sprintf(` %[1]s resource "azuread_group" "test" { display_name = "acctestGroup-%[2]d" security_enabled = true - owners = [ + members = [ azuread_user.testA.object_id, azuread_user.testB.object_id, azuread_user.testC.object_id @@ -620,63 +650,74 @@ resource "azuread_group" "test" { `, r.templateThreeUsers(data), data.RandomInteger) } -func (r GroupResource) withManyOwnersAndMembers(data acceptance.TestData) string { +func (GroupResource) manyObjectsTemplate(data acceptance.TestData) string { return fmt.Sprintf(` +data "azuread_client_config" "test" {} + data "azuread_domains" "test" { only_initial = true } -resource "azuread_user" "test" { - count = 25 - - user_principal_name = "acctestGroupParticipant${count.index}-%[1]d@${data.azuread_domains.test.domains.0.domain_name}" - display_name = "acctestGroupParticipant${count.index}-%[1]d" - password = "Qwer5678!@#" -} - -resource "azuread_group" "test" { - display_name = "acctestGroup-%[1]d" +resource "azuread_group" "member" { + count = 21 + display_name = "acctestGroupParticipant${count.index}-%[1]d" security_enabled = true - owners = azuread_user.test.*.object_id - members = azuread_user.test.*.object_id -} -`, data.RandomInteger) } -func (GroupResource) withServicePrincipalMember(data acceptance.TestData) string { - return fmt.Sprintf(` resource "azuread_application" "test" { - display_name = "acctestGroup-%[1]d" + count = 27 + display_name = "acctestGroupParticipant${count.index}-%[1]d" } resource "azuread_service_principal" "test" { - application_id = azuread_application.test.application_id + count = 27 + application_id = azuread_application.test[count.index].application_id } -resource "azuread_group" "test" { - display_name = "acctestGroup-%[1]d" - security_enabled = true - members = [azuread_service_principal.test.object_id] +resource "azuread_user" "test" { + count = 17 + user_principal_name = "acctestGroupParticipant${count.index}-%[1]d@${data.azuread_domains.test.domains.0.domain_name}" + display_name = "acctestGroupParticipant${count.index}-%[1]d" + password = "Qwer5678!@#" } `, data.RandomInteger) } -func (GroupResource) withServicePrincipalOwner(data acceptance.TestData) string { +func (r GroupResource) withManyOwnersAndMembers(data acceptance.TestData) string { return fmt.Sprintf(` -resource "azuread_application" "test" { - display_name = "acctestGroup-%[1]d" -} +%[1]s -resource "azuread_service_principal" "test" { - application_id = azuread_application.test.application_id +resource "azuread_group" "test" { + display_name = "acctestGroup-%[2]d" + security_enabled = true + + owners = flatten([ + data.azuread_client_config.test.object_id, + azuread_service_principal.test.*.object_id, + azuread_user.test.*.object_id, + ]) + + members = flatten([ + data.azuread_client_config.test.object_id, + azuread_group.member.*.object_id, + azuread_service_principal.test.*.object_id, + azuread_user.test.*.object_id, + ]) +} +`, r.manyObjectsTemplate(data), data.RandomInteger) } +func (r GroupResource) withOneOwnerAndNoMembers(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]s + resource "azuread_group" "test" { - display_name = "acctestGroup-%[1]d" + display_name = "acctestGroup-%[2]d" security_enabled = true - owners = [azuread_service_principal.test.object_id] + owners = [azuread_user.test.0.object_id] + members = [] } -`, data.RandomInteger) +`, r.manyObjectsTemplate(data), data.RandomInteger) } func (GroupResource) preventDuplicateNamesPass(data acceptance.TestData) string { diff --git a/internal/services/groups/groups_data_source.go b/internal/services/groups/groups_data_source.go index de4800a44f..0ee7950ba8 100644 --- a/internal/services/groups/groups_data_source.go +++ b/internal/services/groups/groups_data_source.go @@ -65,6 +65,7 @@ func groupsDataSource() *schema.Resource { func groupsDataSourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).Groups.GroupsClient + client.BaseClient.DisableRetries = true var groups []msgraph.Group var expectedCount int diff --git a/internal/services/serviceprincipals/client/client.go b/internal/services/serviceprincipals/client/client.go index d3928811fd..ee8acac1cb 100644 --- a/internal/services/serviceprincipals/client/client.go +++ b/internal/services/serviceprincipals/client/client.go @@ -7,14 +7,19 @@ import ( ) type Client struct { + DirectoryObjectsClient *msgraph.DirectoryObjectsClient ServicePrincipalsClient *msgraph.ServicePrincipalsClient } func NewClient(o *common.ClientOptions) *Client { - msClient := msgraph.NewServicePrincipalsClient(o.TenantID) - o.ConfigureClient(&msClient.BaseClient) + directoryObjectsClient := msgraph.NewDirectoryObjectsClient(o.TenantID) + o.ConfigureClient(&directoryObjectsClient.BaseClient) + + servicePrincipalsClient := msgraph.NewServicePrincipalsClient(o.TenantID) + o.ConfigureClient(&servicePrincipalsClient.BaseClient) return &Client{ - ServicePrincipalsClient: msClient, + DirectoryObjectsClient: directoryObjectsClient, + ServicePrincipalsClient: servicePrincipalsClient, } } diff --git a/internal/services/serviceprincipals/service_principal_certificate_resource.go b/internal/services/serviceprincipals/service_principal_certificate_resource.go index ad8a35af2c..b9b82e05f5 100644 --- a/internal/services/serviceprincipals/service_principal_certificate_resource.go +++ b/internal/services/serviceprincipals/service_principal_certificate_resource.go @@ -163,7 +163,9 @@ func servicePrincipalCertificateResourceCreate(ctx context.Context, d *schema.Re newCredentials = append(newCredentials, *credential) properties := msgraph.ServicePrincipal{ - ID: &id.ObjectId, + DirectoryObject: msgraph.DirectoryObject{ + ID: &id.ObjectId, + }, KeyCredentials: &newCredentials, } if _, err := client.Update(ctx, properties); err != nil { @@ -257,7 +259,9 @@ func servicePrincipalCertificateResourceDelete(ctx context.Context, d *schema.Re } properties := msgraph.ServicePrincipal{ - ID: &id.ObjectId, + DirectoryObject: msgraph.DirectoryObject{ + ID: &id.ObjectId, + }, KeyCredentials: &newCredentials, } if _, err := client.Update(ctx, properties); err != nil { diff --git a/internal/services/serviceprincipals/service_principal_data_source.go b/internal/services/serviceprincipals/service_principal_data_source.go index 5410961bcc..c9a12c60cd 100644 --- a/internal/services/serviceprincipals/service_principal_data_source.go +++ b/internal/services/serviceprincipals/service_principal_data_source.go @@ -199,6 +199,7 @@ func servicePrincipalData() *schema.Resource { func servicePrincipalDataSourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).ServicePrincipals.ServicePrincipalsClient + client.BaseClient.DisableRetries = true var servicePrincipal *msgraph.ServicePrincipal diff --git a/internal/services/serviceprincipals/service_principal_password_resource.go b/internal/services/serviceprincipals/service_principal_password_resource.go index 0ee23a7dc9..d4f7fc38ce 100644 --- a/internal/services/serviceprincipals/service_principal_password_resource.go +++ b/internal/services/serviceprincipals/service_principal_password_resource.go @@ -2,14 +2,13 @@ package serviceprincipals import ( "context" + "encoding/base64" "errors" "log" "net/http" "strings" "time" - "github.com/hashicorp/terraform-provider-azuread/internal/services/serviceprincipals/migrations" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/manicminer/hamilton/msgraph" @@ -17,6 +16,7 @@ import ( "github.com/hashicorp/terraform-provider-azuread/internal/clients" "github.com/hashicorp/terraform-provider-azuread/internal/helpers" + "github.com/hashicorp/terraform-provider-azuread/internal/services/serviceprincipals/migrations" "github.com/hashicorp/terraform-provider-azuread/internal/services/serviceprincipals/parse" "github.com/hashicorp/terraform-provider-azuread/internal/tf" "github.com/hashicorp/terraform-provider-azuread/internal/validate" @@ -182,7 +182,16 @@ func servicePrincipalPasswordResourceRead(ctx context.Context, d *schema.Resourc return nil } - tf.Set(d, "display_name", credential.DisplayName) + if credential.DisplayName != nil { + tf.Set(d, "display_name", credential.DisplayName) + } else if credential.CustomKeyIdentifier != nil { + displayName, err := base64.StdEncoding.DecodeString(*credential.CustomKeyIdentifier) + if err != nil { + return tf.ErrorDiagPathF(err, "display_name", "Parsing CustomKeyIdentifier") + } + tf.Set(d, "display_name", string(displayName)) + } + tf.Set(d, "key_id", id.KeyId) tf.Set(d, "service_principal_id", id.ObjectId) diff --git a/internal/services/serviceprincipals/service_principal_resource.go b/internal/services/serviceprincipals/service_principal_resource.go index d29adadde1..a8f62fa0a3 100644 --- a/internal/services/serviceprincipals/service_principal_resource.go +++ b/internal/services/serviceprincipals/service_principal_resource.go @@ -33,9 +33,9 @@ func servicePrincipalResource() *schema.Resource { DeleteContext: servicePrincipalResourceDelete, Timeouts: &schema.ResourceTimeout{ - Create: schema.DefaultTimeout(5 * time.Minute), + Create: schema.DefaultTimeout(10 * time.Minute), Read: schema.DefaultTimeout(5 * time.Minute), - Update: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(10 * time.Minute), Delete: schema.DefaultTimeout(5 * time.Minute), }, @@ -109,6 +109,17 @@ func servicePrincipalResource() *schema.Resource { }, }, + "owners": { + Description: "A list of object IDs of principals that will be granted ownership of the service principal", + Type: schema.TypeSet, + Optional: true, + Set: schema.HashString, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validate.UUID, + }, + }, + "preferred_single_sign_on_mode": { Description: "The single sign-on mode configured for this application. Azure AD uses the preferred single sign-on mode to launch the application from Microsoft 365 or the Azure AD My Apps", Type: schema.TypeString, @@ -231,6 +242,8 @@ func servicePrincipalResource() *schema.Resource { func servicePrincipalResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).ServicePrincipals.ServicePrincipalsClient + directoryObjectsClient := meta.(*clients.Client).ServicePrincipals.DirectoryObjectsClient + callerId := meta.(*clients.Client).Claims.ObjectId appId := d.Get("application_id").(string) result, _, err := client.List(ctx, odata.Query{Filter: fmt.Sprintf("appId eq '%s'", appId)}) @@ -272,6 +285,51 @@ func servicePrincipalResourceCreate(ctx context.Context, d *schema.ResourceData, Tags: tf.ExpandStringSlicePtr(d.Get("tags").(*schema.Set).List()), } + // Sort the owners into two slices, the first containing up to 20 and the rest overflowing to the second slice + // The calling principal should always be in the first slice of owners + callerObject, _, err := directoryObjectsClient.Get(ctx, callerId, odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve calling principal object %q", callerId) + } + if callerObject == nil { + return tf.ErrorDiagF(errors.New("returned callerObject was nil"), "Could not retrieve calling principal object %q", callerId) + } + ownersFirst20 := msgraph.Owners{*callerObject} + var ownersExtra msgraph.Owners + + // Track whether we need to remove the calling principal later on + removeCallerOwner := true + + // Retrieve and set the initial owners, which can be up to 20 in total when creating the service principal + if v, ok := d.GetOk("owners"); ok { + ownerCount := 0 + for _, id := range v.(*schema.Set).List() { + if strings.EqualFold(id.(string), callerId) { + removeCallerOwner = false + continue + } + ownerObject, _, err := directoryObjectsClient.Get(ctx, id.(string), odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve owner principal object %q", id) + } + if ownerObject == nil { + return tf.ErrorDiagF(errors.New("ownerObject was nil"), "Could not retrieve owner principal object %q", id) + } + if ownerObject.ODataId == nil { + return tf.ErrorDiagF(errors.New("ODataId was nil"), "Could not retrieve owner principal object %q", id) + } + if ownerCount < 19 { + ownersFirst20 = append(ownersFirst20, *ownerObject) + } else { + ownersExtra = append(ownersExtra, *ownerObject) + } + ownerCount++ + } + } + + // Set the initial owners, which should include the calling principal plus up to 19 of owners specified in configuration + properties.Owners = &ownersFirst20 + servicePrincipal, _, err = client.Create(ctx, properties) if err != nil { return tf.ErrorDiagF(err, "Could not create service principal") @@ -282,14 +340,32 @@ func servicePrincipalResourceCreate(ctx context.Context, d *schema.ResourceData, } d.SetId(*servicePrincipal.ID) + // Add any remaining owners after the service principal is created + if len(ownersExtra) > 0 { + servicePrincipal.Owners = &ownersExtra + if _, err := client.AddOwners(ctx, servicePrincipal); err != nil { + return tf.ErrorDiagF(err, "Could not add owners to service principal with object ID: %q", d.Id()) + } + } + + // If the calling principal was not included in configuration, remove it now + if removeCallerOwner { + if _, err = client.RemoveOwners(ctx, d.Id(), &[]string{callerId}); err != nil { + return tf.ErrorDiagF(err, "Could not remove initial owner from service principal with object ID: %q", d.Id()) + } + } + return servicePrincipalResourceRead(ctx, d, meta) } func servicePrincipalResourceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).ServicePrincipals.ServicePrincipalsClient + directoryObjectsClient := meta.(*clients.Client).ServicePrincipals.DirectoryObjectsClient properties := msgraph.ServicePrincipal{ - ID: utils.String(d.Id()), + DirectoryObject: msgraph.DirectoryObject{ + ID: utils.String(d.Id()), + }, AlternativeNames: tf.ExpandStringSlicePtr(d.Get("alternative_names").(*schema.Set).List()), AccountEnabled: utils.Bool(d.Get("account_enabled").(bool)), AppRoleAssignmentRequired: utils.Bool(d.Get("app_role_assignment_required").(bool)), @@ -305,6 +381,43 @@ func servicePrincipalResourceUpdate(ctx context.Context, d *schema.ResourceData, return tf.ErrorDiagF(err, "Updating service principal with object ID: %q", d.Id()) } + if v, ok := d.GetOk("owners"); ok && d.HasChange("owners") { + owners, _, err := client.ListOwners(ctx, d.Id()) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve owners for service principal with object ID: %q", d.Id()) + } + + desiredOwners := *tf.ExpandStringSlicePtr(v.(*schema.Set).List()) + existingOwners := *owners + ownersForRemoval := utils.Difference(existingOwners, desiredOwners) + ownersToAdd := utils.Difference(desiredOwners, existingOwners) + + if len(ownersToAdd) > 0 { + newOwners := make(msgraph.Owners, 0) + for _, m := range ownersToAdd { + ownerObject, _, err := directoryObjectsClient.Get(ctx, m, odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve owner principal object %q", m) + } + if ownerObject == nil { + return tf.ErrorDiagF(errors.New("returned ownerObject was nil"), "Could not retrieve owner principal object %q", m) + } + newOwners = append(newOwners, *ownerObject) + } + + properties.Owners = &newOwners + if _, err := client.AddOwners(ctx, &properties); err != nil { + return tf.ErrorDiagF(err, "Could not add owners to service principal with object ID: %q", d.Id()) + } + } + + if len(ownersForRemoval) > 0 { + if _, err = client.RemoveOwners(ctx, d.Id(), &ownersForRemoval); err != nil { + return tf.ErrorDiagF(err, "Could not remove owners from service principal with object ID: %q", d.Id()) + } + } + } + return servicePrincipalResourceRead(ctx, d, meta) } @@ -358,6 +471,12 @@ func servicePrincipalResourceRead(ctx context.Context, d *schema.ResourceData, m tf.Set(d, "tags", servicePrincipal.Tags) tf.Set(d, "type", servicePrincipal.ServicePrincipalType) + owners, _, err := client.ListOwners(ctx, *servicePrincipal.ID) + if err != nil { + return tf.ErrorDiagPathF(err, "owners", "Could not retrieve owners for service principal with object ID %q", d.Id()) + } + tf.Set(d, "owners", owners) + return nil } diff --git a/internal/services/serviceprincipals/service_principal_resource_test.go b/internal/services/serviceprincipals/service_principal_resource_test.go index 68fdd43097..1e39d1d8e2 100644 --- a/internal/services/serviceprincipals/service_principal_resource_test.go +++ b/internal/services/serviceprincipals/service_principal_resource_test.go @@ -102,6 +102,94 @@ func TestAccServicePrincipal_update(t *testing.T) { }) } +func TestAccServicePrincipal_owners(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_service_principal", "test") + r := ServicePrincipalResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("0"), + ), + }, + data.ImportStep(), + { + Config: r.singleOwner(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("1"), + ), + }, + data.ImportStep(), + { + Config: r.noOwners(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("0"), + ), + }, + data.ImportStep(), + { + Config: r.singleOwner(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("1"), + ), + }, + data.ImportStep(), + { + Config: r.threeOwners(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("3"), + ), + }, + data.ImportStep(), + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("0"), + ), + }, + data.ImportStep(), + }) +} + +func TestAccApplication_createWithNoOwners(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_service_principal", "test") + r := ServicePrincipalResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.noOwners(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("0"), + ), + }, + data.ImportStep(), + }) +} + +func TestAccServicePrincipal_manyOwners(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_service_principal", "test") + r := ServicePrincipalResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.manyOwners(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("45"), + ), + }, + data.ImportStep(), + }) +} + func TestAccServicePrincipal_useExisting(t *testing.T) { data := acceptance.BuildTestData(t, "azuread_service_principal", "msgraph") r := ServicePrincipalResource{} @@ -230,6 +318,125 @@ resource "azuread_service_principal" "test" { `, data.RandomInteger, data.UUID(), data.UUID(), data.UUID(), data.UUID()) } +func (ServicePrincipalResource) templateThreeUsers(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azuread" {} + +data "azuread_domains" "test" { + only_initial = true +} + +resource "azuread_user" "testA" { + user_principal_name = "acctestUser.%[1]d.A@${data.azuread_domains.test.domains.0.domain_name}" + display_name = "acctestUser-%[1]d-A" + password = "%[2]s" +} + +resource "azuread_user" "testB" { + user_principal_name = "acctestUser.%[1]d.B@${data.azuread_domains.test.domains.0.domain_name}" + display_name = "acctestUser-%[1]d-B" + mail_nickname = "acctestUser-%[1]d-B" + password = "%[2]s" +} + +resource "azuread_user" "testC" { + user_principal_name = "acctestUser.%[1]d.C@${data.azuread_domains.test.domains.0.domain_name}" + display_name = "acctestUser-%[1]d-C" + password = "%[2]s" +} +`, data.RandomInteger, data.RandomPassword) +} + +func (ServicePrincipalResource) noOwners(data acceptance.TestData) string { + return fmt.Sprintf(` +resource "azuread_application" "test" { + display_name = "acctestServicePrincipal-%[1]d" +} + +resource "azuread_service_principal" "test" { + application_id = azuread_application.test.application_id + owners = [] +} +`, data.RandomInteger) +} + +func (r ServicePrincipalResource) singleOwner(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]s + +resource "azuread_application" "test" { + display_name = "acctestServicePrincipal-%[2]d" +} + +resource "azuread_service_principal" "test" { + application_id = azuread_application.test.application_id + owners = [ + azuread_user.testA.object_id, + ] +} +`, r.templateThreeUsers(data), data.RandomInteger) +} + +func (r ServicePrincipalResource) threeOwners(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]s + +resource "azuread_application" "test" { + display_name = "acctestServicePrincipal-%[2]d" +} + +resource "azuread_service_principal" "test" { + application_id = azuread_application.test.application_id + owners = [ + azuread_user.testA.object_id, + azuread_user.testB.object_id, + azuread_user.testC.object_id, + ] +} +`, r.templateThreeUsers(data), data.RandomInteger) +} + +func (r ServicePrincipalResource) manyOwners(data acceptance.TestData) string { + return fmt.Sprintf(` +data "azuread_client_config" "test" {} + +data "azuread_domains" "test" { + only_initial = true +} + +resource "azuread_application" "owner" { + count = 27 + display_name = "acctestServicePrincipalOwner${count.index}-%[1]d" +} + +resource "azuread_service_principal" "owner" { + count = 27 + application_id = azuread_application.owner[count.index].application_id +} + +resource "azuread_user" "owner" { + count = 17 + user_principal_name = "acctestServicePrincipalOwner${count.index}-%[1]d@${data.azuread_domains.test.domains.0.domain_name}" + display_name = "acctestServicePrincipalOwner${count.index}-%[1]d" + password = "Qwer5678!@#" +} + +resource "azuread_application" "test" { + display_name = "acctestServicePrincipal-%[1]d" +} + +resource "azuread_service_principal" "test" { + application_id = azuread_application.test.application_id + + owners = flatten([ + data.azuread_client_config.test.object_id, + azuread_service_principal.owner.*.object_id, + azuread_user.owner.*.object_id, + ]) +} +`, data.RandomInteger) +} + func (ServicePrincipalResource) useExisting(_ acceptance.TestData) string { return ` resource "azuread_service_principal" "msgraph" { diff --git a/internal/services/users/user_data_source.go b/internal/services/users/user_data_source.go index 091f85132f..ac4f8e86b5 100644 --- a/internal/services/users/user_data_source.go +++ b/internal/services/users/user_data_source.go @@ -286,6 +286,7 @@ func userDataSource() *schema.Resource { func userDataSourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).Users.UsersClient + client.BaseClient.DisableRetries = true var user msgraph.User diff --git a/internal/services/users/user_resource.go b/internal/services/users/user_resource.go index 82c7be73cb..d2b339f3d7 100644 --- a/internal/services/users/user_resource.go +++ b/internal/services/users/user_resource.go @@ -341,10 +341,6 @@ func userResource() *schema.Resource { } func userResourceCustomizeDiff(ctx context.Context, diff *schema.ResourceDiff, meta interface{}) error { - if diff.Id() == "" && diff.Get("password").(string) == "" { - return fmt.Errorf("`password` is required when creating a new user") - } - ageGroup := diff.Get("age_group").(string) consentRequired := diff.Get("consent_provided_for_minor").(string) @@ -358,6 +354,11 @@ func userResourceCustomizeDiff(ctx context.Context, diff *schema.ResourceDiff, m func userResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).Users.UsersClient + password := d.Get("password").(string) + if password == "" { + return tf.ErrorDiagPathF(errors.New("`password` is required when creating a new user"), "password", "Could not create user") + } + upn := d.Get("user_principal_name").(string) mailNickName := d.Get("mail_nickname").(string) @@ -395,7 +396,7 @@ func userResourceCreate(ctx context.Context, d *schema.ResourceData, meta interf PasswordProfile: &msgraph.UserPasswordProfile{ ForceChangePasswordNextSignIn: utils.Bool(d.Get("force_password_change").(bool)), - Password: utils.String(d.Get("password").(string)), + Password: utils.String(password), }, } @@ -425,7 +426,9 @@ func userResourceUpdate(ctx context.Context, d *schema.ResourceData, meta interf client := meta.(*clients.Client).Users.UsersClient properties := msgraph.User{ - ID: utils.String(d.Id()), + DirectoryObject: msgraph.DirectoryObject{ + ID: utils.String(d.Id()), + }, AccountEnabled: utils.Bool(d.Get("account_enabled").(bool)), AgeGroup: utils.NullableString(d.Get("age_group").(string)), City: utils.NullableString(d.Get("city").(string)), @@ -451,10 +454,10 @@ func userResourceUpdate(ctx context.Context, d *schema.ResourceData, meta interf UsageLocation: utils.NullableString(d.Get("usage_location").(string)), } - if d.HasChange("password") { + if password := d.Get("password").(string); d.HasChange("password") && password != "" { properties.PasswordProfile = &msgraph.UserPasswordProfile{ ForceChangePasswordNextSignIn: utils.Bool(d.Get("force_password_change").(bool)), - Password: utils.String(d.Get("password").(string)), + Password: utils.String(password), } } diff --git a/internal/services/users/user_resource_test.go b/internal/services/users/user_resource_test.go index 39b2d1c965..76fe378f1e 100644 --- a/internal/services/users/user_resource_test.go +++ b/internal/services/users/user_resource_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "regexp" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" @@ -98,6 +99,33 @@ func TestAccUser_threeUsersABC(t *testing.T) { }) } +func TestAccUser_withRandomProvider(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_user", "test") + r := UserResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.withRandomProvider(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("force_password_change", "password"), + }) +} + +func TestAccUser_passwordOmitted(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_user", "test") + r := UserResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.passwordOmitted(data), + ExpectError: regexp.MustCompile("`password` is required when creating a new user"), + }, + }) +} + func (r UserResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) { client := clients.Users.UsersClient client.BaseClient.DisableRetries = true @@ -202,3 +230,39 @@ resource "azuread_user" "testC" { } `, data.RandomInteger, data.RandomPassword) } + +func (UserResource) withRandomProvider(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azuread" {} +provider "random" {} + +data "azuread_domains" "test" { + only_initial = true +} + +resource "random_password" "test" { + length = 32 +} + +resource "azuread_user" "test" { + user_principal_name = "acctestUser.%[1]d@${data.azuread_domains.test.domains.0.domain_name}" + display_name = "acctestUser-%[1]d" + password = random_password.test.result +} +`, data.RandomInteger) +} + +func (UserResource) passwordOmitted(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azuread" {} + +data "azuread_domains" "test" { + only_initial = true +} + +resource "azuread_user" "test" { + user_principal_name = "acctestUser.%[1]d@${data.azuread_domains.test.domains.0.domain_name}" + display_name = "acctestUser-%[1]d" +} +`, data.RandomInteger) +} diff --git a/internal/services/users/users_data_source.go b/internal/services/users/users_data_source.go index 8e93407d8c..9fa6d63c92 100644 --- a/internal/services/users/users_data_source.go +++ b/internal/services/users/users_data_source.go @@ -146,6 +146,7 @@ func usersData() *schema.Resource { func usersDataSourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).Users.UsersClient + client.BaseClient.DisableRetries = true var users []msgraph.User var expectedCount int diff --git a/internal/tf/pluginsdk.go b/internal/tf/pluginsdk.go new file mode 100644 index 0000000000..e46c5c3fa0 --- /dev/null +++ b/internal/tf/pluginsdk.go @@ -0,0 +1,6 @@ +package tf + +// PluginSdkUnknownValue is a dummy value used/sent by the plugin SDK when a real value is not known at plan time, +// e.g. during a CustomizeDiff function +// See https://github.com/hashicorp/terraform-plugin-sdk/blob/main/internal/configs/hcl2shim/values.go#L16 +const PluginSdkUnknownValue = "74D93920-ED26-11E3-AC10-0800200C9A66" diff --git a/internal/validate/uri.go b/internal/validate/uri.go index 56b204054e..132387057c 100644 --- a/internal/validate/uri.go +++ b/internal/validate/uri.go @@ -40,7 +40,7 @@ func IsLogoutURL(i interface{}, path cty.Path) (ret diag.Diagnostics) { } func IsRedirectURI(i interface{}, path cty.Path) (ret diag.Diagnostics) { - ret = IsURIFunc([]string{"http", "https"}, false, true)(i, path) + ret = IsURIFunc([]string{"http", "https", "ms-appx-web"}, false, true)(i, path) if len(ret) > 0 { return } diff --git a/vendor/github.com/manicminer/hamilton/auth/azcli.go b/vendor/github.com/manicminer/hamilton/auth/azcli.go index 14bf2fa562..280030ac44 100644 --- a/vendor/github.com/manicminer/hamilton/auth/azcli.go +++ b/vendor/github.com/manicminer/hamilton/auth/azcli.go @@ -31,7 +31,6 @@ type AzureCliAuthorizer struct { // Token returns an access token using the Azure CLI as an authentication mechanism. func (a AzureCliAuthorizer) Token() (*oauth2.Token, error) { - // We don't need to handle token caching and refreshing since az-cli does that for us var token struct { AccessToken string `json:"accessToken"` ExpiresOn string `json:"expiresOn"` @@ -86,11 +85,12 @@ func NewAzureCliConfig(api Api, tenantId string) (*AzureCliConfig, error) { // TokenSource provides a source for obtaining access tokens using AzureCliAuthorizer. func (c *AzureCliConfig) TokenSource(ctx context.Context) Authorizer { - return &AzureCliAuthorizer{ + // Cache access tokens internally to avoid unnecessary `az` invocations + return NewCachedAuthorizer(&AzureCliAuthorizer{ TenantID: c.TenantID, ctx: ctx, conf: c, - } + }) } // checkAzVersion tries to determine the version of Azure CLI in the path and checks for a compatible version diff --git a/vendor/github.com/manicminer/hamilton/auth/cache.go b/vendor/github.com/manicminer/hamilton/auth/cache.go index b981dda6b6..ebc9a05881 100644 --- a/vendor/github.com/manicminer/hamilton/auth/cache.go +++ b/vendor/github.com/manicminer/hamilton/auth/cache.go @@ -6,15 +6,17 @@ import ( "golang.org/x/oauth2" ) -// cachedAuthorizer caches a token until it expires, then acquires a new token from source -type cachedAuthorizer struct { - source Authorizer - mutex sync.RWMutex - token *oauth2.Token +// CachedAuthorizer caches a token until it expires, then acquires a new token from Source +type CachedAuthorizer struct { + // Source contains the underlying Authorizer for obtaining tokens + Source Authorizer + + mutex sync.RWMutex + token *oauth2.Token } // Token returns the current token if it's still valid, else will acquire a new token -func (c *cachedAuthorizer) Token() (*oauth2.Token, error) { +func (c *CachedAuthorizer) Token() (*oauth2.Token, error) { c.mutex.RLock() valid := c.token != nil && c.token.Valid() c.mutex.RUnlock() @@ -22,7 +24,7 @@ func (c *cachedAuthorizer) Token() (*oauth2.Token, error) { if !valid { c.mutex.Lock() defer c.mutex.Unlock() - token, err := c.source.Token() + token, err := c.Source.Token() if err != nil { return nil, err } @@ -32,10 +34,10 @@ func (c *cachedAuthorizer) Token() (*oauth2.Token, error) { return c.token, nil } -// CachedAuthorizer returns an Authorizer that caches an access token for the duration of its validity. +// NewCachedAuthorizer returns an Authorizer that caches an access token for the duration of its validity. // If the cached token expires, a new one is acquired and cached. -func CachedAuthorizer(src Authorizer) Authorizer { - return &cachedAuthorizer{ - source: src, +func NewCachedAuthorizer(src Authorizer) Authorizer { + return &CachedAuthorizer{ + Source: src, } } diff --git a/vendor/github.com/manicminer/hamilton/auth/clientcredentials.go b/vendor/github.com/manicminer/hamilton/auth/clientcredentials.go index aed4dda4df..78d8edaba3 100644 --- a/vendor/github.com/manicminer/hamilton/auth/clientcredentials.go +++ b/vendor/github.com/manicminer/hamilton/auth/clientcredentials.go @@ -79,9 +79,9 @@ type ClientCredentialsConfig struct { func (c *ClientCredentialsConfig) TokenSource(ctx context.Context, authType ClientCredentialsType) (source Authorizer) { switch authType { case ClientCredentialsAssertionType: - source = CachedAuthorizer(clientAssertionAuthorizer{ctx, c}) + source = NewCachedAuthorizer(&clientAssertionAuthorizer{ctx, c}) case ClientCredentialsSecretType: - source = CachedAuthorizer(clientSecretAuthorizer{ctx, c}) + source = NewCachedAuthorizer(&clientSecretAuthorizer{ctx, c}) } return } diff --git a/vendor/github.com/manicminer/hamilton/auth/msi.go b/vendor/github.com/manicminer/hamilton/auth/msi.go index 88ddba9cca..909ce0b280 100644 --- a/vendor/github.com/manicminer/hamilton/auth/msi.go +++ b/vendor/github.com/manicminer/hamilton/auth/msi.go @@ -115,7 +115,7 @@ func NewMsiConfig(ctx context.Context, resource string, msiEndpoint string) (*Ms // TokenSource provides a source for obtaining access tokens using MsiAuthorizer. func (c *MsiConfig) TokenSource(ctx context.Context) Authorizer { - return CachedAuthorizer(&MsiAuthorizer{ctx: ctx, conf: c}) + return NewCachedAuthorizer(&MsiAuthorizer{ctx: ctx, conf: c}) } func azureMetadata(ctx context.Context, url string) (body []byte, err error) { diff --git a/vendor/github.com/manicminer/hamilton/msgraph/app_role_assignments.go b/vendor/github.com/manicminer/hamilton/msgraph/app_role_assignments.go index f6491f46ca..8ac0d0a4fa 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/app_role_assignments.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/app_role_assignments.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" ) @@ -58,17 +58,20 @@ func (c *AppRoleAssignmentsClient) List(ctx context.Context, id string) (*[]AppR if err != nil { return nil, status, fmt.Errorf("AppRoleAssignmentsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { AppRoleAssignments []AppRoleAssignment `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.AppRoleAssignments, status, nil } @@ -85,12 +88,14 @@ func (c *AppRoleAssignmentsClient) Remove(ctx context.Context, id, appRoleAssign if err != nil { return status, fmt.Errorf("AppRoleAssignmentsClient.BaseClient.Delete(): %v", err) } + return status, nil } // Assign assigns an app role to a user, group or service principal depending on client resource type. func (c *AppRoleAssignmentsClient) Assign(ctx context.Context, clientServicePrincipalId, resourceServicePrincipalId, appRoleId string) (*AppRoleAssignment, int, error) { var status int + data := struct { PrincipalId string `json:"principalId"` ResourceId string `json:"resourceId"` @@ -105,6 +110,7 @@ func (c *AppRoleAssignmentsClient) Assign(ctx context.Context, clientServicePrin if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -117,14 +123,17 @@ func (c *AppRoleAssignmentsClient) Assign(ctx context.Context, clientServicePrin if err != nil { return nil, status, fmt.Errorf("AppRoleAssignmentsClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var appRoleAssignment AppRoleAssignment if err := json.Unmarshal(respBody, &appRoleAssignment); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &appRoleAssignment, status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/applications.go b/vendor/github.com/manicminer/hamilton/msgraph/applications.go index 0c0a9cb91c..a7effa11ef 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/applications.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/applications.go @@ -5,7 +5,7 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "net/http" "github.com/manicminer/hamilton/odata" @@ -37,27 +37,32 @@ func (c *ApplicationsClient) List(ctx context.Context, query odata.Query) (*[]Ap if err != nil { return nil, status, fmt.Errorf("ApplicationsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Applications []Application `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.Applications, status, nil } // Create creates a new Application. func (c *ApplicationsClient) Create(ctx context.Context, application Application) (*Application, int, error) { var status int + body, err := json.Marshal(application) if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ValidStatusCodes: []int{http.StatusCreated}, @@ -69,15 +74,18 @@ func (c *ApplicationsClient) Create(ctx context.Context, application Application if err != nil { return nil, status, fmt.Errorf("ApplicationsClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var newApplication Application if err := json.Unmarshal(respBody, &newApplication); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &newApplication, status, nil } @@ -95,15 +103,18 @@ func (c *ApplicationsClient) Get(ctx context.Context, id string, query odata.Que if err != nil { return nil, status, fmt.Errorf("ApplicationsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var application Application if err := json.Unmarshal(respBody, &application); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &application, status, nil } @@ -122,28 +133,34 @@ func (c *ApplicationsClient) GetDeleted(ctx context.Context, id string, query od if err != nil { return nil, status, fmt.Errorf("ApplicationsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var application Application if err := json.Unmarshal(respBody, &application); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &application, status, nil } // Update amends the manifest of an existing Application. func (c *ApplicationsClient) Update(ctx context.Context, application Application) (int, error) { var status int + if application.ID == nil { return status, errors.New("ApplicationsClient.Update(): cannot update application with nil ID") } + body, err := json.Marshal(application) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Patch(ctx, PatchHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -156,6 +173,7 @@ func (c *ApplicationsClient) Update(ctx context.Context, application Application if err != nil { return status, fmt.Errorf("ApplicationsClient.BaseClient.Patch(): %v", err) } + return status, nil } @@ -172,6 +190,7 @@ func (c *ApplicationsClient) Delete(ctx context.Context, id string) (int, error) if err != nil { return status, fmt.Errorf("ApplicationsClient.BaseClient.Delete(): %v", err) } + return status, nil } @@ -189,6 +208,7 @@ func (c *ApplicationsClient) DeletePermanently(ctx context.Context, id string) ( if err != nil { return status, fmt.Errorf("ApplicationsClient.BaseClient.Delete(): %v", err) } + return status, nil } @@ -206,14 +226,16 @@ func (c *ApplicationsClient) ListDeleted(ctx context.Context, query odata.Query) if err != nil { return nil, status, err } + defer resp.Body.Close() - respBody, _ := ioutil.ReadAll(resp.Body) + respBody, _ := io.ReadAll(resp.Body) var data struct { DeletedApps []Application `json:"value"` } if err = json.Unmarshal(respBody, &data); err != nil { return nil, status, err } + return &data.DeletedApps, status, nil } @@ -231,21 +253,25 @@ func (c *ApplicationsClient) RestoreDeleted(ctx context.Context, id string) (*Ap if err != nil { return nil, status, fmt.Errorf("ApplicationsClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var restoredApplication Application if err = json.Unmarshal(respBody, &restoredApplication); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &restoredApplication, status, nil } // AddPassword appends a new password credential to an Application. func (c *ApplicationsClient) AddPassword(ctx context.Context, applicationId string, passwordCredential PasswordCredential) (*PasswordCredential, int, error) { var status int + body, err := json.Marshal(struct { PwdCredential PasswordCredential `json:"passwordCredential"` }{ @@ -254,6 +280,7 @@ func (c *ApplicationsClient) AddPassword(ctx context.Context, applicationId stri if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -266,21 +293,25 @@ func (c *ApplicationsClient) AddPassword(ctx context.Context, applicationId stri if err != nil { return nil, status, fmt.Errorf("ApplicationsClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var newPasswordCredential PasswordCredential if err := json.Unmarshal(respBody, &newPasswordCredential); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &newPasswordCredential, status, nil } // RemovePassword removes a password credential from an Application. func (c *ApplicationsClient) RemovePassword(ctx context.Context, applicationId string, keyId string) (int, error) { var status int + body, err := json.Marshal(struct { KeyId string `json:"keyId"` }{ @@ -289,6 +320,7 @@ func (c *ApplicationsClient) RemovePassword(ctx context.Context, applicationId s if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -301,6 +333,7 @@ func (c *ApplicationsClient) RemovePassword(ctx context.Context, applicationId s if err != nil { return status, fmt.Errorf("ApplicationsClient.BaseClient.Post(): %v", err) } + return status, nil } @@ -319,11 +352,13 @@ func (c *ApplicationsClient) ListOwners(ctx context.Context, id string) (*[]stri if err != nil { return nil, status, fmt.Errorf("ApplicationsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Owners []struct { Type string `json:"@odata.type"` @@ -333,10 +368,12 @@ func (c *ApplicationsClient) ListOwners(ctx context.Context, id string) (*[]stri if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + ret := make([]string, len(data.Owners)) for i, v := range data.Owners { ret[i] = v.Id } + return &ret, status, nil } @@ -356,11 +393,13 @@ func (c *ApplicationsClient) GetOwner(ctx context.Context, applicationId, ownerI if err != nil { return nil, status, fmt.Errorf("ApplicationsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Context string `json:"@odata.context"` Type string `json:"@odata.type"` @@ -370,37 +409,36 @@ func (c *ApplicationsClient) GetOwner(ctx context.Context, applicationId, ownerI if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.Id, status, nil } -// AddOwners adds a new owner to an Application. -// First populate the Owners field of the Application using the AppendOwner method of the model, then call this method. +// AddOwners adds new owners to an Application. +// First populate the `owners` field, then call this method func (c *ApplicationsClient) AddOwners(ctx context.Context, application *Application) (int, error) { var status int + if application.ID == nil { return status, errors.New("cannot update application with nil ID") } if application.Owners == nil { return status, errors.New("cannot update application with nil Owners") } + for _, owner := range *application.Owners { // don't fail if an owner already exists checkOwnerAlreadyExists := func(resp *http.Response, o *odata.OData) bool { - if resp.StatusCode == http.StatusBadRequest && o.Error != nil { + if resp.StatusCode == http.StatusBadRequest && o != nil && o.Error != nil { return o.Error.Match(odata.ErrorAddedObjectReferencesAlreadyExist) } return false } - data := struct { - Owner string `json:"@odata.id"` - }{ - Owner: owner, - } - body, err := json.Marshal(data) + body, err := json.Marshal(DirectoryObject{ODataId: owner.ODataId}) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -415,6 +453,7 @@ func (c *ApplicationsClient) AddOwners(ctx context.Context, application *Applica return status, fmt.Errorf("ApplicationsClient.BaseClient.Post(): %v", err) } } + return status, nil } @@ -423,9 +462,11 @@ func (c *ApplicationsClient) AddOwners(ctx context.Context, application *Applica // ownerIds is a *[]string containing object IDs of owners to remove. func (c *ApplicationsClient) RemoveOwners(ctx context.Context, applicationId string, ownerIds *[]string) (int, error) { var status int + if ownerIds == nil { return status, errors.New("cannot remove, nil ownerIds") } + for _, ownerId := range *ownerIds { // check for ownership before attempting deletion if _, status, err := c.GetOwner(ctx, applicationId, ownerId); err != nil { @@ -437,7 +478,7 @@ func (c *ApplicationsClient) RemoveOwners(ctx context.Context, applicationId str // despite the above check, sometimes owners are just gone checkOwnerGone := func(resp *http.Response, o *odata.OData) bool { - if resp.StatusCode == http.StatusBadRequest && o.Error != nil { + if resp.StatusCode == http.StatusBadRequest && o != nil && o.Error != nil { return o.Error.Match(odata.ErrorRemovedObjectReferencesDoNotExist) } return false @@ -457,6 +498,7 @@ func (c *ApplicationsClient) RemoveOwners(ctx context.Context, applicationId str return status, fmt.Errorf("ApplicationsClient.BaseClient.Delete(): %v", err) } } + return status, nil } @@ -473,10 +515,11 @@ func (c *ApplicationsClient) ListExtensions(ctx context.Context, id string, quer if err != nil { return nil, status, fmt.Errorf("ApplicationsClient.BaseClient.List(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } var data struct { @@ -485,16 +528,19 @@ func (c *ApplicationsClient) ListExtensions(ctx context.Context, id string, quer if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.ApplicationExtension, status, nil } // Create creates a new ApplicationExtension. func (c *ApplicationsClient) CreateExtension(ctx context.Context, applicationExtension ApplicationExtension, id string) (*ApplicationExtension, int, error) { var status int + body, err := json.Marshal(applicationExtension) if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ValidStatusCodes: []int{http.StatusCreated}, @@ -506,10 +552,11 @@ func (c *ApplicationsClient) CreateExtension(ctx context.Context, applicationExt if err != nil { return nil, status, fmt.Errorf("ApplicationsClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } var newApplicationExtension ApplicationExtension @@ -533,5 +580,6 @@ func (c *ApplicationsClient) DeleteExtension(ctx context.Context, applicationId, if err != nil { return status, fmt.Errorf("ApplicationsClient.BaseClient.Delete(): %v", err) } + return status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/authentication_methods.go b/vendor/github.com/manicminer/hamilton/msgraph/authentication_methods.go new file mode 100644 index 0000000000..20285c8262 --- /dev/null +++ b/vendor/github.com/manicminer/hamilton/msgraph/authentication_methods.go @@ -0,0 +1,815 @@ +package msgraph + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + + "github.com/manicminer/hamilton/odata" +) + +// AuthenticationMethodsClient performs operations on the Authentications methods endpoint under Identity and Sign-in +type AuthenticationMethodsClient struct { + BaseClient Client +} + +// NewAuthenticationMethodsClient returns a new AuthenticationMethodsClient +func NewAuthenticationMethodsClient(tenantId string) *AuthenticationMethodsClient { + return &AuthenticationMethodsClient{ + BaseClient: NewClient(VersionBeta, tenantId), + } +} + +// List all authentication methods +func (c *AuthenticationMethodsClient) List(ctx context.Context, userID string, query odata.Query) (*[]AuthenticationMethod, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + DisablePaging: query.Top > 0, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/users/%s/authentication/methods", userID), + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("ApplicationsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var data struct { + AuthenticationMethods *[]json.RawMessage `json:"value"` + } + + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + //The graph API returns a mixture of types, this loop matches up the result to the appropriate model + + var ret []AuthenticationMethod + + if data.AuthenticationMethods == nil { + return &ret, status, nil + } + + for _, authMethod := range *data.AuthenticationMethods { + var o odata.OData + if err := json.Unmarshal(authMethod, &o); err != nil { + return nil, status, fmt.Errorf("json.Unmarshall(): %v", err) + } + + if o.Type == nil { + continue + } + switch *o.Type { + case odata.TypeFido2AuthenticationMethod: + var auth Fido2AuthenticationMethod + if err := json.Unmarshal(authMethod, &auth); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + ret = append(ret, auth) + case odata.TypeMicrosoftAuthenticatorAuthenticationMethod: + var auth MicrosoftAuthenticatorAuthenticationMethod + if err := json.Unmarshal(authMethod, &auth); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + ret = append(ret, auth) + case odata.TypeWindowsHelloForBusinessAuthenticationMethod: + var auth WindowsHelloForBusinessAuthenticationMethod + if err := json.Unmarshal(authMethod, &auth); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + ret = append(ret, auth) + case odata.TypeTemporaryAccessPassAuthenticationMethod: + var auth TemporaryAccessPassAuthenticationMethod + if err := json.Unmarshal(authMethod, &auth); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + ret = append(ret, auth) + case odata.TypePhoneAuthenticationMethod: + var auth PhoneAuthenticationMethod + if err := json.Unmarshal(authMethod, &auth); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + ret = append(ret, auth) + case odata.TypeEmailAuthenticationMethod: + var auth EmailAuthenticationMethod + if err := json.Unmarshal(authMethod, &auth); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + ret = append(ret, auth) + case odata.TypePasswordAuthenticationMethod: + var auth PasswordAuthenticationMethod + if err := json.Unmarshal(authMethod, &auth); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + ret = append(ret, auth) + } + } + + return &ret, status, nil +} + +func (c *AuthenticationMethodsClient) ListFido2Methods(ctx context.Context, userID string, query odata.Query) (*[]Fido2AuthenticationMethod, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + DisablePaging: query.Top > 0, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/users/%s/authentication/fido2Methods", userID), + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("AuthenticationMethodsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var data struct { + Fido2Methods []Fido2AuthenticationMethod `json:"value"` + } + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &data.Fido2Methods, status, nil +} + +func (c *AuthenticationMethodsClient) GetFido2Method(ctx context.Context, userID, id string, query odata.Query) (*Fido2AuthenticationMethod, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/users/%s/authentication/fido2Methods/%s", userID, id), + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("AuthenticationMethodsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var fido2Method Fido2AuthenticationMethod + if err := json.Unmarshal(respBody, &fido2Method); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &fido2Method, status, nil +} + +func (c *AuthenticationMethodsClient) DeleteFido2Method(ctx context.Context, userID, id string) (int, error) { + _, status, _, err := c.BaseClient.Delete(ctx, DeleteHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusNoContent}, + Uri: Uri{ + Entity: fmt.Sprintf("/users/%s/authentication/fido2Methods/%s", userID, id), + HasTenantId: true, + }, + }) + if err != nil { + return status, fmt.Errorf("AuthenticationMethodsClient.BaseClient.Delete(): %v", err) + } + + return status, nil +} + +func (c *AuthenticationMethodsClient) ListMicrosoftAuthenticatorMethods(ctx context.Context, userID string, query odata.Query) (*[]MicrosoftAuthenticatorAuthenticationMethod, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + DisablePaging: query.Top > 0, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/users/%s/authentication/microsoftAuthenticatorMethods", userID), + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("AuthenticationMethodsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var data struct { + MicrosoftAuthenticatorMethods []MicrosoftAuthenticatorAuthenticationMethod `json:"value"` + } + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &data.MicrosoftAuthenticatorMethods, status, nil +} + +func (c *AuthenticationMethodsClient) GetMicrosoftAuthenticatorMethod(ctx context.Context, userID, id string, query odata.Query) (*MicrosoftAuthenticatorAuthenticationMethod, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/users/%s/authentication/microsoftAuthenticatorMethods/%s", userID, id), + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("AuthenticationMethodsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var microsoftAuthenticatorMethod MicrosoftAuthenticatorAuthenticationMethod + if err := json.Unmarshal(respBody, µsoftAuthenticatorMethod); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return µsoftAuthenticatorMethod, status, nil +} + +func (c *AuthenticationMethodsClient) DeleteMicrosoftAuthenticatorMethod(ctx context.Context, userID, id string) (int, error) { + _, status, _, err := c.BaseClient.Delete(ctx, DeleteHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusNoContent}, + Uri: Uri{ + Entity: fmt.Sprintf("/users/%s/authentication/microsoftAuthenticatorMethods/%s", userID, id), + HasTenantId: true, + }, + }) + if err != nil { + return status, fmt.Errorf("AuthenticationMethodsClient.BaseClient.Delete(): %v", err) + } + + return status, nil +} + +func (c *AuthenticationMethodsClient) ListWindowsHelloMethods(ctx context.Context, userID string, query odata.Query) (*[]WindowsHelloForBusinessAuthenticationMethod, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + DisablePaging: query.Top > 0, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/users/%s/authentication/windowsHelloForBusinessMethods", userID), + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("AuthenticationMethodsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var data struct { + WindowsHelloForBusinessMethods []WindowsHelloForBusinessAuthenticationMethod `json:"value"` + } + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &data.WindowsHelloForBusinessMethods, status, nil +} + +func (c *AuthenticationMethodsClient) GetWindowsHelloMethod(ctx context.Context, userID, id string, query odata.Query) (*WindowsHelloForBusinessAuthenticationMethod, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/users/%s/authentication/windowsHelloForBusinessMethods/%s", userID, id), + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("AuthenticationMethodsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var windowsHelloForBusinessMethod WindowsHelloForBusinessAuthenticationMethod + if err := json.Unmarshal(respBody, &windowsHelloForBusinessMethod); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &windowsHelloForBusinessMethod, status, nil +} + +func (c *AuthenticationMethodsClient) DeleteWindowsHelloMethod(ctx context.Context, userID, id string) (int, error) { + _, status, _, err := c.BaseClient.Delete(ctx, DeleteHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusNoContent}, + Uri: Uri{ + Entity: fmt.Sprintf("/users/%s/authentication/windowsHelloForBusinessMethods/%s", userID, id), + HasTenantId: true, + }, + }) + if err != nil { + return status, fmt.Errorf("AuthenticationMethodsClient.BaseClient.Delete(): %v", err) + } + + return status, nil +} + +func (c *AuthenticationMethodsClient) ListTemporaryAccessPassMethods(ctx context.Context, userID string, query odata.Query) (*[]TemporaryAccessPassAuthenticationMethod, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + DisablePaging: query.Top > 0, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/users/%s/authentication/temporaryAccessPassMethods", userID), + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("AuthenticationMethodsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var data struct { + TempAccessPassMethods []TemporaryAccessPassAuthenticationMethod `json:"value"` + } + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &data.TempAccessPassMethods, status, nil +} + +func (c *AuthenticationMethodsClient) GetTemporaryAccessPassMethod(ctx context.Context, userID, id string, query odata.Query) (*TemporaryAccessPassAuthenticationMethod, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/users/%s/authentication/temporaryAccessPassMethods/%s", userID, id), + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("AuthenticationMethodsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var temporaryAccessPassMethod TemporaryAccessPassAuthenticationMethod + if err := json.Unmarshal(respBody, &temporaryAccessPassMethod); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &temporaryAccessPassMethod, status, nil +} + +func (c *AuthenticationMethodsClient) CreateTemporaryAccessPassMethod(ctx context.Context, userID string, accessPass TemporaryAccessPassAuthenticationMethod) (*TemporaryAccessPassAuthenticationMethod, int, error) { + var status int + + body, err := json.Marshal(accessPass) + if err != nil { + return nil, status, fmt.Errorf("json.Marshal(): %v", err) + } + + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ + Body: body, + ValidStatusCodes: []int{http.StatusCreated}, + Uri: Uri{ + Entity: fmt.Sprintf("/users/%s/authentication/temporaryAccessPassMethods", userID), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("AuthenticationMethodsClient.BaseClient.Post(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var newTempAccessPassAuthMethod TemporaryAccessPassAuthenticationMethod + if err := json.Unmarshal(respBody, &newTempAccessPassAuthMethod); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &newTempAccessPassAuthMethod, status, nil +} + +func (c *AuthenticationMethodsClient) DeleteTemporaryAccessPassMethod(ctx context.Context, userID, id string) (int, error) { + _, status, _, err := c.BaseClient.Delete(ctx, DeleteHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusNoContent}, + Uri: Uri{ + Entity: fmt.Sprintf("/users/%s/authentication/temporaryAccessPassMethods/%s", userID, id), + HasTenantId: true, + }, + }) + if err != nil { + return status, fmt.Errorf("AuthenticationMethodsClient.BaseClient.Delete(): %v", err) + } + + return status, nil +} + +func (c *AuthenticationMethodsClient) ListPhoneMethods(ctx context.Context, userID string, query odata.Query) (*[]PhoneAuthenticationMethod, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + DisablePaging: query.Top > 0, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/users/%s/authentication/phoneMethods", userID), + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("AuthenticationMethodsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var data struct { + PhoneAuthenticationMethods []PhoneAuthenticationMethod `json:"value"` + } + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &data.PhoneAuthenticationMethods, status, nil +} + +func (c *AuthenticationMethodsClient) GetPhoneMethod(ctx context.Context, userID, id string, query odata.Query) (*PhoneAuthenticationMethod, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/users/%s/authentication/phoneMethods/%s", userID, id), + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("AuthenticationMethodsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var phoneMethod PhoneAuthenticationMethod + if err := json.Unmarshal(respBody, &phoneMethod); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &phoneMethod, status, nil +} + +func (c *AuthenticationMethodsClient) CreatePhoneMethod(ctx context.Context, userID string, phone PhoneAuthenticationMethod) (*PhoneAuthenticationMethod, int, error) { + var status int + + body, err := json.Marshal(phone) + if err != nil { + return nil, status, fmt.Errorf("json.Marshal(): %v", err) + } + + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ + Body: body, + ValidStatusCodes: []int{http.StatusCreated}, + Uri: Uri{ + Entity: fmt.Sprintf("/users/%s/authentication/phoneMethods", userID), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("AuthenticationMethodsClient.BaseClient.Post(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var newPhoneMethod PhoneAuthenticationMethod + if err := json.Unmarshal(respBody, &newPhoneMethod); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &newPhoneMethod, status, nil +} + +func (c *AuthenticationMethodsClient) DeletePhoneMethod(ctx context.Context, userID, id string) (int, error) { + _, status, _, err := c.BaseClient.Delete(ctx, DeleteHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusNoContent}, + Uri: Uri{ + Entity: fmt.Sprintf("/users/%s/authentication/phoneMethods/%s", userID, id), + HasTenantId: true, + }, + }) + if err != nil { + return status, fmt.Errorf("AuthenticationMethodsClient.BaseClient.Delete(): %v", err) + } + + return status, nil +} + +func (c *AuthenticationMethodsClient) UpdatePhoneMethod(ctx context.Context, userID string, phone PhoneAuthenticationMethod) (int, error) { + var status int + + if phone.ID == nil { + return status, errors.New("AuthenticationMethodsClient.Update(): cannot update phone auth method with nil ID") + } + + body, err := json.Marshal(phone) + if err != nil { + return status, fmt.Errorf("json.Marshal(): %v", err) + } + + _, status, _, err = c.BaseClient.Put(ctx, PutHttpRequestInput{ + Body: body, + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/users/%s/authentication/phoneMethods/%s", userID, *phone.ID), + HasTenantId: true, + }, + }) + if err != nil { + return status, fmt.Errorf("AuthenticationMethodsClient.BaseClient.Put(): %v", err) + } + + return status, nil +} + +func (c *AuthenticationMethodsClient) EnablePhoneSMS(ctx context.Context, userID, id string) (int, error) { + var status int + + _, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusNoContent}, + Uri: Uri{ + Entity: fmt.Sprintf("/users/%s/authentication/phoneMethods/%s/enableSmsSignIn", userID, id), + HasTenantId: true, + }, + }) + if err != nil { + return status, fmt.Errorf("AuthenticationMethodsClient.BaseClient.Post(): %v", err) + } + + return status, nil +} + +func (c *AuthenticationMethodsClient) DisablePhoneSMS(ctx context.Context, userID, id string) (int, error) { + var status int + + _, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusNoContent}, + Uri: Uri{ + Entity: fmt.Sprintf("/users/%s/authentication/phoneMethods/%s/disableSmsSignIn", userID, id), + HasTenantId: true, + }, + }) + if err != nil { + return status, fmt.Errorf("AuthenticationMethodsClient.BaseClient.Post(): %v", err) + } + + return status, nil +} + +func (c *AuthenticationMethodsClient) ListEmailMethods(ctx context.Context, userID string, query odata.Query) (*[]EmailAuthenticationMethod, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + DisablePaging: query.Top > 0, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/users/%s/authentication/emailMethods", userID), + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("AuthenticationMethodsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var data struct { + EmailAuthMethods []EmailAuthenticationMethod `json:"value"` + } + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &data.EmailAuthMethods, status, nil +} + +func (c *AuthenticationMethodsClient) GetEmailMethod(ctx context.Context, userID, id string, query odata.Query) (*EmailAuthenticationMethod, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/users/%s/authentication/emailMethods/%s", userID, id), + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("AuthenticationMethodsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var emailMethod EmailAuthenticationMethod + if err := json.Unmarshal(respBody, &emailMethod); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &emailMethod, status, nil +} + +func (c *AuthenticationMethodsClient) UpdateEmailMethod(ctx context.Context, userID string, email EmailAuthenticationMethod) (int, error) { + var status int + + if email.ID == nil { + return status, errors.New("AuthenticationMethodsClient.Update(): cannot update phone auth method with nil ID") + } + + body, err := json.Marshal(email) + if err != nil { + return status, fmt.Errorf("json.Marshal(): %v", err) + } + + _, status, _, err = c.BaseClient.Put(ctx, PutHttpRequestInput{ + Body: body, + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/users/%s/authentication/emailMethods/%s", userID, *email.ID), + HasTenantId: true, + }, + }) + if err != nil { + return status, fmt.Errorf("AuthenticationMethodsClient.BaseClient.Put(): %v", err) + } + + return status, nil +} + +func (c *AuthenticationMethodsClient) DeleteEmailMethod(ctx context.Context, userID, id string) (int, error) { + _, status, _, err := c.BaseClient.Delete(ctx, DeleteHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusNoContent}, + Uri: Uri{ + Entity: fmt.Sprintf("/users/%s/authentication/emailMethods/%s", userID, id), + HasTenantId: true, + }, + }) + if err != nil { + return status, fmt.Errorf("AuthenticationMethodsClient.BaseClient.Delete(): %v", err) + } + + return status, nil +} + +func (c *AuthenticationMethodsClient) CreateEmailMethod(ctx context.Context, userID string, email EmailAuthenticationMethod) (*EmailAuthenticationMethod, int, error) { + var status int + + body, err := json.Marshal(email) + if err != nil { + return nil, status, fmt.Errorf("json.Marshal(): %v", err) + } + + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ + Body: body, + ValidStatusCodes: []int{http.StatusCreated}, + Uri: Uri{ + Entity: fmt.Sprintf("/users/%s/authentication/emailMethods", userID), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("AuthenticationMethodsClient.BaseClient.Post(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var newEmailMethod EmailAuthenticationMethod + if err := json.Unmarshal(respBody, &newEmailMethod); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &newEmailMethod, status, nil +} + +func (c *AuthenticationMethodsClient) ListPasswordMethods(ctx context.Context, userID string, query odata.Query) (*[]PasswordAuthenticationMethod, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + DisablePaging: query.Top > 0, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/users/%s/authentication/passwordMethods", userID), + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("AuthenticationMethodsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var data struct { + PasswordMethods []PasswordAuthenticationMethod `json:"value"` + } + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &data.PasswordMethods, status, nil +} + +func (c *AuthenticationMethodsClient) GetPasswordMethod(ctx context.Context, userID, id string, query odata.Query) (*PasswordAuthenticationMethod, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/users/%s/authentication/passwordMethods/%s", userID, id), + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("AuthenticationMethodsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var passwordMethod PasswordAuthenticationMethod + if err := json.Unmarshal(respBody, &passwordMethod); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &passwordMethod, status, nil +} diff --git a/vendor/github.com/manicminer/hamilton/msgraph/client.go b/vendor/github.com/manicminer/hamilton/msgraph/client.go index 8610bba1de..1d56f08dbc 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/client.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/client.go @@ -35,7 +35,7 @@ type ResponseMiddleware func(*http.Request, *http.Response) (*http.Response, err // RetryOn404ConsistencyFailureFunc can be used to retry a request when a 404 response is received func RetryOn404ConsistencyFailureFunc(resp *http.Response, _ *odata.OData) bool { - return resp.StatusCode == http.StatusNotFound + return resp != nil && resp.StatusCode == http.StatusNotFound } // ValidStatusFunc is a function that tests whether an HTTP response is considered valid for the particular request. @@ -85,7 +85,7 @@ type Client struct { // HttpClient is the underlying http.Client, which by default uses a retryable client HttpClient *http.Client - retryableClient *retryablehttp.Client + RetryableClient *retryablehttp.Client } // NewClient returns a new Client configured with the specified API version and tenant ID. @@ -99,7 +99,7 @@ func NewClient(apiVersion ApiVersion, tenantId string) Client { TenantId: tenantId, UserAgent: "Hamilton (Go-http-client/1.1)", HttpClient: r.StandardClient(), - retryableClient: r, + RetryableClient: r, } } @@ -152,7 +152,7 @@ func (c Client) performRequest(req *http.Request, input HttpRequestInput) (*http } } - c.retryableClient.CheckRetry = func(ctx context.Context, resp *http.Response, err error) (bool, error) { + c.RetryableClient.CheckRetry = func(ctx context.Context, resp *http.Response, err error) (bool, error) { if resp != nil && !c.DisableRetries { if resp.StatusCode == http.StatusFailedDependency { return true, nil diff --git a/vendor/github.com/manicminer/hamilton/msgraph/conditionalaccesspolicy.go b/vendor/github.com/manicminer/hamilton/msgraph/conditionalaccesspolicy.go index 99f95d0c80..0c99d3a2a8 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/conditionalaccesspolicy.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/conditionalaccesspolicy.go @@ -5,7 +5,7 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "net/http" "github.com/manicminer/hamilton/odata" @@ -37,17 +37,20 @@ func (c *ConditionalAccessPolicyClient) List(ctx context.Context, query odata.Qu if err != nil { return nil, status, fmt.Errorf("ConditionalAccessPolicyClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { ConditionalAccessPolicys []ConditionalAccessPolicy `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.ConditionalAccessPolicys, status, nil } @@ -58,6 +61,7 @@ func (c *ConditionalAccessPolicyClient) Create(ctx context.Context, conditionalA if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ValidStatusCodes: []int{http.StatusCreated}, @@ -69,15 +73,18 @@ func (c *ConditionalAccessPolicyClient) Create(ctx context.Context, conditionalA if err != nil { return nil, status, fmt.Errorf("ConditionalAccessPolicyClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var newConditionalAccessPolicy ConditionalAccessPolicy if err := json.Unmarshal(respBody, &newConditionalAccessPolicy); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &newConditionalAccessPolicy, status, nil } @@ -95,21 +102,25 @@ func (c *ConditionalAccessPolicyClient) Get(ctx context.Context, id string, quer if err != nil { return nil, status, fmt.Errorf("ConditionalAccessPolicyClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var conditionalAccessPolicy ConditionalAccessPolicy if err := json.Unmarshal(respBody, &conditionalAccessPolicy); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &conditionalAccessPolicy, status, nil } // Update amends an existing ConditionalAccessPolicy. func (c *ConditionalAccessPolicyClient) Update(ctx context.Context, conditionalAccessPolicy ConditionalAccessPolicy) (int, error) { var status int + if conditionalAccessPolicy.ID == nil { return status, errors.New("cannot update conditionalAccessPolicy with nil ID") } @@ -118,6 +129,7 @@ func (c *ConditionalAccessPolicyClient) Update(ctx context.Context, conditionalA if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Patch(ctx, PatchHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -130,6 +142,7 @@ func (c *ConditionalAccessPolicyClient) Update(ctx context.Context, conditionalA if err != nil { return status, fmt.Errorf("ConditionalAccessPolicyClient.BaseClient.Patch(): %v", err) } + return status, nil } @@ -146,5 +159,6 @@ func (c *ConditionalAccessPolicyClient) Delete(ctx context.Context, id string) ( if err != nil { return status, fmt.Errorf("ConditionalAccessPolicyClient.BaseClient.Delete(): %v", err) } + return status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/directory_audit_reports.go b/vendor/github.com/manicminer/hamilton/msgraph/directory_audit_reports.go index 203b9a2fe1..38cb78b02b 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/directory_audit_reports.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/directory_audit_reports.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "github.com/manicminer/hamilton/odata" @@ -36,17 +36,20 @@ func (c *DirectoryAuditReportsClient) List(ctx context.Context, query odata.Quer if err != nil { return nil, status, fmt.Errorf("DirectoryAuditReportsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { DirectoryAuditReports []DirectoryAudit `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.DirectoryAuditReports, status, nil } @@ -64,14 +67,17 @@ func (c *DirectoryAuditReportsClient) Get(ctx context.Context, id string, query if err != nil { return nil, status, fmt.Errorf("DirectoryAuditReportsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var directoryAuditReport DirectoryAudit if err := json.Unmarshal(respBody, &directoryAuditReport); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &directoryAuditReport, status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/directory_objects.go b/vendor/github.com/manicminer/hamilton/msgraph/directory_objects.go new file mode 100644 index 0000000000..f9183fd7cf --- /dev/null +++ b/vendor/github.com/manicminer/hamilton/msgraph/directory_objects.go @@ -0,0 +1,210 @@ +package msgraph + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/manicminer/hamilton/internal/utils" + "github.com/manicminer/hamilton/odata" +) + +// DirectoryObjectsClient performs operations on Directory Objects (the base type for other objects such as users and groups) +type DirectoryObjectsClient struct { + BaseClient Client +} + +// NewDirectoryObjectsClient returns a new DirectoryObjectsClient. +func NewDirectoryObjectsClient(tenantId string) *DirectoryObjectsClient { + return &DirectoryObjectsClient{ + BaseClient: NewClient(Version10, tenantId), + } +} + +// Get retrieves a DirectoryObject. +func (c *DirectoryObjectsClient) Get(ctx context.Context, id string, query odata.Query) (*DirectoryObject, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/directoryObjects/%s", id), + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("DirectoryObjects.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var directoryObject DirectoryObject + if err := json.Unmarshal(respBody, &directoryObject); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &directoryObject, status, nil +} + +// GetByIds retrieves multiple DirectoryObjects from a list of IDs. +func (c *DirectoryObjectsClient) GetByIds(ctx context.Context, ids []string, types []odata.ShortType) (*[]DirectoryObject, int, error) { + var status int + + body, err := json.Marshal(struct { + IDs []string `json:"ids"` + Types []odata.Type `json:"types"` + }{ + IDs: ids, + Types: types, + }) + if err != nil { + return nil, status, fmt.Errorf("json.Marshal(): %v", err) + } + + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ + Body: body, + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: "/directoryObjects/getByIds", + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("DirectoryObjects.BaseClient.Post(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var data struct { + Objects []DirectoryObject `json:"value"` + } + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &data.Objects, status, nil +} + +// Delete removes a DirectoryObject. +func (c *DirectoryObjectsClient) Delete(ctx context.Context, id string) (int, error) { + _, status, _, err := c.BaseClient.Delete(ctx, DeleteHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusNoContent}, + Uri: Uri{ + Entity: fmt.Sprintf("/directoryObjects/%s", id), + HasTenantId: true, + }, + }) + if err != nil { + return status, fmt.Errorf("DirectoryObjects.BaseClient.Get(): %v", err) + } + + return status, nil +} + +// GetMemberGroups retrieves IDs of the groups and directory roles that a directory object is a member of. +// id is the object ID of the directory object. +func (c *DirectoryObjectsClient) GetMemberGroups(ctx context.Context, id string, securityEnabledOnly bool) (*[]DirectoryObject, int, error) { + var status int + + body, err := json.Marshal(struct { + SecurityEnabledOnly bool `json:"securityEnabledOnly"` + }{ + SecurityEnabledOnly: securityEnabledOnly, + }) + if err != nil { + return nil, status, fmt.Errorf("json.Marshal(): %v", err) + } + + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ + Body: body, + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/directoryObjects/%s/getMemberGroups", id), + HasTenantId: false, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("DirectoryObjectsClient.BaseClient.Post(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var data struct { + IDs []string `json:"value"` + } + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + result := make([]DirectoryObject, len(data.IDs)) + for i, id := range data.IDs { + result[i].ID = utils.StringPtr(id) + } + + return &result, status, nil +} + +// GetMemberObjects retrieves IDs of the groups and directory roles that a directory object is a member of. +// id is the object ID of the directory object. +func (c *DirectoryObjectsClient) GetMemberObjects(ctx context.Context, id string, securityEnabledOnly bool) (*[]DirectoryObject, int, error) { + var status int + + body, err := json.Marshal(struct { + SecurityEnabledOnly bool `json:"securityEnabledOnly"` + }{ + SecurityEnabledOnly: securityEnabledOnly, + }) + if err != nil { + return nil, status, fmt.Errorf("json.Marshal(): %v", err) + } + + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ + Body: body, + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/directoryObjects/%s/getMemberObjects", id), + HasTenantId: false, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("DirectoryObjectsClient.BaseClient.Post(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var data struct { + IDs []string `json:"value"` + } + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + result := make([]DirectoryObject, len(data.IDs)) + for i, id := range data.IDs { + result[i].ID = utils.StringPtr(id) + } + + return &result, status, nil +} diff --git a/vendor/github.com/manicminer/hamilton/msgraph/directory_role_templates.go b/vendor/github.com/manicminer/hamilton/msgraph/directory_role_templates.go index cf9c7464a0..ab95032b4d 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/directory_role_templates.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/directory_role_templates.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" ) @@ -32,17 +32,20 @@ func (c *DirectoryRoleTemplatesClient) List(ctx context.Context) (*[]DirectoryRo if err != nil { return nil, status, fmt.Errorf("DirectoryRoleTemplatesClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { DirectoryRoleTemplates []DirectoryRoleTemplate `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.DirectoryRoleTemplates, status, nil } @@ -59,14 +62,17 @@ func (c *DirectoryRoleTemplatesClient) Get(ctx context.Context, id string) (*Dir if err != nil { return nil, status, fmt.Errorf("DirectoryRoleTemplatesClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var dirRoleTemplate DirectoryRoleTemplate if err := json.Unmarshal(respBody, &dirRoleTemplate); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &dirRoleTemplate, status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/directory_roles.go b/vendor/github.com/manicminer/hamilton/msgraph/directory_roles.go index 1a9c9c9d98..5612da1d50 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/directory_roles.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/directory_roles.go @@ -5,7 +5,7 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "net/http" "github.com/manicminer/hamilton/odata" @@ -35,17 +35,20 @@ func (c *DirectoryRolesClient) List(ctx context.Context) (*[]DirectoryRole, int, if err != nil { return nil, status, fmt.Errorf("DirectoryRolesClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { DirectoryRoles []DirectoryRole `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.DirectoryRoles, status, nil } @@ -61,15 +64,18 @@ func (c *DirectoryRolesClient) Get(ctx context.Context, id string) (*DirectoryRo if err != nil { return nil, status, fmt.Errorf("DirectoryRolesClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var dirRole DirectoryRole if err := json.Unmarshal(respBody, &dirRole); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &dirRole, status, nil } @@ -87,11 +93,13 @@ func (c *DirectoryRolesClient) ListMembers(ctx context.Context, id string) (*[]s if err != nil { return nil, status, fmt.Errorf("DirectoryRolesClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Members []struct { Type string `json:"@odata.type"` @@ -101,43 +109,45 @@ func (c *DirectoryRolesClient) ListMembers(ctx context.Context, id string) (*[]s if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + ret := make([]string, len(data.Members)) for i, v := range data.Members { ret[i] = v.Id } + return &ret, status, nil } -// AddMembers adds a new member to a Directory Role. -// First populate the Members field of the DirectoryRole using the AppendMember method of the model, then call this method. +// AddMembers adds new members to a Directory Role. +// First populate the `members` field, then call this method func (c *DirectoryRolesClient) AddMembers(ctx context.Context, directoryRole *DirectoryRole) (int, error) { var status int + if directoryRole.ID == nil { return status, errors.New("cannot update directory role with nil ID") } if directoryRole.Members == nil { return status, errors.New("cannot update directory role with nil Owners") } + for _, member := range *directoryRole.Members { // don't fail if a member already exists checkMemberAlreadyExists := func(resp *http.Response, o *odata.OData) bool { - if resp.StatusCode == http.StatusBadRequest { - if o.Error != nil { - return o.Error.Match(odata.ErrorAddedObjectReferencesAlreadyExist) - } + if resp.StatusCode == http.StatusBadRequest && o != nil && o.Error != nil { + return o.Error.Match(odata.ErrorAddedObjectReferencesAlreadyExist) } return false } - data := struct { - Member string `json:"@odata.id"` + body, err := json.Marshal(struct { + Member odata.Id `json:"@odata.id"` }{ - Member: member, - } - body, err := json.Marshal(data) + Member: *member.ODataId, + }) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ValidStatusCodes: []int{http.StatusNoContent}, @@ -151,6 +161,7 @@ func (c *DirectoryRolesClient) AddMembers(ctx context.Context, directoryRole *Di return status, fmt.Errorf("DirectoryRolesClient.BaseClient.Post(): %v", err) } } + return status, nil } @@ -159,9 +170,11 @@ func (c *DirectoryRolesClient) AddMembers(ctx context.Context, directoryRole *Di // memberIds is a *[]string containing object IDs of members to remove. func (c *DirectoryRolesClient) RemoveMembers(ctx context.Context, directoryRoleId string, memberIds *[]string) (int, error) { var status int + if memberIds == nil { return status, errors.New("cannot remove, nil memberIds") } + for _, memberId := range *memberIds { // check for membership before attempting deletion if _, status, err := c.GetMember(ctx, directoryRoleId, memberId); err != nil { @@ -170,6 +183,7 @@ func (c *DirectoryRolesClient) RemoveMembers(ctx context.Context, directoryRoleI } return status, err } + var err error _, status, _, err = c.BaseClient.Delete(ctx, DeleteHttpRequestInput{ ValidStatusCodes: []int{http.StatusNoContent}, @@ -182,6 +196,7 @@ func (c *DirectoryRolesClient) RemoveMembers(ctx context.Context, directoryRoleI return status, fmt.Errorf("DirectoryRolesClient.BaseClient.Delete(): %v", err) } } + return status, nil } @@ -200,11 +215,13 @@ func (c *DirectoryRolesClient) GetMember(ctx context.Context, directoryRoleId, m if err != nil { return nil, status, fmt.Errorf("DirectoryRolesClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Context string `json:"@odata.context"` Type string `json:"@odata.type"` @@ -214,6 +231,7 @@ func (c *DirectoryRolesClient) GetMember(ctx context.Context, directoryRoleId, m if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.Id, status, nil } @@ -224,10 +242,8 @@ func (c *DirectoryRolesClient) Activate(ctx context.Context, roleTemplateID stri // don't fail if a role is already activated checkRoleAlreadyActivated := func(resp *http.Response, o *odata.OData) bool { - if resp.StatusCode == http.StatusBadRequest { - if o.Error != nil { - return o.Error.Match(odata.ErrorConflictingObjectPresentInDirectory) - } + if resp.StatusCode == http.StatusBadRequest && o != nil && o.Error != nil { + return o.Error.Match(odata.ErrorConflictingObjectPresentInDirectory) } return false } @@ -254,14 +270,17 @@ func (c *DirectoryRolesClient) Activate(ctx context.Context, roleTemplateID stri if err != nil { return nil, status, fmt.Errorf("DirectoryRolesClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var newDirRole DirectoryRole if err := json.Unmarshal(respBody, &newDirRole); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &newDirRole, status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/domains.go b/vendor/github.com/manicminer/hamilton/msgraph/domains.go index 35caff51a6..7a14c78b8f 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/domains.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/domains.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "github.com/manicminer/hamilton/odata" @@ -33,21 +33,19 @@ func (c *DomainsClient) List(ctx context.Context, query odata.Query) (*[]Domain, HasTenantId: true, }, }) - if err != nil { return nil, status, fmt.Errorf("DomainsClient.BaseClient.Get(): %v", err) } defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } var data struct { Domains []Domain `json:"value"` } - if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } @@ -58,6 +56,7 @@ func (c *DomainsClient) List(ctx context.Context, query odata.Query) (*[]Domain, // Get retrieves a Domain. func (c *DomainsClient) Get(ctx context.Context, id string, query odata.Query) (*Domain, int, error) { var status int + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, ValidStatusCodes: []int{http.StatusOK}, @@ -70,14 +69,17 @@ func (c *DomainsClient) Get(ctx context.Context, id string, query odata.Query) ( if err != nil { return nil, status, fmt.Errorf("DomainsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var domain Domain if err := json.Unmarshal(respBody, &domain); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &domain, status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/groups.go b/vendor/github.com/manicminer/hamilton/msgraph/groups.go index a2d1f6ebcd..816ee9f194 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/groups.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/groups.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "github.com/manicminer/hamilton/odata" @@ -36,30 +36,40 @@ func (c *GroupsClient) List(ctx context.Context, query odata.Query) (*[]Group, i if err != nil { return nil, status, fmt.Errorf("GroupsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Groups []Group `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.Groups, status, nil } // Create creates a new Group. func (c *GroupsClient) Create(ctx context.Context, group Group) (*Group, int, error) { var status int + body, err := json.Marshal(group) if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + + ownersNotReplicated := func(resp *http.Response, o *odata.OData) bool { + return o != nil && o.Error != nil && o.Error.Match(odata.ErrorResourceDoesNotExist) + } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ - Body: body, - ValidStatusCodes: []int{http.StatusCreated}, + Body: body, + ConsistencyFailureFunc: ownersNotReplicated, + ValidStatusCodes: []int{http.StatusCreated}, Uri: Uri{ Entity: "/groups", HasTenantId: true, @@ -68,15 +78,18 @@ func (c *GroupsClient) Create(ctx context.Context, group Group) (*Group, int, er if err != nil { return nil, status, fmt.Errorf("GroupsClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var newGroup Group if err := json.Unmarshal(respBody, &newGroup); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &newGroup, status, nil } @@ -94,15 +107,18 @@ func (c *GroupsClient) Get(ctx context.Context, id string, query odata.Query) (* if err != nil { return nil, status, fmt.Errorf("GroupsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var group Group if err := json.Unmarshal(respBody, &group); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &group, status, nil } @@ -136,16 +152,18 @@ func (c *GroupsClient) GetWithSchemaExtensions(ctx context.Context, id string, q if err != nil { return nil, status, fmt.Errorf("GroupsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } group.SchemaExtensions = schemaExtensions if err := json.Unmarshal(respBody, group); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return group, status, nil } @@ -163,25 +181,30 @@ func (c *GroupsClient) GetDeleted(ctx context.Context, id string, query odata.Qu if err != nil { return nil, status, fmt.Errorf("GroupsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var group Group if err := json.Unmarshal(respBody, &group); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &group, status, nil } // Update amends an existing Group. func (c *GroupsClient) Update(ctx context.Context, group Group) (int, error) { var status int + body, err := json.Marshal(group) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Patch(ctx, PatchHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -194,6 +217,7 @@ func (c *GroupsClient) Update(ctx context.Context, group Group) (int, error) { if err != nil { return status, fmt.Errorf("GroupsClient.BaseClient.Patch(): %v", err) } + return status, nil } @@ -210,6 +234,7 @@ func (c *GroupsClient) Delete(ctx context.Context, id string) (int, error) { if err != nil { return status, fmt.Errorf("GroupsClient.BaseClient.Delete(): %v", err) } + return status, nil } @@ -226,6 +251,7 @@ func (c *GroupsClient) DeletePermanently(ctx context.Context, id string) (int, e if err != nil { return status, fmt.Errorf("GroupsClient.BaseClient.Delete(): %v", err) } + return status, nil } @@ -243,14 +269,16 @@ func (c *GroupsClient) ListDeleted(ctx context.Context, query odata.Query) (*[]G if err != nil { return nil, status, err } + defer resp.Body.Close() - respBody, _ := ioutil.ReadAll(resp.Body) + respBody, _ := io.ReadAll(resp.Body) var data struct { DeletedGroups []Group `json:"value"` } if err = json.Unmarshal(respBody, &data); err != nil { return nil, status, err } + return &data.DeletedGroups, status, nil } @@ -267,15 +295,18 @@ func (c *GroupsClient) RestoreDeleted(ctx context.Context, id string) (*Group, i if err != nil { return nil, status, fmt.Errorf("GroupsClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var restoredGroup Group if err = json.Unmarshal(respBody, &restoredGroup); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &restoredGroup, status, nil } @@ -294,11 +325,13 @@ func (c *GroupsClient) ListMembers(ctx context.Context, id string) (*[]string, i if err != nil { return nil, status, fmt.Errorf("GroupsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Members []struct { Type string `json:"@odata.type"` @@ -308,10 +341,12 @@ func (c *GroupsClient) ListMembers(ctx context.Context, id string) (*[]string, i if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + ret := make([]string, len(data.Members)) for i, v := range data.Members { ret[i] = v.Id } + return &ret, status, nil } @@ -331,11 +366,13 @@ func (c *GroupsClient) GetMember(ctx context.Context, groupId, memberId string) if err != nil { return nil, status, fmt.Errorf("GroupsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Context string `json:"@odata.context"` Type string `json:"@odata.type"` @@ -345,58 +382,52 @@ func (c *GroupsClient) GetMember(ctx context.Context, groupId, memberId string) if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.Id, status, nil } -// AddMembers adds a new member to a Group. -// First populate the Members field of the Group using the AppendMember method of the model, then call this method. +// AddMembers adds new members to a Group. +// First populate the `members` field, then call this method func (c *GroupsClient) AddMembers(ctx context.Context, group *Group) (int, error) { var status int - // Patching group members support up to 20 members per request - var memberChunks [][]string + if group.Members == nil || len(*group.Members) == 0 { return status, fmt.Errorf("no members specified") } - members := *group.Members - max := len(members) - // Chunk into slices of 20 for batching - for i := 0; i < max; i += 20 { - end := i + 20 - if end > max { - end = max - } - memberChunks = append(memberChunks, members[i:end]) - } - for _, members := range memberChunks { - // don't fail if a member already exists + + for _, member := range *group.Members { + // don't fail if an member already exists checkMemberAlreadyExists := func(resp *http.Response, o *odata.OData) bool { - if resp.StatusCode == http.StatusBadRequest && o.Error != nil { + if resp.StatusCode == http.StatusBadRequest && o != nil && o.Error != nil { return o.Error.Match(odata.ErrorAddedObjectReferencesAlreadyExist) } return false } - data := Group{ - Members: &members, - } - body, err := json.Marshal(data) + body, err := json.Marshal(struct { + Member odata.Id `json:"@odata.id"` + }{ + Member: *member.ODataId, + }) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } - _, status, _, err = c.BaseClient.Patch(ctx, PatchHttpRequestInput{ + + _, status, _, err = c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, ValidStatusCodes: []int{http.StatusNoContent}, ValidStatusFunc: checkMemberAlreadyExists, Uri: Uri{ - Entity: fmt.Sprintf("/groups/%s", *group.ID), + Entity: fmt.Sprintf("/groups/%s/members/$ref", *group.ID), HasTenantId: true, }, }) if err != nil { - return status, fmt.Errorf("GroupsClient.BaseClient.Patch(): %v", err) + return status, fmt.Errorf("GroupsClient.BaseClient.Post(): %v", err) } } + return status, nil } @@ -405,9 +436,11 @@ func (c *GroupsClient) AddMembers(ctx context.Context, group *Group) (int, error // memberIds is a *[]string containing object IDs of members to remove. func (c *GroupsClient) RemoveMembers(ctx context.Context, id string, memberIds *[]string) (int, error) { var status int + if memberIds == nil || len(*memberIds) == 0 { return status, fmt.Errorf("no members specified") } + for _, memberId := range *memberIds { // check for membership before attempting deletion if _, status, err := c.GetMember(ctx, id, memberId); err != nil { @@ -419,7 +452,7 @@ func (c *GroupsClient) RemoveMembers(ctx context.Context, id string, memberIds * // despite the above check, sometimes members are just gone checkMemberGone := func(resp *http.Response, o *odata.OData) bool { - if resp.StatusCode == http.StatusBadRequest && o.Error != nil { + if resp.StatusCode == http.StatusBadRequest && o != nil && o.Error != nil { return o.Error.Match(odata.ErrorRemovedObjectReferencesDoNotExist) } return false @@ -439,6 +472,7 @@ func (c *GroupsClient) RemoveMembers(ctx context.Context, id string, memberIds * return status, fmt.Errorf("GroupsClient.BaseClient.Delete(): %v", err) } } + return status, nil } @@ -457,11 +491,13 @@ func (c *GroupsClient) ListOwners(ctx context.Context, id string) (*[]string, in if err != nil { return nil, status, fmt.Errorf("GroupsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Owners []struct { Type string `json:"@odata.type"` @@ -471,10 +507,12 @@ func (c *GroupsClient) ListOwners(ctx context.Context, id string) (*[]string, in if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + ret := make([]string, len(data.Owners)) for i, v := range data.Owners { ret[i] = v.Id } + return &ret, status, nil } @@ -494,11 +532,13 @@ func (c *GroupsClient) GetOwner(ctx context.Context, groupId, ownerId string) (* if err != nil { return nil, status, fmt.Errorf("GroupsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Context string `json:"@odata.context"` Type string `json:"@odata.type"` @@ -508,34 +548,37 @@ func (c *GroupsClient) GetOwner(ctx context.Context, groupId, ownerId string) (* if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.Id, status, nil } -// AddOwners adds a new owner to a Group. -// First populate the Owners field of the Group using the AppendOwner method of the model, then call this method. +// AddOwners adds new owners to a Group. +// First populate the `owners` field, then call this method func (c *GroupsClient) AddOwners(ctx context.Context, group *Group) (int, error) { var status int + if group.Owners == nil || len(*group.Owners) == 0 { return status, fmt.Errorf("no owners specified") } + for _, owner := range *group.Owners { // don't fail if an owner already exists checkOwnerAlreadyExists := func(resp *http.Response, o *odata.OData) bool { - if resp.StatusCode == http.StatusBadRequest && o.Error != nil { + if resp.StatusCode == http.StatusBadRequest && o != nil && o.Error != nil { return o.Error.Match(odata.ErrorAddedObjectReferencesAlreadyExist) } return false } - data := struct { - Owner string `json:"@odata.id"` + body, err := json.Marshal(struct { + Owner odata.Id `json:"@odata.id"` }{ - Owner: owner, - } - body, err := json.Marshal(data) + Owner: *owner.ODataId, + }) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -550,6 +593,7 @@ func (c *GroupsClient) AddOwners(ctx context.Context, group *Group) (int, error) return status, fmt.Errorf("GroupsClient.BaseClient.Post(): %v", err) } } + return status, nil } @@ -558,9 +602,11 @@ func (c *GroupsClient) AddOwners(ctx context.Context, group *Group) (int, error) // ownerIds is a *[]string containing object IDs of owners to remove. func (c *GroupsClient) RemoveOwners(ctx context.Context, id string, ownerIds *[]string) (int, error) { var status int + if ownerIds == nil || len(*ownerIds) == 0 { return status, fmt.Errorf("no owners specified") } + for _, ownerId := range *ownerIds { // check for ownership before attempting deletion if _, status, err := c.GetOwner(ctx, id, ownerId); err != nil { @@ -572,7 +618,7 @@ func (c *GroupsClient) RemoveOwners(ctx context.Context, id string, ownerIds *[] // despite the above check, sometimes owners are just gone checkOwnerGone := func(resp *http.Response, o *odata.OData) bool { - if resp.StatusCode == http.StatusBadRequest && o.Error != nil { + if resp.StatusCode == http.StatusBadRequest && o != nil && o.Error != nil { return o.Error.Match(odata.ErrorRemovedObjectReferencesDoNotExist) } return false @@ -592,5 +638,6 @@ func (c *GroupsClient) RemoveOwners(ctx context.Context, id string, ownerIds *[] return status, fmt.Errorf("GroupsClient.BaseClient.Delete(): %v", err) } } + return status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/identity_providers.go b/vendor/github.com/manicminer/hamilton/msgraph/identity_providers.go index 79652e9174..734cc747a4 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/identity_providers.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/identity_providers.go @@ -5,7 +5,7 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "net/http" ) @@ -33,27 +33,32 @@ func (c *IdentityProvidersClient) List(ctx context.Context) (*[]IdentityProvider if err != nil { return nil, status, fmt.Errorf("IdentityProvidersClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { IdentityProviders []IdentityProvider `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.IdentityProviders, status, nil } // Create creates a new IdentityProvider. func (c *IdentityProvidersClient) Create(ctx context.Context, provider IdentityProvider) (*IdentityProvider, int, error) { var status int + body, err := json.Marshal(provider) if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ValidStatusCodes: []int{http.StatusCreated}, @@ -65,15 +70,18 @@ func (c *IdentityProvidersClient) Create(ctx context.Context, provider IdentityP if err != nil { return nil, status, fmt.Errorf("IdentityProvidersClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var newProvider IdentityProvider if err := json.Unmarshal(respBody, &newProvider); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &newProvider, status, nil } @@ -90,28 +98,34 @@ func (c *IdentityProvidersClient) Get(ctx context.Context, id string) (*Identity if err != nil { return nil, status, fmt.Errorf("IdentityProvidersClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var provider IdentityProvider if err := json.Unmarshal(respBody, &provider); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &provider, status, nil } // Update amends an existing IdentityProvider. func (c *IdentityProvidersClient) Update(ctx context.Context, provider IdentityProvider) (int, error) { var status int + if provider.ID == nil { return status, errors.New("IdentityProvidersClient.Update(): cannot update identity provider with nil ID") } + body, err := json.Marshal(provider) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Patch(ctx, PatchHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -124,6 +138,7 @@ func (c *IdentityProvidersClient) Update(ctx context.Context, provider IdentityP if err != nil { return status, fmt.Errorf("IdentityProvidersClient.BaseClient.Patch(): %v", err) } + return status, nil } @@ -140,6 +155,7 @@ func (c *IdentityProvidersClient) Delete(ctx context.Context, id string) (int, e if err != nil { return status, fmt.Errorf("IdentityProvidersClient.BaseClient.Delete(): %v", err) } + return status, nil } @@ -155,16 +171,19 @@ func (c *IdentityProvidersClient) ListAvailableProviderTypes(ctx context.Context if err != nil { return nil, status, fmt.Errorf("IdentityProvidersClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { IdentityProviderTypes []string `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.IdentityProviderTypes, status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/invitations.go b/vendor/github.com/manicminer/hamilton/msgraph/invitations.go index 26b2ddd439..82859f00e4 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/invitations.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/invitations.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" ) @@ -23,10 +23,12 @@ func NewInvitationsClient(tenantId string) *InvitationsClient { // Create creates a new Invitation. func (c *InvitationsClient) Create(ctx context.Context, invitation Invitation) (*Invitation, int, error) { var status int + body, err := json.Marshal(invitation) if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ValidStatusCodes: []int{http.StatusCreated}, @@ -38,14 +40,17 @@ func (c *InvitationsClient) Create(ctx context.Context, invitation Invitation) ( if err != nil { return nil, status, fmt.Errorf("InvitationsClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var newInvitation Invitation if err := json.Unmarshal(respBody, &newInvitation); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &newInvitation, status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/me.go b/vendor/github.com/manicminer/hamilton/msgraph/me.go index 7e3903238e..40b3970f8b 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/me.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/me.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "github.com/manicminer/hamilton/odata" @@ -25,6 +25,7 @@ func NewMeClient(tenantId string) *MeClient { // Get retrieves information about the authenticated user. func (c *MeClient) Get(ctx context.Context, query odata.Query) (*Me, int, error) { var status int + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ ValidStatusCodes: []int{http.StatusOK}, Uri: Uri{ @@ -36,21 +37,25 @@ func (c *MeClient) Get(ctx context.Context, query odata.Query) (*Me, int, error) if err != nil { return nil, status, fmt.Errorf("MeClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var me Me if err := json.Unmarshal(respBody, &me); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &me, status, nil } // GetProfile retrieves the profile of the authenticated user. func (c *MeClient) GetProfile(ctx context.Context, query odata.Query) (*Me, int, error) { var status int + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ ValidStatusCodes: []int{http.StatusOK}, Uri: Uri{ @@ -62,15 +67,18 @@ func (c *MeClient) GetProfile(ctx context.Context, query odata.Query) (*Me, int, if err != nil { return nil, status, fmt.Errorf("MeClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var me Me if err := json.Unmarshal(respBody, &me); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &me, status, nil } @@ -78,10 +86,12 @@ func (c *MeClient) GetProfile(ctx context.Context, query odata.Query) (*Me, int, // TODO: Needs testing with an O365 user principal func (c *MeClient) Sendmail(ctx context.Context, message MailMessage) (int, error) { var status int + body, err := json.Marshal(message) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ValidStatusCodes: []int{http.StatusOK, http.StatusAccepted}, @@ -93,5 +103,6 @@ func (c *MeClient) Sendmail(ctx context.Context, message MailMessage) (int, erro if err != nil { return status, fmt.Errorf("MeClient.BaseClient.Post(): %v", err) } + return status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/models.go b/vendor/github.com/manicminer/hamilton/msgraph/models.go index 2f648daf0d..e3086041e8 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/models.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/models.go @@ -7,6 +7,8 @@ import ( "strings" "time" + "github.com/manicminer/hamilton/odata" + "github.com/manicminer/hamilton/environments" "github.com/manicminer/hamilton/errors" ) @@ -36,7 +38,9 @@ type AppIdentity struct { // Application describes an Application object. type Application struct { - ID *string `json:"id,omitempty"` + DirectoryObject + Owners *Owners `json:"owners@odata.bind,omitempty"` + AddIns *[]AddIn `json:"addIns,omitempty"` Api *ApplicationApi `json:"api,omitempty"` AppId *string `json:"appId,omitempty"` @@ -46,7 +50,7 @@ type Application struct { DeletedDateTime *time.Time `json:"deletedDateTime,omitempty"` DisabledByMicrosoftStatus interface{} `json:"disabledByMicrosoftStatus,omitempty"` DisplayName *string `json:"displayName,omitempty"` - GroupMembershipClaims *[]GroupMembershipClaim `json:"groupMembershipClaims,omitempty"` + GroupMembershipClaims *[]GroupMembershipClaim `json:"-"` // see Application.MarshalJSON / Application.UnmarshalJSON IdentifierUris *[]string `json:"identifierUris,omitempty"` Info *InformationalUrl `json:"info,omitempty"` IsAuthorizationServiceEnabled *bool `json:"isAuthorizationServiceEnabled,omitempty"` @@ -69,8 +73,6 @@ type Application struct { UniqueName *string `json:"uniqueName,omitempty"` VerifiedPublisher *VerifiedPublisher `json:"verifiedPublisher,omitempty"` Web *ApplicationWeb `json:"web,omitempty"` - - Owners *[]string `json:"owners@odata.bind,omitempty"` } func (a Application) MarshalJSON() ([]byte, error) { @@ -83,25 +85,30 @@ func (a Application) MarshalJSON() ([]byte, error) { theClaims := StringNullWhenEmpty(strings.Join(claims, ",")) val = &theClaims } + + // Local type needed to avoid recursive MarshalJSON calls type application Application - return json.Marshal(&struct { + app := struct { GroupMembershipClaims *StringNullWhenEmpty `json:"groupMembershipClaims,omitempty"` *application }{ GroupMembershipClaims: val, application: (*application)(&a), - }) + } + buf, err := json.Marshal(&app) + return buf, err } func (a *Application) UnmarshalJSON(data []byte) error { + // Local type needed to avoid recursive UnmarshalJSON calls type application Application - app := &struct { + app := struct { GroupMembershipClaims *string `json:"groupMembershipClaims"` *application }{ application: (*application)(a), } - if err := json.Unmarshal(data, app); err != nil { + if err := json.Unmarshal(data, &app); err != nil { return err } if app.GroupMembershipClaims != nil { @@ -114,17 +121,6 @@ func (a *Application) UnmarshalJSON(data []byte) error { return nil } -// AppendOwner appends a new owner object URI to the Owners slice. -func (a *Application) AppendOwner(endpoint environments.ApiEndpoint, apiVersion ApiVersion, id string) { - val := fmt.Sprintf("%s/%s/directoryObjects/%s", endpoint, apiVersion, id) - var owners []string - if a.Owners != nil { - owners = *a.Owners - } - owners = append(owners, val) - a.Owners = &owners -} - // AppendAppRole adds a new AppRole to an Application, checking to see if it already exists. func (a *Application) AppendAppRole(role AppRole) error { if role.ID == nil { @@ -336,12 +332,14 @@ type AuditActivityInitiator struct { User *UserIdentity `json:"user,omitempty"` } +type AuthenticationMethod interface{} + type BaseNamedLocation struct { - ODataType *string `json:"@odata.type,omitempty"` - ID *string `json:"id,omitempty"` - DisplayName *string `json:"displayName,omitempty"` - CreatedDateTime *time.Time `json:"createdDateTime,omitempty"` - ModifiedDateTime *time.Time `json:"modifiedDateTime,omitempty"` + ODataType *odata.Type `json:"@odata.type,omitempty"` + ID *string `json:"id,omitempty"` + DisplayName *string `json:"displayName,omitempty"` + CreatedDateTime *time.Time `json:"createdDateTime,omitempty"` + ModifiedDateTime *time.Time `json:"modifiedDateTime,omitempty"` } type CloudAppSecurityControl struct { @@ -417,6 +415,30 @@ type CountryNamedLocation struct { IncludeUnknownCountriesAndRegions *bool `json:"includeUnknownCountriesAndRegions,omitempty"` } +type CredentialUserRegistrationCount struct { + ID *string `json:"id,omitempty"` + TotalUserCount *int64 `json:"totalUserCount,omitempty"` + UserRegistrationCounts *[]UserRegistrationCount `json:"userRegistrationCounts,omitempty"` +} + +type CredentialUsageSummary struct { + AuthMethod *UsageAuthMethod `json:"usageAuthMethod,omitempty"` + FailureActivityCount *int64 `json:"failureActivityCount,omitempty"` + Feature *FeatureType `json:"feature,omitempty"` + ID *string `json:"id,omitempty"` + SuccessfulActivityCount *int64 `json:"successfulActivityCount,omitempty"` +} +type CredentialUserRegistrationDetails struct { + AuthMethods *[]RegistrationAuthMethod `json:"authMethods,omitempty"` + ID *string `json:"id,omitempty"` + IsCapable *bool `json:"isCapable,omitempty"` + IsEnabled *bool `json:"isEnabled,omitempty"` + IsMfaRegistered *bool `json:"isMfaRegistered,omitempty"` + IsRegistered *bool `json:"isRegistered,omitempty"` + UserDisplayName *string `json:"userDisplayName,omitempty"` + UserPrincipalName *string `json:"UserPrincipalName,omitempty"` +} + type DeviceDetail struct { Browser *string `json:"browser,omitempty"` DeviceId *string `json:"deviceId,omitempty"` @@ -441,24 +463,36 @@ type DirectoryAudit struct { TargetResources *[]TargetResource `json:"targetResources,omitempty"` } +type DirectoryObject struct { + ODataId *odata.Id `json:"@odata.id,omitempty"` + ODataType *odata.Type `json:"@odata.type,omitempty"` + ID *string `json:"id,omitempty"` +} + +func (o *DirectoryObject) Uri(endpoint environments.ApiEndpoint, apiVersion ApiVersion) string { + if o.ID == nil { + return "" + } + return fmt.Sprintf("%s/%s/directoryObjects/%s", endpoint, apiVersion, *o.ID) +} + type DirectoryRole struct { - ID *string `json:"id,omitempty"` + DirectoryObject + Members *Members `json:"-"` + Description *string `json:"description,omitempty"` DisplayName *string `json:"displayName,omitempty"` RoleTemplateId *string `json:"roleTemplateId,omitempty"` - - Members *[]string `json:"-"` } -// AppendMember appends a new member object URI to the Members slice. -func (d *DirectoryRole) AppendMember(endpoint environments.ApiEndpoint, apiVersion ApiVersion, id string) { - val := fmt.Sprintf("%s/%s/directoryObjects/%s", endpoint, apiVersion, id) - var members []string - if d.Members != nil { - members = *d.Members +func (r *DirectoryRole) UnmarshalJSON(data []byte) error { + // Local type needed to avoid recursive UnmarshalJSON calls + type directoryrole DirectoryRole + r2 := (*directoryrole)(r) + if err := json.Unmarshal(data, r2); err != nil { + return err } - members = append(members, val) - d.Members = &members + return nil } // DirectoryRoleTemplate describes a Directory Role Template. @@ -496,11 +530,26 @@ type EmailAddress struct { Name *string `json:"name,omitempty"` } +type EmailAuthenticationMethod struct { + ID *string `json:"id,omitempty"` + EmailAddress *string `json:"emailAddress,omitempty"` +} + type ExtensionSchemaProperty struct { Name *string `json:"name,omitempty"` Type ExtensionSchemaPropertyDataType `json:"type,omitempty"` } +type Fido2AuthenticationMethod struct { + ID *string `json:"id,omitempty"` + DisplayName *string `json:"displayName,omitempty"` + CreatedDateTime *time.Time `json:"createdDateTime,omitempty"` + AAGuid *string `json:"aaGuid,omitempty"` + Model *string `json:"model,omitempty"` + AttestationCertificates *[]string `json:"attestationCertificates,omitempty"` + AttestationLevel *AttestationLevel `json:"attestationLevel,omitempty"` +} + type GeoCoordinates struct { Altitude *float64 `json:"altitude,omitempty"` Latitude *float64 `json:"latitude,omitempty"` @@ -509,7 +558,11 @@ type GeoCoordinates struct { // Group describes a Group object. type Group struct { - ID *string `json:"id,omitempty"` + DirectoryObject + Members *Members `json:"members@odata.bind,omitempty"` + Owners *Owners `json:"owners@odata.bind,omitempty"` + SchemaExtensions *[]SchemaExtensionData `json:"-"` + AllowExternalSenders *string `json:"allowExternalSenders,omitempty"` AssignedLabels *[]GroupAssignedLabel `json:"assignedLabels,omitempty"` AssignedLicenses *[]GroupAssignedLicense `json:"assignLicenses,omitempty"` @@ -550,17 +603,13 @@ type Group struct { UnseenCount *int `json:"unseenCount,omitempty"` Visibility *GroupVisibility `json:"visibility,omitempty"` IsAssignableToRole *bool `json:"isAssignableToRole,omitempty"` - - SchemaExtensions *[]SchemaExtensionData `json:"-"` - - Members *[]string `json:"members@odata.bind,omitempty"` - Owners *[]string `json:"owners@odata.bind,omitempty"` } func (g Group) MarshalJSON() ([]byte, error) { docs := make([][]byte, 0) + // Local type needed to avoid recursive MarshalJSON calls type group Group - d, err := json.Marshal(group(g)) + d, err := json.Marshal((*group)(&g)) if err != nil { return d, err } @@ -578,6 +627,7 @@ func (g Group) MarshalJSON() ([]byte, error) { } func (g *Group) UnmarshalJSON(data []byte) error { + // Local type needed to avoid recursive UnmarshalJSON calls type group Group g2 := (*group)(g) if err := json.Unmarshal(data, g2); err != nil { @@ -599,28 +649,6 @@ func (g *Group) UnmarshalJSON(data []byte) error { return nil } -// AppendMember appends a new member object URI to the Members slice. -func (g *Group) AppendMember(endpoint environments.ApiEndpoint, apiVersion ApiVersion, id string) { - val := fmt.Sprintf("%s/%s/directoryObjects/%s", endpoint, apiVersion, id) - var members []string - if g.Members != nil { - members = *g.Members - } - members = append(members, val) - g.Members = &members -} - -// AppendOwner appends a new owner object URI to the Owners slice. -func (g *Group) AppendOwner(endpoint environments.ApiEndpoint, apiVersion ApiVersion, id string) { - val := fmt.Sprintf("%s/%s/directoryObjects/%s", endpoint, apiVersion, id) - var owners []string - if g.Owners != nil { - owners = *g.Owners - } - owners = append(owners, val) - g.Owners = &owners -} - // HasTypes returns true if the group has all the specified GroupTypes func (g *Group) HasTypes(types []GroupType) bool { for _, t := range types { @@ -656,12 +684,12 @@ type GroupOnPremisesProvisioningError struct { } type IdentityProvider struct { - ODataType *string `json:"@odata.type,omitempty"` - ID *string `json:"id,omitempty"` - ClientId *string `json:"clientId,omitempty"` - ClientSecret *string `json:"clientSecret,omitempty"` - Type *string `json:"identityProviderType,omitempty"` - Name *string `json:"displayName,omitempty"` + ODataType *odata.Type `json:"@odata.type,omitempty"` + ID *string `json:"id,omitempty"` + ClientId *string `json:"clientId,omitempty"` + ClientSecret *string `json:"clientSecret,omitempty"` + Type *string `json:"identityProviderType,omitempty"` + Name *string `json:"displayName,omitempty"` } type ImplicitGrantSettings struct { @@ -764,6 +792,14 @@ type Message struct { BccRecipients *[]Recipient `json:"bccRecipients,omitempty"` } +type MicrosoftAuthenticatorAuthenticationMethod struct { + CreatedDateTime *time.Time `json:"createdDateTime,omitempty"` + DisplayName *string `json:"displayName,omitempty"` + ID *string `json:"id,omitempty"` + DeviceTag *string `json:"deviceTag,omitempty"` + PhoneAppVersion *string `json:"phoneAppVersion,omitempty"` +} + type ModifiedProperty struct { DisplayName *string `json:"displayName,omitempty"` NewValue *string `json:"newValue,omitempty"` @@ -834,6 +870,12 @@ type PasswordCredential struct { StartDateTime *time.Time `json:"startDateTime,omitempty"` } +type PasswordAuthenticationMethod struct { + CreationDateTime *time.Time `json:"creationDateTime,omitempty"` + ID *string `json:"id,omitempty"` + Password *string `json:"password,omitempty"` +} + type PasswordSingleSignOnSettings struct { Fields *[]SingleSignOnField `json:"fields,omitempty"` } @@ -854,6 +896,11 @@ type PersistentBrowserSessionControl struct { Mode *string `json:"mode,omitempty"` } +type PhoneAuthenticationMethod struct { + ID *string `json:"id,omitempty"` + PhoneNumber *string `json:"phoneNumber,omitempty"` + PhoneType *AuthenticationPhoneType `json:"phoneType,omitempty"` +} type PublicClient struct { RedirectUris *[]string `json:"redirectUris,omitempty"` } @@ -899,7 +946,9 @@ func (se SchemaExtensionData) MarshalJSON() ([]byte, error) { // ServicePrincipal describes a Service Principal object. type ServicePrincipal struct { - ID *string `json:"id,omitempty"` + DirectoryObject + Owners *Owners `json:"owners@odata.bind,omitempty"` + AccountEnabled *bool `json:"accountEnabled,omitempty"` AddIns *[]AddIn `json:"addIns,omitempty"` AlternativeNames *[]string `json:"alternativeNames,omitempty"` @@ -933,19 +982,16 @@ type ServicePrincipal struct { Tags *[]string `json:"tags,omitempty"` TokenEncryptionKeyId *string `json:"tokenEncryptionKeyId,omitempty"` VerifiedPublisher *VerifiedPublisher `json:"verifiedPublisher,omitempty"` - - Owners *[]string `json:"owners@odata.bind,omitempty"` } -// AppendOwner appends a new owner object URI to the Owners slice. -func (a *ServicePrincipal) AppendOwner(endpoint string, apiVersion string, id string) { - val := fmt.Sprintf("%s/%s/directoryObjects/%s", endpoint, apiVersion, id) - var owners []string - if a.Owners != nil { - owners = *a.Owners +func (s *ServicePrincipal) UnmarshalJSON(data []byte) error { + // Local type needed to avoid recursive UnmarshalJSON calls + type serviceprincipal ServicePrincipal + s2 := (*serviceprincipal)(s) + if err := json.Unmarshal(data, s2); err != nil { + return err } - owners = append(owners, val) - a.Owners = &owners + return nil } type SignInActivity struct { @@ -1007,9 +1053,21 @@ type TargetResource struct { ModifiedProperties *[]ModifiedProperty `json:"modifiedProperties,omitempty"` } +type TemporaryAccessPassAuthenticationMethod struct { + ID *string `json:"id,omitempty"` + TemporaryAccessPass *string `json:"temporaryAccessPass,omitempty"` + CreatedDateTime *time.Time `json:"createdDateTime,omitempty"` + StartDateTime *time.Time `json:"startDateTime,omitempty"` + LifetimeInMinutes *int32 `json:"lifetimeInMinutes,omitempty"` + IsUsableOnce *bool `json:"isUsableOnce,omitempty"` + IsUsable *bool `json:"isUsable,omitempty"` + MethodUsabilityReason *MethodUsabilityReason `json:"methodUsabilityReason,omitempty"` +} + // User describes a User object. type User struct { - ID *string `json:"id,omitempty"` + DirectoryObject + AboutMe *string `json:"aboutMe,omitempty"` AccountEnabled *bool `json:"accountEnabled,omitempty"` AgeGroup *AgeGroup `json:"ageGroup,omitempty"` @@ -1036,6 +1094,7 @@ type User struct { JobTitle *StringNullWhenEmpty `json:"jobTitle,omitempty"` Mail *StringNullWhenEmpty `json:"mail,omitempty"` MailNickname *string `json:"mailNickname,omitempty"` + MemberOf *[]DirectoryObject `json:"memberOf,omitempty"` MobilePhone *StringNullWhenEmpty `json:"mobilePhone,omitempty"` MySite *string `json:"mySite,omitempty"` OfficeLocation *StringNullWhenEmpty `json:"officeLocation,omitempty"` @@ -1075,6 +1134,7 @@ type User struct { func (u User) MarshalJSON() ([]byte, error) { docs := make([][]byte, 0) + // Local type needed to avoid recursive MarshalJSON calls type user User d, err := json.Marshal(user(u)) if err != nil { @@ -1094,6 +1154,7 @@ func (u User) MarshalJSON() ([]byte, error) { } func (u *User) UnmarshalJSON(data []byte) error { + // Local type needed to avoid recursive UnmarshalJSON calls type user User u2 := (*user)(u) if err := json.Unmarshal(data, u2); err != nil { @@ -1128,8 +1189,53 @@ type UserPasswordProfile struct { Password *string `json:"password,omitempty"` } +type UserRegistrationCount struct { + RegistrationStatus *RegistrationStatus `json:"registrationStatus,omitempty"` + RegistrationCount *int64 `json:"registrationCount,omitempty"` +} + +type UserRegistrationFeatureCount struct { + Feature *AuthenticationMethodFeature `json:"feature,omitempty"` + UserCount *int64 `json:"userCount"` +} +type UserRegistrationFeatureSummary struct { + TotalUserCount *int64 `json:"totalUserCount,omitempty"` + UserRegistrationFeatureCounts *[]UserRegistrationFeatureCount `json:"userRegistrationFeatureCounts"` + UserRoles IncludedUserRoles `json:"userRoles,omitempty"` + UserTypes IncludedUserTypes `json:"userTypes,omitempty"` +} + +type UserRegistrationMethodCount struct { + AuthenticationMethod *string `json:"authenticationMethod,omitempty"` + UserCount *int64 `json:"userCount,omitempty"` +} + +type UserRegistrationMethodSummary struct { + TotalUserCount *int64 `json:"totalUserCount"` + UserRegistrationMethodsCount *[]UserRegistrationMethodCount `json:"userRegistrationMethodCounts,omitempty"` + UerRoles IncludedUserRoles `json:"userRoles,omitempty"` + UserTypes IncludedUserTypes `json:"userTypes,omitempty"` +} + +type UserCredentialUsageDetails struct { + AuthMethod *UsageAuthMethod `json:"authMethod,omitempty"` + EventDateTime *time.Time `json:"eventDateTime,omitempty"` + FailureReason *string `json:"failureReason,omitempty"` + Feature *FeatureType `json:"feature,omitempty"` + ID *string `json:"id,omitempty"` + IsSuccess *bool `json:"isSuccess,omitempty"` + UserDisplayName *string `json:"userDisplayName,omitempty"` + UserPrincipalName *string `json:"userPrincipalName,omitempty"` +} type VerifiedPublisher struct { AddedDateTime *time.Time `json:"addedDateTime,omitempty"` DisplayName *string `json:"displayName,omitempty"` VerifiedPublisherId *string `json:"verifiedPublisherId,omitempty"` } + +type WindowsHelloForBusinessAuthenticationMethod struct { + CreatedDateTime *time.Time `json:"createdDateTime,omitempty"` + DisplayName *string `json:"displayName,omitempty"` + ID *string `json:"id,omitempty"` + KeyStrength *AuthenticationMethodKeyStrength `json:"authenticationMethodKeyStrength,omitempty"` +} diff --git a/vendor/github.com/manicminer/hamilton/msgraph/namedlocations.go b/vendor/github.com/manicminer/hamilton/msgraph/namedlocations.go index 96bfcf552a..ac5c5845a6 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/namedlocations.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/namedlocations.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "github.com/manicminer/hamilton/internal/utils" @@ -40,9 +40,9 @@ func (c *NamedLocationsClient) List(ctx context.Context, query odata.Query) (*[] } defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } var data struct { @@ -71,13 +71,13 @@ func (c *NamedLocationsClient) List(ctx context.Context, query odata.Query) (*[] continue } switch *o.Type { - case "#microsoft.graph.countryNamedLocation": + case odata.TypeCountryNamedLocation: var loc CountryNamedLocation if err := json.Unmarshal(namedLocation, &loc); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } ret = append(ret, loc) - case "#microsoft.graph.ipNamedLocation": + case odata.TypeIpNamedLocation: var loc IPNamedLocation if err := json.Unmarshal(namedLocation, &loc); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) @@ -103,6 +103,7 @@ func (c *NamedLocationsClient) Delete(ctx context.Context, id string) (int, erro if err != nil { return status, fmt.Errorf("NamedLocationsClient.BaseClient.Delete(): %v", err) } + return status, nil } @@ -110,11 +111,12 @@ func (c *NamedLocationsClient) Delete(ctx context.Context, id string) (int, erro func (c *NamedLocationsClient) CreateIP(ctx context.Context, ipNamedLocation IPNamedLocation) (*IPNamedLocation, int, error) { var status int - ipNamedLocation.ODataType = utils.StringPtr("#microsoft.graph.ipNamedLocation") + ipNamedLocation.ODataType = utils.StringPtr(odata.TypeIpNamedLocation) body, err := json.Marshal(ipNamedLocation) if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ValidStatusCodes: []int{http.StatusCreated}, @@ -126,15 +128,18 @@ func (c *NamedLocationsClient) CreateIP(ctx context.Context, ipNamedLocation IPN if err != nil { return nil, status, fmt.Errorf("NamedLocationsClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var newIPNamedLocation IPNamedLocation if err := json.Unmarshal(respBody, &newIPNamedLocation); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &newIPNamedLocation, status, nil } @@ -142,12 +147,12 @@ func (c *NamedLocationsClient) CreateIP(ctx context.Context, ipNamedLocation IPN func (c *NamedLocationsClient) CreateCountry(ctx context.Context, countryNamedLocation CountryNamedLocation) (*CountryNamedLocation, int, error) { var status int - countryNamedLocation.ODataType = utils.StringPtr("#microsoft.graph.countryNamedLocation") - + countryNamedLocation.ODataType = utils.StringPtr(odata.TypeCountryNamedLocation) body, err := json.Marshal(countryNamedLocation) if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ValidStatusCodes: []int{http.StatusCreated}, @@ -159,15 +164,18 @@ func (c *NamedLocationsClient) CreateCountry(ctx context.Context, countryNamedLo if err != nil { return nil, status, fmt.Errorf("NamedLocationsClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var newCountryNamedLocation CountryNamedLocation if err := json.Unmarshal(respBody, &newCountryNamedLocation); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &newCountryNamedLocation, status, nil } @@ -185,15 +193,18 @@ func (c *NamedLocationsClient) GetIP(ctx context.Context, id string, query odata if err != nil { return nil, status, fmt.Errorf("NamedLocationsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var ipNamedLocation IPNamedLocation if err := json.Unmarshal(respBody, &ipNamedLocation); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &ipNamedLocation, status, nil } @@ -211,10 +222,11 @@ func (c *NamedLocationsClient) Get(ctx context.Context, id string, query odata.Q if err != nil { return nil, status, fmt.Errorf("NamedLocationsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } var o odata.OData @@ -231,13 +243,13 @@ func (c *NamedLocationsClient) Get(ctx context.Context, id string, query odata.Q } switch *o.Type { - case "#microsoft.graph.countryNamedLocation": + case odata.TypeCountryNamedLocation: var loc CountryNamedLocation if err := json.Unmarshal(respBody, &loc); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } ret = loc - case "#microsoft.graph.ipNamedLocation": + case odata.TypeIpNamedLocation: var loc IPNamedLocation if err := json.Unmarshal(respBody, &loc); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) @@ -263,15 +275,18 @@ func (c *NamedLocationsClient) GetCountry(ctx context.Context, id string, query if err != nil { return nil, status, fmt.Errorf("NamedLocationsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var countryNamedLocation CountryNamedLocation if err := json.Unmarshal(respBody, &countryNamedLocation); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &countryNamedLocation, status, nil } @@ -279,12 +294,12 @@ func (c *NamedLocationsClient) GetCountry(ctx context.Context, id string, query func (c *NamedLocationsClient) UpdateIP(ctx context.Context, ipNamedLocation IPNamedLocation) (int, error) { var status int - ipNamedLocation.ODataType = utils.StringPtr("#microsoft.graph.ipNamedLocation") - + ipNamedLocation.ODataType = utils.StringPtr(odata.TypeIpNamedLocation) body, err := json.Marshal(ipNamedLocation) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Patch(ctx, PatchHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -297,6 +312,7 @@ func (c *NamedLocationsClient) UpdateIP(ctx context.Context, ipNamedLocation IPN if err != nil { return status, fmt.Errorf("NamedLocationsClient.BaseClient.Patch(): %v", err) } + return status, nil } @@ -304,12 +320,12 @@ func (c *NamedLocationsClient) UpdateIP(ctx context.Context, ipNamedLocation IPN func (c *NamedLocationsClient) UpdateCountry(ctx context.Context, countryNamedLocation CountryNamedLocation) (int, error) { var status int - countryNamedLocation.ODataType = utils.StringPtr("#microsoft.graph.countryNamedLocation") - + countryNamedLocation.ODataType = utils.StringPtr(odata.TypeCountryNamedLocation) body, err := json.Marshal(countryNamedLocation) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Patch(ctx, PatchHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -322,5 +338,6 @@ func (c *NamedLocationsClient) UpdateCountry(ctx context.Context, countryNamedLo if err != nil { return status, fmt.Errorf("NamedLocationsClient.BaseClient.Patch(): %v", err) } + return status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/reports.go b/vendor/github.com/manicminer/hamilton/msgraph/reports.go new file mode 100644 index 0000000000..61ca552457 --- /dev/null +++ b/vendor/github.com/manicminer/hamilton/msgraph/reports.go @@ -0,0 +1,199 @@ +package msgraph + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/manicminer/hamilton/odata" +) + +// ReportsClient Client performs operations on reports. +type ReportsClient struct { + BaseClient Client +} + +// NewReportsClient returns a new ReportsClient. +func NewReportsClient(tenantId string) *ReportsClient { + return &ReportsClient{ + BaseClient: NewClient(VersionBeta, tenantId), + } +} + +func (c *ReportsClient) GetCredentialUserRegistrationCount(ctx context.Context, query odata.Query) (*[]CredentialUserRegistrationCount, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + DisablePaging: query.Top > 0, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: "/reports/getCredentialUserRegistrationCount", + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("ReportsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var data struct { + CredentialUserRegistrationCount []CredentialUserRegistrationCount `json:"value"` + } + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &data.CredentialUserRegistrationCount, status, nil +} + +func (c *ReportsClient) GetCredentialUserRegistrationDetails(ctx context.Context, query odata.Query) (*[]CredentialUserRegistrationDetails, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + DisablePaging: query.Top > 0, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: "/reports/credentialUserRegistrationDetails", + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("ReportsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var data struct { + CredentialUserRegistrationDetails []CredentialUserRegistrationDetails `json:"value"` + } + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &data.CredentialUserRegistrationDetails, status, nil +} + +func (c *ReportsClient) GetUserCredentialUsageDetails(ctx context.Context, query odata.Query) (*[]UserCredentialUsageDetails, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + DisablePaging: query.Top > 0, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: "/reports/userCredentialUsageDetails", + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("ReportsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var data struct { + UserCredentialUsageDetails []UserCredentialUsageDetails `json:"value"` + } + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &data.UserCredentialUsageDetails, status, nil +} + +func (c *ReportsClient) GetCredentialUsageSummary(ctx context.Context, period CredentialUsageSummaryPeriod, query odata.Query) (*[]CredentialUsageSummary, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + DisablePaging: query.Top > 0, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/reports/getCredentialUsageSummary(period='%s')", period), + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("ReportsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var data struct { + CredentialUsageSummary []CredentialUsageSummary `json:"value"` + } + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &data.CredentialUsageSummary, status, nil +} + +func (c *ReportsClient) GetAuthenticationMethodsUsersRegisteredByFeature(ctx context.Context, query odata.Query) (*UserRegistrationFeatureSummary, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + DisablePaging: query.Top > 0, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: "/reports/authenticationMethods/usersRegisteredByFeature", + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("ReportsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var userRegistrationFeatureSummary UserRegistrationFeatureSummary + if err := json.Unmarshal(respBody, &userRegistrationFeatureSummary); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &userRegistrationFeatureSummary, status, nil +} + +func (c *ReportsClient) GetAuthenticationMethodsUsersRegisteredByMethod(ctx context.Context, query odata.Query) (*UserRegistrationMethodSummary, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + DisablePaging: query.Top > 0, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: "/reports/authenticationMethods/usersRegisteredByMethod", + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("ReportsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var userRegistrationMethodSummary UserRegistrationMethodSummary + if err := json.Unmarshal(respBody, &userRegistrationMethodSummary); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &userRegistrationMethodSummary, status, nil +} diff --git a/vendor/github.com/manicminer/hamilton/msgraph/schema_extensions.go b/vendor/github.com/manicminer/hamilton/msgraph/schema_extensions.go index 4a1ea75db4..9cb378bcf1 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/schema_extensions.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/schema_extensions.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "github.com/manicminer/hamilton/odata" @@ -36,17 +36,20 @@ func (c *SchemaExtensionsClient) List(ctx context.Context, query odata.Query) (* if err != nil { return nil, status, fmt.Errorf("SchemaExtensionsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { SchemaExtensions []SchemaExtension `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.SchemaExtensions, status, nil } @@ -64,25 +67,30 @@ func (c *SchemaExtensionsClient) Get(ctx context.Context, id string, query odata if err != nil { return nil, status, fmt.Errorf("SchemaExtensionsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var schemaExtension SchemaExtension if err := json.Unmarshal(respBody, &schemaExtension); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &schemaExtension, status, nil } // Update amends an existing schema Extension. func (c *SchemaExtensionsClient) Update(ctx context.Context, schemaExtension SchemaExtension) (int, error) { var status int + body, err := json.Marshal(schemaExtension) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Patch(ctx, PatchHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -95,16 +103,19 @@ func (c *SchemaExtensionsClient) Update(ctx context.Context, schemaExtension Sch if err != nil { return status, fmt.Errorf("SchemaExtensionsClient.BaseClient.Patch(): %v", err) } + return status, nil } // Create creates a new Schema Extension func (c *SchemaExtensionsClient) Create(ctx context.Context, schemaExtension SchemaExtension) (*SchemaExtension, int, error) { var status int + body, err := json.Marshal(schemaExtension) if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ValidStatusCodes: []int{http.StatusCreated}, @@ -116,15 +127,18 @@ func (c *SchemaExtensionsClient) Create(ctx context.Context, schemaExtension Sch if err != nil { return nil, status, fmt.Errorf("SchemaExtensionsClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var newSchemaExtension SchemaExtension if err := json.Unmarshal(respBody, &newSchemaExtension); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &newSchemaExtension, status, nil } @@ -141,5 +155,6 @@ func (c *SchemaExtensionsClient) Delete(ctx context.Context, id string) (int, er if err != nil { return status, fmt.Errorf("SchemaExtensionsClient.BaseClient.Delete(): %v", err) } + return status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/serviceprincipals.go b/vendor/github.com/manicminer/hamilton/msgraph/serviceprincipals.go index 85efe6bf18..8aefea2568 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/serviceprincipals.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/serviceprincipals.go @@ -5,7 +5,7 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "net/http" "github.com/manicminer/hamilton/odata" @@ -37,30 +37,36 @@ func (c *ServicePrincipalsClient) List(ctx context.Context, query odata.Query) ( if err != nil { return nil, status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { ServicePrincipals []ServicePrincipal `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.ServicePrincipals, status, nil } // Create creates a new Service Principal. func (c *ServicePrincipalsClient) Create(ctx context.Context, servicePrincipal ServicePrincipal) (*ServicePrincipal, int, error) { var status int + body, err := json.Marshal(servicePrincipal) if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + appNotReplicated := func(resp *http.Response, o *odata.OData) bool { - return o.Error != nil && o.Error.Match(odata.ErrorServicePrincipalInvalidAppId) + return o != nil && o.Error != nil && o.Error.Match(odata.ErrorServicePrincipalInvalidAppId) } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ConsistencyFailureFunc: appNotReplicated, @@ -73,15 +79,18 @@ func (c *ServicePrincipalsClient) Create(ctx context.Context, servicePrincipal S if err != nil { return nil, status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var newServicePrincipal ServicePrincipal if err := json.Unmarshal(respBody, &newServicePrincipal); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &newServicePrincipal, status, nil } @@ -99,28 +108,34 @@ func (c *ServicePrincipalsClient) Get(ctx context.Context, id string, query odat if err != nil { return nil, status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var servicePrincipal ServicePrincipal if err := json.Unmarshal(respBody, &servicePrincipal); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &servicePrincipal, status, nil } // Update amends an existing Service Principal. func (c *ServicePrincipalsClient) Update(ctx context.Context, servicePrincipal ServicePrincipal) (int, error) { var status int + if servicePrincipal.ID == nil { return status, errors.New("cannot update service principal with nil ID") } + body, err := json.Marshal(servicePrincipal) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Patch(ctx, PatchHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -133,6 +148,7 @@ func (c *ServicePrincipalsClient) Update(ctx context.Context, servicePrincipal S if err != nil { return status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Patch(): %v", err) } + return status, nil } @@ -149,6 +165,7 @@ func (c *ServicePrincipalsClient) Delete(ctx context.Context, id string) (int, e if err != nil { return status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Delete(): %v", err) } + return status, nil } @@ -167,11 +184,13 @@ func (c *ServicePrincipalsClient) ListOwners(ctx context.Context, id string) (*[ if err != nil { return nil, status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Owners []struct { Type string `json:"@odata.type"` @@ -181,10 +200,12 @@ func (c *ServicePrincipalsClient) ListOwners(ctx context.Context, id string) (*[ if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + ret := make([]string, len(data.Owners)) for i, v := range data.Owners { ret[i] = v.Id } + return &ret, status, nil } @@ -204,11 +225,13 @@ func (c *ServicePrincipalsClient) GetOwner(ctx context.Context, servicePrincipal if err != nil { return nil, status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Context string `json:"@odata.context"` Type string `json:"@odata.type"` @@ -218,37 +241,40 @@ func (c *ServicePrincipalsClient) GetOwner(ctx context.Context, servicePrincipal if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.Id, status, nil } -// AddOwners adds a new owner to a Service Principal. -// First populate the Owners field of the ServicePrincipal using the AppendOwner method of the model, then call this method. +// AddOwners adds owners to a Service Principal. +// First populate the `owners` field, then call this method func (c *ServicePrincipalsClient) AddOwners(ctx context.Context, servicePrincipal *ServicePrincipal) (int, error) { var status int + if servicePrincipal.ID == nil { return status, errors.New("cannot update service principal with nil ID") } if servicePrincipal.Owners == nil { return status, errors.New("cannot update service principal with nil Owners") } + for _, owner := range *servicePrincipal.Owners { // don't fail if an owner already exists checkOwnerAlreadyExists := func(resp *http.Response, o *odata.OData) bool { - if resp.StatusCode == http.StatusBadRequest && o.Error != nil { + if resp.StatusCode == http.StatusBadRequest && o != nil && o.Error != nil { return o.Error.Match(odata.ErrorAddedObjectReferencesAlreadyExist) } return false } - data := struct { - Owner string `json:"@odata.id"` + body, err := json.Marshal(struct { + Owner odata.Id `json:"@odata.id"` }{ - Owner: owner, - } - body, err := json.Marshal(data) + Owner: *owner.ODataId, + }) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -263,6 +289,7 @@ func (c *ServicePrincipalsClient) AddOwners(ctx context.Context, servicePrincipa return status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Post(): %v", err) } } + return status, nil } @@ -271,9 +298,11 @@ func (c *ServicePrincipalsClient) AddOwners(ctx context.Context, servicePrincipa // ownerIds is a *[]string containing object IDs of owners to remove. func (c *ServicePrincipalsClient) RemoveOwners(ctx context.Context, servicePrincipalId string, ownerIds *[]string) (int, error) { var status int + if ownerIds == nil { return status, errors.New("cannot remove, nil ownerIds") } + for _, ownerId := range *ownerIds { // check for ownership before attempting deletion if _, status, err := c.GetOwner(ctx, servicePrincipalId, ownerId); err != nil { @@ -285,7 +314,7 @@ func (c *ServicePrincipalsClient) RemoveOwners(ctx context.Context, servicePrinc // despite the above check, sometimes owners are just gone checkOwnerGone := func(resp *http.Response, o *odata.OData) bool { - if resp.StatusCode == http.StatusBadRequest && o.Error != nil { + if resp.StatusCode == http.StatusBadRequest && o != nil && o.Error != nil { return o.Error.Match(odata.ErrorRemovedObjectReferencesDoNotExist) } return false @@ -304,6 +333,7 @@ func (c *ServicePrincipalsClient) RemoveOwners(ctx context.Context, servicePrinc return status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Delete(): %v", err) } } + return status, nil } @@ -322,23 +352,27 @@ func (c *ServicePrincipalsClient) ListGroupMemberships(ctx context.Context, id s if err != nil { return nil, status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Groups []Group `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.Groups, status, nil } // AddPassword appends a new password credential to a Service Principal. func (c *ServicePrincipalsClient) AddPassword(ctx context.Context, servicePrincipalId string, passwordCredential PasswordCredential) (*PasswordCredential, int, error) { var status int + body, err := json.Marshal(struct { PwdCredential PasswordCredential `json:"passwordCredential"` }{ @@ -347,6 +381,7 @@ func (c *ServicePrincipalsClient) AddPassword(ctx context.Context, servicePrinci if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -359,21 +394,25 @@ func (c *ServicePrincipalsClient) AddPassword(ctx context.Context, servicePrinci if err != nil { return nil, status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var newPasswordCredential PasswordCredential if err := json.Unmarshal(respBody, &newPasswordCredential); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &newPasswordCredential, status, nil } // RemovePassword removes a password credential from a Service Principal. func (c *ServicePrincipalsClient) RemovePassword(ctx context.Context, servicePrincipalId string, keyId string) (int, error) { var status int + body, err := json.Marshal(struct { KeyId string `json:"keyId"` }{ @@ -382,6 +421,7 @@ func (c *ServicePrincipalsClient) RemovePassword(ctx context.Context, servicePri if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -394,6 +434,7 @@ func (c *ServicePrincipalsClient) RemovePassword(ctx context.Context, servicePri if err != nil { return status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Post(): %v", err) } + return status, nil } @@ -412,8 +453,9 @@ func (c *ServicePrincipalsClient) ListOwnedObjects(ctx context.Context, id strin if err != nil { return nil, status, err } + defer resp.Body.Close() - respBody, _ := ioutil.ReadAll(resp.Body) + respBody, _ := io.ReadAll(resp.Body) var data struct { OwnedObjects []struct { Type string `json:"@odata.type"` @@ -423,10 +465,12 @@ func (c *ServicePrincipalsClient) ListOwnedObjects(ctx context.Context, id strin if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, err } + ret := make([]string, len(data.OwnedObjects)) for i, v := range data.OwnedObjects { ret[i] = v.Id } + return &ret, status, nil } @@ -444,17 +488,20 @@ func (c *ServicePrincipalsClient) ListAppRoleAssignments(ctx context.Context, re if err != nil { return nil, status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { AppRoleAssignments []AppRoleAssignment `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.AppRoleAssignments, status, nil } @@ -471,6 +518,7 @@ func (c *ServicePrincipalsClient) RemoveAppRoleAssignment(ctx context.Context, r if err != nil { return status, fmt.Errorf("AppRoleAssignmentsClient.BaseClient.Delete(): %v", err) } + return status, nil } @@ -482,6 +530,7 @@ func (c *ServicePrincipalsClient) RemoveAppRoleAssignment(ctx context.Context, r // appRoleId: The id of the appRole (defined on the resource service principal) to assign to a user, group, or service principal. func (c *ServicePrincipalsClient) AssignAppRoleForResource(ctx context.Context, principalId, resourceId, appRoleId string) (*AppRoleAssignment, int, error) { var status int + data := struct { PrincipalId string `json:"principalId"` ResourceId string `json:"resourceId"` @@ -491,11 +540,11 @@ func (c *ServicePrincipalsClient) AssignAppRoleForResource(ctx context.Context, ResourceId: resourceId, AppRoleId: appRoleId, } - body, err := json.Marshal(data) if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -508,14 +557,17 @@ func (c *ServicePrincipalsClient) AssignAppRoleForResource(ctx context.Context, if err != nil { return nil, status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var appRoleAssignment AppRoleAssignment if err := json.Unmarshal(respBody, &appRoleAssignment); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &appRoleAssignment, status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/sign_in_reports.go b/vendor/github.com/manicminer/hamilton/msgraph/sign_in_reports.go index bd4f906012..88ffa314dc 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/sign_in_reports.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/sign_in_reports.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "github.com/manicminer/hamilton/odata" @@ -36,17 +36,20 @@ func (c *SignInReportsClient) List(ctx context.Context, query odata.Query) (*[]S if err != nil { return nil, status, fmt.Errorf("SignInLogsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { SignInLogs []SignInReport `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.SignInLogs, status, nil } @@ -64,14 +67,17 @@ func (c *SignInReportsClient) Get(ctx context.Context, id string, query odata.Qu if err != nil { return nil, status, fmt.Errorf("SignInLogsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var signInReport SignInReport if err := json.Unmarshal(respBody, &signInReport); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &signInReport, status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/users.go b/vendor/github.com/manicminer/hamilton/msgraph/users.go index 39680f618b..83b36c9d6a 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/users.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/users.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "github.com/manicminer/hamilton/odata" @@ -36,27 +36,32 @@ func (c *UsersClient) List(ctx context.Context, query odata.Query) (*[]User, int if err != nil { return nil, status, fmt.Errorf("UsersClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Users []User `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.Users, status, nil } // Create creates a new User. func (c *UsersClient) Create(ctx context.Context, user User) (*User, int, error) { var status int + body, err := json.Marshal(user) if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ValidStatusCodes: []int{http.StatusCreated}, @@ -68,15 +73,18 @@ func (c *UsersClient) Create(ctx context.Context, user User) (*User, int, error) if err != nil { return nil, status, fmt.Errorf("UsersClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var newUser User if err := json.Unmarshal(respBody, &newUser); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &newUser, status, nil } @@ -94,15 +102,18 @@ func (c *UsersClient) Get(ctx context.Context, id string, query odata.Query) (*U if err != nil { return nil, status, fmt.Errorf("UsersClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var user User if err := json.Unmarshal(respBody, &user); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &user, status, nil } @@ -136,16 +147,18 @@ func (c *UsersClient) GetWithSchemaExtensions(ctx context.Context, id string, qu if err != nil { return nil, status, fmt.Errorf("UsersClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } user.SchemaExtensions = schemaExtensions if err := json.Unmarshal(respBody, user); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return user, status, nil } @@ -163,25 +176,30 @@ func (c *UsersClient) GetDeleted(ctx context.Context, id string, query odata.Que if err != nil { return nil, status, fmt.Errorf("UsersClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var user User if err := json.Unmarshal(respBody, &user); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &user, status, nil } // Update amends an existing User. func (c *UsersClient) Update(ctx context.Context, user User) (int, error) { var status int + body, err := json.Marshal(user) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Patch(ctx, PatchHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -194,6 +212,7 @@ func (c *UsersClient) Update(ctx context.Context, user User) (int, error) { if err != nil { return status, fmt.Errorf("UsersClient.BaseClient.Patch(): %v", err) } + return status, nil } @@ -210,6 +229,7 @@ func (c *UsersClient) Delete(ctx context.Context, id string) (int, error) { if err != nil { return status, fmt.Errorf("UsersClient.BaseClient.Delete(): %v", err) } + return status, nil } @@ -226,6 +246,7 @@ func (c *UsersClient) DeletePermanently(ctx context.Context, id string) (int, er if err != nil { return status, fmt.Errorf("UsersClient.BaseClient.Delete(): %v", err) } + return status, nil } @@ -243,14 +264,16 @@ func (c *UsersClient) ListDeleted(ctx context.Context, query odata.Query) (*[]Us if err != nil { return nil, status, err } + defer resp.Body.Close() - respBody, _ := ioutil.ReadAll(resp.Body) + respBody, _ := io.ReadAll(resp.Body) var data struct { DeletedUsers []User `json:"value"` } if err = json.Unmarshal(respBody, &data); err != nil { return nil, status, err } + return &data.DeletedUsers, status, nil } @@ -267,15 +290,18 @@ func (c *UsersClient) RestoreDeleted(ctx context.Context, id string) (*User, int if err != nil { return nil, status, fmt.Errorf("UsersClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var restoredUser User if err = json.Unmarshal(respBody, &restoredUser); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &restoredUser, status, nil } @@ -294,17 +320,20 @@ func (c *UsersClient) ListGroupMemberships(ctx context.Context, id string, query if err != nil { return nil, status, fmt.Errorf("UsersClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Groups []Group `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.Groups, status, nil } @@ -312,10 +341,12 @@ func (c *UsersClient) ListGroupMemberships(ctx context.Context, id string, query // TODO: Needs testing with an O365 user principal func (c *UsersClient) Sendmail(ctx context.Context, id string, message MailMessage) (int, error) { var status int + body, err := json.Marshal(message) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ValidStatusCodes: []int{http.StatusOK, http.StatusAccepted}, @@ -327,5 +358,6 @@ func (c *UsersClient) Sendmail(ctx context.Context, id string, message MailMessa if err != nil { return status, fmt.Errorf("UsersClient.BaseClient.Post(): %v", err) } + return status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/valuetypes.go b/vendor/github.com/manicminer/hamilton/msgraph/valuetypes.go index f5acae9513..ac2c0e6859 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/valuetypes.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/valuetypes.go @@ -1,6 +1,11 @@ package msgraph -import "encoding/json" +import ( + "encoding/json" + goerrors "errors" + + "github.com/manicminer/hamilton/odata" +) // StringNullWhenEmpty is a string type that marshals its JSON representation as null when set to its zero value. // Can be used with a pointer reference with the `omitempty` tag to omit a field when the pointer is nil, but send a @@ -51,6 +56,39 @@ const ( AppRoleAllowedMemberTypeUser AppRoleAllowedMemberType = "User" ) +type AttestationLevel = string + +const ( + AttestationLevelAttested AttestationLevel = "attested" + AttestationLevelNotAttested AttestationLevel = "notAttested" +) + +type AuthenticationMethodFeature = string + +const ( + AuthenticationMethodFeatureSsprRegistered AuthenticationMethodFeature = "ssprRegistered" + AuthenticationMethodFeatureSsprEnabled AuthenticationMethodFeature = "ssprEnabled" + AuthenticationMethodFeatureSsprCapable AuthenticationMethodFeature = "ssprCapable" + AuthenticationMethodFeaturePasswordlessCapable AuthenticationMethodFeature = "passwordlessCapable" + AuthenticationMethodFeatureMfaCapable AuthenticationMethodFeature = "mfaCapable" +) + +type AuthenticationMethodKeyStrength = string + +const ( + AuthenticationMethodKeyStrengthNormal AuthenticationMethodKeyStrength = "normal" + AuthenticationMethodKeyStrengthWeak AuthenticationMethodKeyStrength = "weak" + AuthenticationMethodKeyStrengthUnknown AuthenticationMethodKeyStrength = "unknown" +) + +type AuthenticationPhoneType = string + +const ( + AuthenticationPhoneTypeMobile AuthenticationPhoneType = "mobile" + AuthenticationPhoneTypeAlternateMobile AuthenticationPhoneType = "alternateMobile" + AuthenticationPhoneTypeOffice AuthenticationPhoneType = "office" +) + type BodyType = string const ( @@ -67,6 +105,14 @@ const ( ConsentProvidedForMinorNotRequired ConsentProvidedForMinor = "NotRequired" ) +type CredentialUsageSummaryPeriod = string + +const ( + CredentialUsageSummaryPeriod30 CredentialUsageSummaryPeriod = "D30" + CredentialUsageSummaryPeriod7 CredentialUsageSummaryPeriod = "D7" + CredentialUsageSummaryPeriod1 CredentialUsageSummaryPeriod = "D1" +) + type ExtensionSchemaTargetType = string const ( @@ -91,6 +137,14 @@ const ( ExtensionSchemaPropertyDataString ExtensionSchemaPropertyDataType = "String" ) +type FeatureType = string + +const ( + FeatureTypeRegistration FeatureType = "registration" + FeatureTypeReset FeatureType = "reset" + FeatureTypeUnknownFutureValue FeatureType = "unknownFutureValue" +) + type GroupType = string const ( @@ -157,6 +211,64 @@ const ( KeyCredentialUsageVerify KeyCredentialUsage = "Verify" ) +type Members []DirectoryObject + +func (o Members) MarshalJSON() ([]byte, error) { + members := make([]odata.Id, len(o)) + for i, v := range o { + if v.ODataId == nil { + return nil, goerrors.New("marshaling Members: encountered DirectoryObject with nil ODataId") + } + members[i] = *v.ODataId + } + return json.Marshal(members) +} + +func (o *Members) UnmarshalJSON(data []byte) error { + var members []odata.Id + if err := json.Unmarshal(data, &members); err != nil { + return err + } + for _, v := range members { + *o = append(*o, DirectoryObject{ODataId: &v}) + } + return nil +} + +type MethodUsabilityReason string + +const ( + MethodUsabilityReasonEnabledByPolicy MethodUsabilityReason = "enabledByPolicy" + MethodUsabilityReasonDisabledByPolicy MethodUsabilityReason = "disabledByPolicy" + MethodUsabilityReasonExpired MethodUsabilityReason = "expired" + MethodUsabilityReasonNotYetValid MethodUsabilityReason = "notYetValid" + MethodUsabilityReasonOneTimeUsed MethodUsabilityReason = "oneTimeUsed" +) + +type Owners []DirectoryObject + +func (o Owners) MarshalJSON() ([]byte, error) { + owners := make([]odata.Id, len(o)) + for i, v := range o { + if v.ODataId == nil { + return nil, goerrors.New("marshaling Owners: encountered DirectoryObject with nil ODataId") + } + owners[i] = *v.ODataId + } + return json.Marshal(owners) +} + +func (o *Owners) UnmarshalJSON(data []byte) error { + var owners []odata.Id + if err := json.Unmarshal(data, &owners); err != nil { + return err + } + for _, v := range owners { + *o = append(*o, DirectoryObject{ODataId: &v}) + } + return nil +} + type PermissionScopeType = string const ( @@ -174,6 +286,30 @@ const ( PreferredSingleSignOnModeSaml PreferredSingleSignOnMode = "saml" ) +type RegistrationAuthMethod = string + +const ( + RegistrationAuthMethodEmail RegistrationAuthMethod = "email" + RegistrationAuthMethodMobilePhone RegistrationAuthMethod = "mobilePhone" + RegistrationAuthMethodOfficePhone RegistrationAuthMethod = "officePhone" + RegistrationAuthMethodSecurityQuestion RegistrationAuthMethod = "securityQuestion" + RegistrationAuthMethodAppNotification RegistrationAuthMethod = "appNotification" + RegistrationAuthMethodAppCode RegistrationAuthMethod = "appCode" + RegistrationAuthMethodAlternateMobilePhone RegistrationAuthMethod = "alternateMobilePhone" + RegistrationAuthMethodFido RegistrationAuthMethod = "fido" + RegistrationAuthMethodAppPassword RegistrationAuthMethod = "appPassword" + RegistrationAuthMethodUnknownFutureValue RegistrationAuthMethod = "unknownFutureValue" +) + +type RegistrationStatus = string + +const ( + RegistrationStatusRegistered RegistrationStatus = "registered" + RegistrationStatusEnabled RegistrationStatus = "enabled" + RegistrationStatusCapable RegistrationStatus = "capable" + RegistrationStatusMfaRegistered RegistrationStatus = "mfaRegistered" +) + type ResourceAccessType = string const ( @@ -209,3 +345,36 @@ const ( SignInAudienceAzureADandPersonalMicrosoftAccount SignInAudience = "AzureADandPersonalMicrosoftAccount" SignInAudiencePersonalMicrosoftAccount SignInAudience = "PersonalMicrosoftAccount" ) + +type UsageAuthMethod = string + +const ( + UsageAuthMethodEmail UsageAuthMethod = "email" + UsageAuthMethodMobileSMS UsageAuthMethod = "mobileSMS" + UsageAuthMethodMobileCall UsageAuthMethod = "mobileCall" + UsageAuthMethodOfficePhone UsageAuthMethod = "officePhone" + UsageAuthMethodSecurityQuestion UsageAuthMethod = "securityQuestion" + UsageAuthMethodAppNotification UsageAuthMethod = "appNotification" + UsageAuthMethodAppCode UsageAuthMethod = "appCode" + UsageAuthMethodAlternativeMobileCall UsageAuthMethod = "alternateMobileCall" + UsageAuthMethodFido UsageAuthMethod = "fido" + UsageAuthMethodAppPassword UsageAuthMethod = "appPassword" + UsageAuthMethodUnknownFutureValue UsageAuthMethod = "unknownFutureValue" +) + +type IncludedUserRoles = string + +const ( + IncludedUserRolesAll IncludedUserRoles = "all" + IncludedUserRolesPrivilegedAdmin IncludedUserRoles = "privilegedAdmin" + IncludedUserRolesAdmin IncludedUserRoles = "admin" + IncludedUserRolesUser IncludedUserRoles = "user" +) + +type IncludedUserTypes = string + +const ( + IncludedUserTypesAll IncludedUserTypes = "all" + IncludedUserTypesMember IncludedUserTypes = "member" + IncludedUserTypesGuest IncludedUserTypes = "guest" +) diff --git a/vendor/github.com/manicminer/hamilton/odata/odata.go b/vendor/github.com/manicminer/hamilton/odata/odata.go index 1a7e09ba8e..6c7f52936c 100644 --- a/vendor/github.com/manicminer/hamilton/odata/odata.go +++ b/vendor/github.com/manicminer/hamilton/odata/odata.go @@ -10,20 +10,86 @@ import ( const ( ErrorAddedObjectReferencesAlreadyExist = "One or more added object references already exist" ErrorConflictingObjectPresentInDirectory = "A conflicting object with one or more of the specified property values is present in the directory" + ErrorResourceDoesNotExist = "Resource '.+' does not exist or one of its queried reference-property objects are not present" ErrorRemovedObjectReferencesDoNotExist = "One or more removed object references do not exist" - ErrorServicePrincipalInvalidAppId = "The appId '.+' of the service principal does not reference a valid application object." + ErrorServicePrincipalInvalidAppId = "The appId '.+' of the service principal does not reference a valid application object" +) + +type Id string + +func (o *Id) UnmarshalJSON(data []byte) error { + var id string + if err := json.Unmarshal(data, &id); err != nil { + return err + } + *o = Id(regexp.MustCompile(`/v2/`).ReplaceAllString(id, `/v1.0/`)) + return nil +} + +type ShortType = string + +const ( + ShortTypeAdministrativeUnit ShortType = "administrativeUnit" + ShortTypeApplication ShortType = "application" + ShortTypeConditionalAccessPolicy ShortType = "conditionalAccessPolicy" + ShortTypeCountryNamedLocation ShortType = "countryNamedLocation" + ShortTypeDevice ShortType = "device" + ShortTypeDirectoryRole ShortType = "directoryRole" + ShortTypeDirectoryRoleTemplate ShortType = "directoryRoleTemplate" + ShortTypeDomain ShortType = "domain" + ShortTypeEmailAuthenticationMethod ShortType = "emailAuthenticationMethod" + ShortTypeFido2AuthenticationMethod ShortType = "fido2AuthenticationMethod" + ShortTypeGroup ShortType = "group" + ShortTypeIpNamedLocation ShortType = "ipNamedLocation" + ShortTypeNamedLocation ShortType = "namedLocation" + ShortTypeMicrosoftAuthenticatorAuthenticationMethod ShortType = "microsoftAuthenticatorAuthenticationMethod" + ShortTypeOrganization ShortType = "organization" + ShortTypePasswordAuthenticationMethod ShortType = "passwordAuthenticationMethod" + ShortTypePhoneAuthenticationMethod ShortType = "phoneAuthenticationMethod" + ShortTypeServicePrincipal ShortType = "servicePrincipal" + ShortTypeSocialIdentityProvider ShortType = "socialIdentityProvider" + ShortTypeTemporaryAccessPassAuthenticationMethod ShortType = "temporaryAccessPassAuthenticationMethod" + ShortTypeUser ShortType = "user" + ShortTypeWindowsHelloForBusinessAuthenticationMethod ShortType = "windowsHelloForBusinessAuthenticationMethod" +) + +type Type = string + +const ( + TypeAdministrativeUnit Type = "#microsoft.graph.administrativeUnit" + TypeApplication Type = "#microsoft.graph.application" + TypeConditionalAccessPolicy Type = "#microsoft.graph.conditionalAccessPolicy" + TypeCountryNamedLocation Type = "#microsoft.graph.countryNamedLocation" + TypeDevice Type = "#microsoft.graph.device" + TypeDirectoryRole Type = "#microsoft.graph.directoryRole" + TypeDirectoryRoleTemplate Type = "#microsoft.graph.directoryRoleTemplate" + TypeDomain Type = "#microsoft.graph.domain" + TypeEmailAuthenticationMethod Type = "#microsoft.graph.emailAuthenticationMethod" + TypeFido2AuthenticationMethod Type = "#microsoft.graph.fido2AuthenticationMethod" + TypeGroup Type = "#microsoft.graph.group" + TypeIpNamedLocation Type = "#microsoft.graph.ipNamedLocation" + TypeNamedLocation Type = "#microsoft.graph.namedLocation" + TypeMicrosoftAuthenticatorAuthenticationMethod Type = "#microsoft.graph.microsoftAuthenticatorAuthenticationMethod" + TypeOrganization Type = "#microsoft.graph.organization" + TypePasswordAuthenticationMethod Type = "#microsoft.graph.passwordAuthenticationMethod" + TypePhoneAuthenticationMethod Type = "#microsoft.graph.phoneAuthenticationMethod" + TypeServicePrincipal Type = "#microsoft.graph.servicePrincipal" + TypeSocialIdentityProvider Type = "#microsoft.graph.socialIdentityProvider" + TypeTemporaryAccessPassAuthenticationMethod Type = "#microsoft.graph.temporaryAccessPassAuthenticationMethod" + TypeUser Type = "#microsoft.graph.user" + TypeWindowsHelloForBusinessAuthenticationMethod Type = "#microsoft.graph.windowsHelloForBusinessAuthenticationMethod" ) // OData is used to unmarshall OData metadata from an API response. type OData struct { Context *string `json:"@odata.context"` MetadataEtag *string `json:"@odata.metadataEtag"` - Type *string `json:"@odata.type"` + Type *Type `json:"@odata.type"` Count *string `json:"@odata.count"` NextLink *string `json:"@odata.nextLink"` Delta *string `json:"@odata.delta"` DeltaLink *string `json:"@odata.deltaLink"` - Id *string `json:"@odata.id"` + Id *Id `json:"@odata.id"` Etag *string `json:"@odata.etag"` Error *Error `json:"-"` diff --git a/vendor/modules.txt b/vendor/modules.txt index fd0fc039a0..b4015d6804 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -192,7 +192,7 @@ github.com/klauspost/compress/fse github.com/klauspost/compress/huff0 github.com/klauspost/compress/zstd github.com/klauspost/compress/zstd/internal/xxhash -# github.com/manicminer/hamilton v0.23.1 +# github.com/manicminer/hamilton v0.26.0 ## explicit github.com/manicminer/hamilton/auth github.com/manicminer/hamilton/environments