Skip to content

Commit

Permalink
fixed hashicorp#1031 adds provision on demand
Browse files Browse the repository at this point in the history
  • Loading branch information
iwarapter committed Mar 1, 2024
1 parent 79f6519 commit 559acda
Show file tree
Hide file tree
Showing 5 changed files with 375 additions and 2 deletions.
109 changes: 109 additions & 0 deletions docs/resources/synchronization_job_provision_on_demand.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
---
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.

## Attributes Reference

No additional attributes are exported.

## Import

This resource does not support importing.
5 changes: 3 additions & 2 deletions internal/services/synchronization/registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
}

Expand Down
35 changes: 35 additions & 0 deletions internal/services/synchronization/synchronization.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}{}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package synchronization

import (
"context"
"errors"
"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/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("synchronization_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("parameter").([]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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package synchronization_test

import (
"context"
"fmt"
"regexp"
"testing"

"github.com/hashicorp/go-azure-helpers/lang/pointer"

"github.com/hashicorp/terraform-plugin-testing/terraform"
"github.com/hashicorp/terraform-provider-azuread/internal/acceptance"
"github.com/hashicorp/terraform-provider-azuread/internal/clients"
)

type SynchronizationJobProvisionOnDemandResource struct{}

func TestAccSynchronizationJobProvisionOnDemand_basic(t *testing.T) {
data := acceptance.BuildTestData(t, "azuread_synchronization_job_provision_on_demand", "test")
r := SynchronizationJobProvisionOnDemandResource{}

data.ResourceTest(t, r, []acceptance.TestStep{
{
// The provisioned app isn't actually integrated so this will never work
Config: r.basic(data),
ExpectError: regexp.MustCompile("CredentialsMissing: Please configure provisioning by providing your admin credentials then retry the provision on-demand."),
},
})
}

func (r SynchronizationJobProvisionOnDemandResource) Exists(ctx context.Context, client *clients.Client, state *terraform.InstanceState) (*bool, error) {
return pointer.To(true), nil
}

func (SynchronizationJobProvisionOnDemandResource) template(data acceptance.TestData) string {
return fmt.Sprintf(`
provider "azuread" {}
data "azuread_client_config" "test" {}
data "azuread_application_template" "test" {
display_name = "Azure Databricks SCIM Provisioning Connector"
}
resource "azuread_application" "test" {
display_name = "acctestSynchronizationJob-%[1]d"
owners = [data.azuread_client_config.test.object_id]
template_id = data.azuread_application_template.test.template_id
}
resource "azuread_service_principal" "test" {
application_id = azuread_application.test.application_id
owners = [data.azuread_client_config.test.object_id]
use_existing = true
}
resource "azuread_synchronization_job" "test" {
service_principal_id = azuread_service_principal.test.id
template_id = "dataBricks"
}
resource "azuread_group" "test" {
display_name = "acctestGroup-%[1]d"
security_enabled = true
}
`, data.RandomInteger)
}

func (r SynchronizationJobProvisionOnDemandResource) basic(data acceptance.TestData) string {
return fmt.Sprintf(`
%[1]s
resource "azuread_synchronization_job_provision_on_demand" "test" {
service_principal_id = azuread_service_principal.test.id
synchronization_job_id = trimprefix(azuread_synchronization_job.test.id, "${azuread_service_principal.test.id}/job/")
parameter {
rule_id = "03f7d90d-bf71-41b1-bda6-aaf0ddbee5d8" //no api to check this so assuming the rule id is the same globally :finger_crossed:
subject {
object_id = azuread_group.test.id
object_type_name = "Group"
}
}
}
`, r.template(data))
}

0 comments on commit 559acda

Please sign in to comment.