From 5ec1fb51d6ba6db041088b45b67acaa667f9c6f0 Mon Sep 17 00:00:00 2001 From: Iain Adams Date: Wed, 22 Feb 2023 17:09:48 +0000 Subject: [PATCH] fixed #1031 adds provision on demand --- ...synchronization_job_provision_on_demand.md | 105 +++++++++++++ .../services/synchronization/registration.go | 5 +- .../synchronization/synchronization.go | 35 +++++ ...zation_job_provision_on_demand_resource.go | 140 ++++++++++++++++++ 4 files changed, 283 insertions(+), 2 deletions(-) create mode 100644 docs/resources/synchronization_job_provision_on_demand.md create mode 100644 internal/services/synchronization/synchronization_job_provision_on_demand_resource.go diff --git a/docs/resources/synchronization_job_provision_on_demand.md b/docs/resources/synchronization_job_provision_on_demand.md new file mode 100644 index 0000000000..de8dae4de4 --- /dev/null +++ b/docs/resources/synchronization_job_provision_on_demand.md @@ -0,0 +1,105 @@ +--- +subcategory: "Synchronization" +--- + +# Resource: azuread_synchronization_job_provision_on_demand + +Manages synchronization job on demand provisioning associated with a service principal (enterprise 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: `Synchronization.ReadWrite.All` + +## 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 +} + +data "azuread_application_template" "example" { + display_name = "Azure Databricks SCIM Provisioning Connector" +} + +resource "azuread_application" "example" { + display_name = "example" + template_id = data.azuread_application_template.example.template_id + feature_tags { + enterprise = true + gallery = true + } +} + +resource "azuread_service_principal" "example" { + application_id = azuread_application.example.application_id + use_existing = true +} + +resource "azuread_synchronization_secret" "example" { + service_principal_id = azuread_service_principal.example.id + + credential { + key = "BaseAddress" + value = "https://adb-example.azuredatabricks.net/api/2.0/preview/scim" + } + credential { + key = "SecretToken" + value = "some-token" + } +} + +resource "azuread_synchronization_job" "example" { + service_principal_id = azuread_service_principal.example.id + template_id = "dataBricks" + enabled = true +} + +resource "azuread_synchronization_job_provision_on_demand" "example" { + service_principal_id = azuread_service_principal.example.id + synchronization_job_id = azuread_synchronization_job.example.id + parameter { + # see specific synchronization schema for rule id https://learn.microsoft.com/en-us/graph/api/synchronization-synchronizationschema-get?view=graph-rest-beta + rule_id = "" + subject { + object_id = azuread_group.example.object_id + object_type_name = "Group" + } + } +} + +``` + +## Argument Reference + +The following arguments are supported: + + +- `synchronization_job_id` (Required) Identifier of the synchronization template this job is based on. +- `parameter` (Required) One or more `parameter` blocks as documented below. +- `service_principal_id` (Required) The object ID of the service principal for the synchronization job. + +--- + +`parameter` block supports the following: + +* `rule_id` (Required) The identifier of the synchronizationRule to be applied. This rule ID is defined in the schema for a given synchronization job or template. +* `subject` (Required) One or more `subject` blocks as documented below. + +--- + +`subject` block supports the following: + +* `object_id` (String) The identifier of an object to which a synchronizationJob is to be applied. +* `object_type_name` (String) The type of the object to which a synchronizationJob is to be applied. + +## Import + +This resource does not support importing. diff --git a/internal/services/synchronization/registration.go b/internal/services/synchronization/registration.go index 51eca7e9b4..da1b7c7821 100644 --- a/internal/services/synchronization/registration.go +++ b/internal/services/synchronization/registration.go @@ -35,8 +35,9 @@ func (r Registration) SupportedDataSources() map[string]*pluginsdk.Resource { // SupportedResources returns the supported Resources supported by this Service func (r Registration) SupportedResources() map[string]*pluginsdk.Resource { return map[string]*pluginsdk.Resource{ - "azuread_synchronization_job": synchronizationJobResource(), - "azuread_synchronization_secret": synchronizationSecretResource(), + "azuread_synchronization_job": synchronizationJobResource(), + "azuread_synchronization_job_provision_on_demand": synchronizationJobProvisionOnDemandResource(), + "azuread_synchronization_secret": synchronizationSecretResource(), } } diff --git a/internal/services/synchronization/synchronization.go b/internal/services/synchronization/synchronization.go index bc9ee184f1..484fd6af07 100644 --- a/internal/services/synchronization/synchronization.go +++ b/internal/services/synchronization/synchronization.go @@ -48,6 +48,41 @@ func expandSynchronizationSecretKeyStringValuePair(in []interface{}) *[]msgraph. return &result } +func expandSynchronizationJobApplicationParameters(in []interface{}) *[]msgraph.SynchronizationJobApplicationParameters { + result := make([]msgraph.SynchronizationJobApplicationParameters, 0) + + for _, raw := range in { + if raw == nil { + continue + } + item := raw.(map[string]interface{}) + + result = append(result, msgraph.SynchronizationJobApplicationParameters{ + Subjects: expandSynchronizationJobSubject(item["subjects"].([]interface{})), + RuleId: pointer.To(item["rule_id"].(string)), + }) + } + + return &result +} + +func expandSynchronizationJobSubject(in []interface{}) *[]msgraph.SynchronizationJobSubject { + result := make([]msgraph.SynchronizationJobSubject, 0) + for _, raw := range in { + if raw == nil { + continue + } + item := raw.(map[string]interface{}) + + result = append(result, msgraph.SynchronizationJobSubject{ + ObjectId: pointer.To(item["object_id"].(string)), + ObjectTypeName: pointer.To(item["object_type_name"].(string)), + }) + } + + return &result +} + func flattenSynchronizationSchedule(in *msgraph.SynchronizationSchedule) []map[string]interface{} { if in == nil { return []map[string]interface{}{} diff --git a/internal/services/synchronization/synchronization_job_provision_on_demand_resource.go b/internal/services/synchronization/synchronization_job_provision_on_demand_resource.go new file mode 100644 index 0000000000..5adcb2f46f --- /dev/null +++ b/internal/services/synchronization/synchronization_job_provision_on_demand_resource.go @@ -0,0 +1,140 @@ +package synchronization + +import ( + "context" + "errors" + "github.com/hashicorp/go-azure-sdk/sdk/odata" + "net/http" + "time" + + "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/tf" + "github.com/hashicorp/terraform-provider-azuread/internal/tf/validation" + "github.com/manicminer/hamilton/msgraph" +) + +func synchronizationJobProvisionOnDemandResource() *schema.Resource { + return &schema.Resource{ + CreateContext: synchronizationProvisionOnDemandResourceCreate, + ReadContext: synchronizationProvisionOnDemandResourceRead, + DeleteContext: synchronizationProvisionOnDemandResourceDelete, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(15 * time.Minute), + Read: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), + }, + SchemaVersion: 0, + + Schema: map[string]*schema.Schema{ + "service_principal_id": { + Description: "The object ID of the service principal for which this synchronization job should be provisioned", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validation.ValidateDiag(validation.IsUUID), + }, + "synchronization_job_id": { + Description: "The identifier for the synchronization jop.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "parameter": { + Description: "Represents the objects that will be provisioned and the synchronization rules executed. The resource is primarily used for on-demand provisioning.", + Type: schema.TypeList, + Required: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "rule_id": { + Description: "The identifier of the synchronizationRule to be applied. This rule ID is defined in the schema for a given synchronization job or template.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "subject": { + Description: "The identifiers of one or more objects to which a synchronizationJob is to be applied.", + Type: schema.TypeList, + Required: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "object_id": { + Description: "The identifier of an object to which a synchronization Job is to be applied. Can be one of the following: (1) An onPremisesDistinguishedName for synchronization from Active Directory to Azure AD. (2) The user ID for synchronization from Azure AD to a third-party. (3) The Worker ID of the Workday worker for synchronization from Workday to either Active Directory or Azure AD.", + Type: schema.TypeString, + Required: true, + }, + "object_type_name": { + Description: "The type of the object to which a synchronization Job is to be applied. Can be one of the following: `user` for synchronizing between Active Directory and Azure AD, `User` for synchronizing a user between Azure AD and a third-party application, `Worker` for synchronization a user between Workday and either Active Directory or Azure AD, `Group` for synchronizing a group between Azure AD and a third-party application.", + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{"Group", "user", "User", "Worker"}, false), + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func synchronizationProvisionOnDemandResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).ServicePrincipals.SynchronizationJobClient + spClient := meta.(*clients.Client).ServicePrincipals.ServicePrincipalsClient + objectId := d.Get("service_principal_id").(string) + jobId := d.Get("job_id").(string) + + tf.LockByName(servicePrincipalResourceName, objectId) + defer tf.UnlockByName(servicePrincipalResourceName, objectId) + + servicePrincipal, status, err := spClient.Get(ctx, objectId, odata.Query{}) + if err != nil { + if status == http.StatusNotFound { + return tf.ErrorDiagPathF(nil, "service_principal_id", "Service principal with object ID %q was not found", objectId) + } + return tf.ErrorDiagPathF(err, "service_principal_id", "Retrieving service principal with object ID %q", objectId) + } + if servicePrincipal == nil || servicePrincipal.ID() == nil { + return tf.ErrorDiagF(errors.New("nil service principal or service principal with nil ID was returned"), "API error retrieving service principal with object ID %q", objectId) + } + + job, status, err := client.Get(ctx, jobId, objectId) + if err != nil { + if status == http.StatusNotFound { + return tf.ErrorDiagPathF(nil, "job_id", "Job with object ID %q was not found for service principle %q", jobId, objectId) + } + return tf.ErrorDiagPathF(err, "job_id", "Retrieving job with object ID %q for service principle %q", jobId, objectId) + } + if job == nil || job.ID == nil { + return tf.ErrorDiagF(errors.New("nil job or job with nil ID was returned"), "API error retrieving job with object ID %q/%s", objectId, jobId) + } + // Create a new synchronization job + synchronizationProvisionOnDemand := &msgraph.SynchronizationJobProvisionOnDemand{ + Parameters: expandSynchronizationJobApplicationParameters(d.Get("parameters").([]interface{})), + } + + _, err = client.ProvisionOnDemand(ctx, jobId, synchronizationProvisionOnDemand, *servicePrincipal.ID()) + if err != nil { + return tf.ErrorDiagF(err, "Creating synchronization job for service principal ID %q", *servicePrincipal.ID()) + } + + id, _ := uuid.GenerateUUID() + d.SetId(id) + + return synchronizationProvisionOnDemandResourceRead(ctx, d, meta) +} + +func synchronizationProvisionOnDemandResourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return nil +} + +func synchronizationProvisionOnDemandResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return nil +}