diff --git a/docs/resources/directory_role_eligibility_schedule_request.md b/docs/resources/directory_role_eligibility_schedule_request.md new file mode 100644 index 0000000000..7d8971090e --- /dev/null +++ b/docs/resources/directory_role_eligibility_schedule_request.md @@ -0,0 +1,59 @@ +--- +subcategory: "Directory Roles" +--- + +# Resource: azuread_directory_role_eligibility_schedule_request + +Manages a single directory role eligibility schedule request within Azure Active Directory. + +## API Permissions + +The following API permissions are required in order to use this resource. + +The calling principal requires one of the following application roles: `RoleEligibilitySchedule.ReadWrite.Directory` or `RoleManagement.ReadWrite.Directory`. + +The calling principal requires one of the following directory roles: `Privileged Role Administrator` or `Global Administrator`. + +## Example Usage + +```terraform +data "azuread_user" "example" { + user_principal_name = "jdoe@hashicorp.com" +} + +resource "azuread_directory_role" "example" { + display_name = "Application Administrator" +} + +resource "azuread_directory_role_eligibility_schedule_request" "example" { + role_definition_id = azuread_directory_role.example.template_id + principal_id = azuread_user.example.object_id + directory_scope_id = "/" + justification = "Example" +} +``` + +~> Note the use of the `template_id` attribute when referencing built-in roles. + +## Argument Reference + +The following arguments are supported: + +* `directory_scope_id` - (Required) Identifier of the directory object representing the scope of the role eligibility. Changing this forces a new resource to be created. +* `justification` - (Required) Justification for why the principal is granted the role eligibility. Changing this forces a new resource to be created. +* `principal_id` - (Required) The object ID of the principal to granted the role eligibility. Changing this forces a new resource to be created. +* `role_definition_id` - (Required) The template ID (in the case of built-in roles) or object ID (in the case of custom roles) of the directory role you want to assign. Changing this forces a new resource to be created. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +*No additional attributes are exported* + +## Import + +Directory role eligibility schedule requests can be imported using the ID of the assignment, e.g. + +```shell +terraform import azuread_directory_role_eligibility_schedule_request.example 822ec710-4c9f-4f71-a27a-451759cc7522 +``` diff --git a/internal/services/directoryroles/client/client.go b/internal/services/directoryroles/client/client.go index 82b6f1f557..1f76338116 100644 --- a/internal/services/directoryroles/client/client.go +++ b/internal/services/directoryroles/client/client.go @@ -9,11 +9,12 @@ import ( ) type Client struct { - DirectoryObjectsClient *msgraph.DirectoryObjectsClient - DirectoryRolesClient *msgraph.DirectoryRolesClient - DirectoryRoleTemplatesClient *msgraph.DirectoryRoleTemplatesClient - RoleAssignmentsClient *msgraph.RoleAssignmentsClient - RoleDefinitionsClient *msgraph.RoleDefinitionsClient + DirectoryObjectsClient *msgraph.DirectoryObjectsClient + DirectoryRolesClient *msgraph.DirectoryRolesClient + DirectoryRoleTemplatesClient *msgraph.DirectoryRoleTemplatesClient + RoleAssignmentsClient *msgraph.RoleAssignmentsClient + RoleDefinitionsClient *msgraph.RoleDefinitionsClient + RoleEligibilityScheduleRequestClient *msgraph.RoleEligibilityScheduleRequestClient } func NewClient(o *common.ClientOptions) *Client { @@ -32,11 +33,15 @@ func NewClient(o *common.ClientOptions) *Client { roleDefinitionsClient := msgraph.NewRoleDefinitionsClient() o.ConfigureClient(&roleDefinitionsClient.BaseClient) + roleEligibilityScheduleRequestClient := msgraph.NewRoleEligibilityScheduleRequestClient() + o.ConfigureClient(&roleEligibilityScheduleRequestClient.BaseClient) + return &Client{ - DirectoryObjectsClient: directoryObjectsClient, - DirectoryRolesClient: directoryRolesClient, - DirectoryRoleTemplatesClient: directoryRoleTemplatesClient, - RoleAssignmentsClient: roleAssignmentsClient, - RoleDefinitionsClient: roleDefinitionsClient, + DirectoryObjectsClient: directoryObjectsClient, + DirectoryRolesClient: directoryRolesClient, + DirectoryRoleTemplatesClient: directoryRoleTemplatesClient, + RoleAssignmentsClient: roleAssignmentsClient, + RoleDefinitionsClient: roleDefinitionsClient, + RoleEligibilityScheduleRequestClient: roleEligibilityScheduleRequestClient, } } diff --git a/internal/services/directoryroles/directory_role_eligibility_schedule_request_resource.go b/internal/services/directoryroles/directory_role_eligibility_schedule_request_resource.go new file mode 100644 index 0000000000..eee9ee4cc4 --- /dev/null +++ b/internal/services/directoryroles/directory_role_eligibility_schedule_request_resource.go @@ -0,0 +1,168 @@ +package directoryroles + +import ( + "context" + "errors" + "fmt" + "log" + "net/http" + "time" + + "github.com/hashicorp/go-azure-sdk/sdk/odata" + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-azuread/internal/clients" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers" + "github.com/hashicorp/terraform-provider-azuread/internal/tf" + "github.com/hashicorp/terraform-provider-azuread/internal/utils" + "github.com/hashicorp/terraform-provider-azuread/internal/validate" + "github.com/manicminer/hamilton/msgraph" +) + +func directoryRoleEligibilityScheduleRequestResource() *schema.Resource { + return &schema.Resource{ + CreateContext: directoryRoleEligibilityScheduleRequestResourceCreate, + ReadContext: directoryRoleEligibilityScheduleRequestResourceRead, + DeleteContext: directoryRoleEligibilityScheduleRequestResourceDelete, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(5 * time.Minute), + Read: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), + }, + + Importer: tf.ValidateResourceIDPriorToImport(func(id string) error { + if _, err := uuid.ParseUUID(id); err != nil { + return fmt.Errorf("specified ID (%q) is not valid: %s", id, err) + } + return nil + }), + + Schema: map[string]*schema.Schema{ + "role_definition_id": { + Description: "The object ID of the directory role for this role eligibility schedule request", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validate.UUID, + }, + + "principal_id": { + Description: "The object ID of the member principal", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validate.UUID, + }, + + "directory_scope_id": { + Description: "Identifier of the directory object representing the scope of the role eligibility schedule request", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validate.NoEmptyStrings, + }, + + "justification": { + Description: "Justification for why the role is assigned", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validate.NoEmptyStrings, + }, + }, + } +} + +func directoryRoleEligibilityScheduleRequestResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).DirectoryRoles.RoleEligibilityScheduleRequestClient + + roleDefinitionId := d.Get("role_definition_id").(string) + principalId := d.Get("principal_id").(string) + justification := d.Get("justification").(string) + directoryScopeId := d.Get("directory_scope_id").(string) + + now := time.Now() + properties := msgraph.UnifiedRoleEligibilityScheduleRequest{ + Action: utils.String(msgraph.UnifiedRoleScheduleRequestActionAdminAssign), + RoleDefinitionId: &roleDefinitionId, + PrincipalId: &principalId, + Justification: &justification, + DirectoryScopeId: &directoryScopeId, + ScheduleInfo: &msgraph.RequestSchedule{ + StartDateTime: &now, + Expiration: &msgraph.ExpirationPattern{ + Type: utils.String(msgraph.ExpirationPatternTypeNoExpiration), + }, + }, + } + + roleEligibilityScheduleRequest, status, err := client.Create(ctx, properties) + if err != nil { + return tf.ErrorDiagF(err, "Eligibility schedule request for role %q to principal %q, received %d with error: %+v", roleDefinitionId, principalId, status, err) + } + if roleEligibilityScheduleRequest == nil || roleEligibilityScheduleRequest.ID == nil { + return tf.ErrorDiagF(errors.New("returned role roleEligibilityScheduleRequest ID was nil"), "API Error") + } + + d.SetId(*roleEligibilityScheduleRequest.ID) + + if err := helpers.WaitForUpdate(ctx, func(ctx context.Context) (*bool, error) { + defer func() { client.BaseClient.DisableRetries = false }() + client.BaseClient.DisableRetries = true + + resr, status, err := client.Get(ctx, *roleEligibilityScheduleRequest.ID, odata.Query{}) + if err != nil { + if status == http.StatusNotFound { + return utils.Bool(false), nil + } + return nil, err + } + return utils.Bool(resr != nil), nil + }); err != nil { + return tf.ErrorDiagF(err, "Waiting for role eligibility schedule request for %q to be created for directory role %q", principalId, roleDefinitionId) + } + + return directoryRoleEligibilityScheduleRequestResourceRead(ctx, d, meta) +} + +func directoryRoleEligibilityScheduleRequestResourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).DirectoryRoles.RoleEligibilityScheduleRequestClient + + id := d.Id() + roleEligibilityScheduleRequest, status, err := client.Get(ctx, id, odata.Query{}) + if err != nil { + if status == http.StatusNotFound { + log.Printf("[DEBUG] roleEligibilityScheduleRequest with ID %q was not found - removing from state", id) + d.SetId("") + return nil + } + return tf.ErrorDiagF(err, "Retrieving roleEligibilityScheduleRequest %q", id) + } + + tf.Set(d, "role_definition_id", roleEligibilityScheduleRequest.RoleDefinitionId) + tf.Set(d, "principal_id", roleEligibilityScheduleRequest.PrincipalId) + tf.Set(d, "justification", roleEligibilityScheduleRequest.Justification) + tf.Set(d, "directory_scope_id", roleEligibilityScheduleRequest.DirectoryScopeId) + + return nil +} + +func directoryRoleEligibilityScheduleRequestResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).DirectoryRoles.RoleEligibilityScheduleRequestClient + + id := d.Id() + roleEligibilityScheduleRequest, _, err := client.Get(ctx, id, odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Retrieving roleEligibilityScheduleRequest %q", id) + } + + roleEligibilityScheduleRequest.Action = utils.String(msgraph.UnifiedRoleScheduleRequestActionAdminRemove) + + if _, _, err := client.Create(ctx, *roleEligibilityScheduleRequest); err != nil { + return tf.ErrorDiagF(err, "Deleting role eligibility schedule request %q: %+v", d.Id(), err) + } + return nil +} diff --git a/internal/services/directoryroles/directory_role_eligibility_schedule_request_resource_test.go b/internal/services/directoryroles/directory_role_eligibility_schedule_request_resource_test.go new file mode 100644 index 0000000000..10e34a91af --- /dev/null +++ b/internal/services/directoryroles/directory_role_eligibility_schedule_request_resource_test.go @@ -0,0 +1,74 @@ +package directoryroles_test + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/hashicorp/go-azure-sdk/sdk/odata" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-azuread/internal/acceptance" + "github.com/hashicorp/terraform-provider-azuread/internal/acceptance/check" + "github.com/hashicorp/terraform-provider-azuread/internal/clients" + "github.com/hashicorp/terraform-provider-azuread/internal/utils" +) + +type RoleEligibilityScheduleRequestResource struct{} + +func TestAccRoleEligibilityScheduleRequest_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_directory_role_eligibility_schedule_request", "test") + r := RoleEligibilityScheduleRequestResource{} + + data.ResourceTestIgnoreDangling(t, r, []resource.TestStep{ + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + }) +} + +func (r RoleEligibilityScheduleRequestResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) { + client := clients.DirectoryRoles.RoleEligibilityScheduleRequestClient + + resr, status, err := client.Get(ctx, state.ID, odata.Query{}) + if err != nil { + fmt.Printf("%s, %v\n", err.Error(), status) + if status == http.StatusNotFound { + return nil, fmt.Errorf("Role Eligibility Schedule Request with ID %q does not exist", state.ID) + } + return nil, fmt.Errorf("failed to retrieve Role Eligibility Schedule Request with object ID %q: %+v", state.ID, err) + } + + return utils.Bool(resr.ID != nil && *resr.ID == state.ID), nil +} + +func (r RoleEligibilityScheduleRequestResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azuread" {} + +data "azuread_domains" "test" { + only_initial = true +} + +resource "azuread_user" "test" { + user_principal_name = "acctestManager.%[1]d@${data.azuread_domains.test.domains.0.domain_name}" + display_name = "acctestManager-%[1]d" + password = "%[2]s" +} + +resource "azuread_directory_role" "test" { + display_name = "Application Administrator" +} + +resource "azuread_directory_role_eligibility_schedule_request" "test" { + role_definition_id = azuread_directory_role.test.template_id + principal_id = azuread_user.test.object_id + directory_scope_id = "/" + justification = "abc" +} +`, data.RandomInteger, data.RandomPassword) +} diff --git a/internal/services/directoryroles/registration.go b/internal/services/directoryroles/registration.go index 5e27f18b62..b87bb407fd 100644 --- a/internal/services/directoryroles/registration.go +++ b/internal/services/directoryroles/registration.go @@ -32,9 +32,10 @@ func (r Registration) SupportedDataSources() map[string]*schema.Resource { // SupportedResources returns the supported Resources supported by this Service func (r Registration) SupportedResources() map[string]*schema.Resource { return map[string]*schema.Resource{ - "azuread_custom_directory_role": customDirectoryRoleResource(), - "azuread_directory_role": directoryRoleResource(), - "azuread_directory_role_assignment": directoryRoleAssignmentResource(), - "azuread_directory_role_member": directoryRoleMemberResource(), + "azuread_custom_directory_role": customDirectoryRoleResource(), + "azuread_directory_role": directoryRoleResource(), + "azuread_directory_role_assignment": directoryRoleAssignmentResource(), + "azuread_directory_role_member": directoryRoleMemberResource(), + "azuread_directory_role_eligibility_schedule_request": directoryRoleEligibilityScheduleRequestResource(), } }