Skip to content

Commit bb242ed

Browse files
authored
Added external_id & force options for service principals (#1293)
1 parent e97ff62 commit bb242ed

File tree

4 files changed

+152
-2
lines changed

4 files changed

+152
-2
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Version changelog
22

3+
## 0.5.7
4+
5+
* Added `external_id` and `force` attributes to `databricks_service_principal` resource ([#1293](https://github.com/databrickslabs/terraform-provider-databricks/pull/1293))
6+
37
## 0.5.6
48

59
* Added `databricks_views` data resource, making `databricks_tables` return only managed or external tables in Unity Catalog ([#1274](https://github.com/databrickslabs/terraform-provider-databricks/issues/1274)).

docs/resources/service_principal.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,13 @@ The following arguments are available:
5050

5151
* `application_id` - This is the application id of the given service principal and will be their form of access and identity. On other clouds than Azure this value is auto-generated.
5252
* `display_name` - (Required) This is an alias for the service principal and can be the full name of the service principal.
53+
* `external_id` - (Optional) ID of the service principal in an external identity provider.
5354
* `allow_cluster_create` - (Optional) Allow the service principal to have [cluster](cluster.md) create privileges. Defaults to false. More fine grained permissions could be assigned with [databricks_permissions](permissions.md#Cluster-usage) and `cluster_id` argument. Everyone without `allow_cluster_create` argument set, but with [permission to use](permissions.md#Cluster-Policy-usage) Cluster Policy would be able to create clusters, but within the boundaries of that specific policy.
5455
* `allow_instance_pool_create` - (Optional) Allow the service principal to have [instance pool](instance_pool.md) create privileges. Defaults to false. More fine grained permissions could be assigned with [databricks_permissions](permissions.md#Instance-Pool-usage) and [instance_pool_id](permissions.md#instance_pool_id) argument.
5556
* `databricks_sql_access` - (Optional) This is a field to allow the group to have access to [Databricks SQL](https://databricks.com/product/databricks-sql) feature through [databricks_sql_endpoint](sql_endpoint.md).
5657
* `workspace_access` - (Optional) This is a field to allow the group to have access to Databricks Workspace.
5758
* `active` - (Optional) Either service principal is active or not. True by default, but can be set to false in case of service principal deactivation with preserving service principal assets.
59+
* `force` - (Optional) Ignore `cannot create service principal: Service principal with application ID X already exists` errors and implicitly import the specific service principal into Terraform state, enforcing entitlements defined in the instance of resource. _This functionality is experimental_ and is designed to simplify corner cases, like Azure Active Directory synchronisation.
5860

5961
## Attribute Reference
6062

scim/resource_service_principal.go

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package scim
33
import (
44
"context"
55
"fmt"
6+
"net/http"
7+
"strings"
68

79
"github.com/databrickslabs/terraform-provider-databricks/common"
810

@@ -35,6 +37,20 @@ func (a ServicePrincipalsAPI) read(servicePrincipalID string) (sp User, err erro
3537
return
3638
}
3739

40+
func (a ServicePrincipalsAPI) filter(filter string) (u []User, err error) {
41+
var sps UserList
42+
req := map[string]string{}
43+
if filter != "" {
44+
req["filter"] = filter
45+
}
46+
err = a.client.Scim(a.context, http.MethodGet, "/preview/scim/v2/ServicePrincipals", req, &sps)
47+
if err != nil {
48+
return
49+
}
50+
u = sps.Resources
51+
return
52+
}
53+
3854
// Update replaces resource-friendly-entity
3955
func (a ServicePrincipalsAPI) Update(servicePrincipalID string, updateRequest User) error {
4056
servicePrincipal, err := a.read(servicePrincipalID)
@@ -62,11 +78,16 @@ func ResourceServicePrincipal() *schema.Resource {
6278
ApplicationID string `json:"application_id,omitempty" tf:"computed,force_new"`
6379
DisplayName string `json:"display_name,omitempty" tf:"computed"`
6480
Active bool `json:"active,omitempty"`
81+
ExternalID string `json:"external_id,omitempty" tf:"suppress_diff"`
6582
}
6683
servicePrincipalSchema := common.StructToSchema(entity{},
6784
func(m map[string]*schema.Schema) map[string]*schema.Schema {
6885
addEntitlementsToSchema(&m)
6986
m["active"].Default = true
87+
m["force"] = &schema.Schema{
88+
Type: schema.TypeBool,
89+
Optional: true,
90+
}
7091
return m
7192
})
7293
spFromData := func(d *schema.ResourceData) User {
@@ -77,15 +98,17 @@ func ResourceServicePrincipal() *schema.Resource {
7798
DisplayName: u.DisplayName,
7899
Active: u.Active,
79100
Entitlements: readEntitlementsFromData(d),
101+
ExternalID: u.ExternalID,
80102
}
81103
}
82104
return common.Resource{
83105
Schema: servicePrincipalSchema,
84106
Create: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
85107
sp := spFromData(d)
86-
servicePrincipal, err := NewServicePrincipalsAPI(ctx, c).Create(sp)
108+
spAPI := NewServicePrincipalsAPI(ctx, c)
109+
servicePrincipal, err := spAPI.Create(sp)
87110
if err != nil {
88-
return err
111+
return createForceOverridesManuallyAddedServicePrincipal(err, d, spAPI, sp)
89112
}
90113
d.SetId(servicePrincipal.ID)
91114
return nil
@@ -106,10 +129,33 @@ func ResourceServicePrincipal() *schema.Resource {
106129
DisplayName: d.Get("display_name").(string),
107130
Active: d.Get("active").(bool),
108131
Entitlements: readEntitlementsFromData(d),
132+
ExternalID: d.Get("external_id").(string),
109133
})
110134
},
111135
Delete: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
112136
return NewServicePrincipalsAPI(ctx, c).Delete(d.Id())
113137
},
114138
}.ToResource()
115139
}
140+
141+
func createForceOverridesManuallyAddedServicePrincipal(err error, d *schema.ResourceData, spAPI ServicePrincipalsAPI, u User) error {
142+
forceCreate := d.Get("force").(bool)
143+
if !forceCreate {
144+
return err
145+
}
146+
// corner-case for overriding manually provisioned service principals
147+
force := fmt.Sprintf("Service principal with application ID %s already exists.", u.ApplicationID)
148+
if err.Error() != force {
149+
return err
150+
}
151+
spList, err := spAPI.filter(fmt.Sprintf("applicationId eq '%s'", strings.ReplaceAll(u.ApplicationID, "'", "")))
152+
if err != nil {
153+
return err
154+
}
155+
if len(spList) == 0 {
156+
return fmt.Errorf("cannot find SP with ID %s for force import", u.ApplicationID)
157+
}
158+
sp := spList[0]
159+
d.SetId(sp.ID)
160+
return spAPI.Update(d.Id(), u)
161+
}

scim/resource_service_principal_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package scim
22

33
import (
44
"context"
5+
"fmt"
56
"os"
67
"testing"
78

@@ -372,3 +373,100 @@ func TestResourceServicePrincipalDelete_Error(t *testing.T) {
372373
}.Apply(t)
373374
require.Error(t, err, err)
374375
}
376+
377+
func TestCreateForceOverridesManuallyAddedServicePrincipalErrorNotMatched(t *testing.T) {
378+
d := ResourceUser().TestResourceData()
379+
d.Set("force", true)
380+
rerr := createForceOverridesManuallyAddedServicePrincipal(
381+
fmt.Errorf("nonsense"), d,
382+
NewServicePrincipalsAPI(context.Background(), &common.DatabricksClient{}), User{})
383+
assert.EqualError(t, rerr, "nonsense")
384+
}
385+
386+
func TestCreateForceOverwriteCannotListServicePrincipals(t *testing.T) {
387+
appID := "12344ca0-e1d7-45d1-951e-f4b93592f123"
388+
qa.HTTPFixturesApply(t, []qa.HTTPFixture{
389+
{
390+
Method: "GET",
391+
Resource: fmt.Sprintf("/api/2.0/preview/scim/v2/ServicePrincipals?filter=applicationId%%20eq%%20%%27%s%%27", appID),
392+
Status: 417,
393+
Response: common.APIError{
394+
Message: "cannot find service principal",
395+
},
396+
},
397+
}, func(ctx context.Context, client *common.DatabricksClient) {
398+
d := ResourceUser().TestResourceData()
399+
d.Set("force", true)
400+
err := createForceOverridesManuallyAddedServicePrincipal(
401+
fmt.Errorf("Service principal with application ID %s already exists.", appID),
402+
d, NewServicePrincipalsAPI(ctx, client), User{
403+
ApplicationID: appID,
404+
})
405+
assert.EqualError(t, err, "cannot find service principal")
406+
})
407+
}
408+
409+
func TestCreateForceOverwriteCannotListAccServicePrincipals(t *testing.T) {
410+
appID := "12344ca0-e1d7-45d1-951e-f4b93592f123"
411+
qa.HTTPFixturesApply(t, []qa.HTTPFixture{
412+
{
413+
Method: "GET",
414+
Resource: fmt.Sprintf("/api/2.0/preview/scim/v2/ServicePrincipals?filter=applicationId%%20eq%%20%%27%s%%27", appID),
415+
Response: UserList{
416+
TotalResults: 0,
417+
},
418+
},
419+
}, func(ctx context.Context, client *common.DatabricksClient) {
420+
d := ResourceUser().TestResourceData()
421+
d.Set("force", true)
422+
err := createForceOverridesManuallyAddedServicePrincipal(
423+
fmt.Errorf("Service principal with application ID %s already exists.", appID),
424+
d, NewServicePrincipalsAPI(ctx, client), User{
425+
ApplicationID: appID,
426+
})
427+
assert.EqualError(t, err, fmt.Sprintf("cannot find SP with ID %s for force import", appID))
428+
})
429+
}
430+
431+
func TestCreateForceOverwriteFindsAndSetsServicePrincipalID(t *testing.T) {
432+
appID := "12344ca0-e1d7-45d1-951e-f4b93592f123"
433+
qa.HTTPFixturesApply(t, []qa.HTTPFixture{
434+
{
435+
Method: "GET",
436+
Resource: fmt.Sprintf("/api/2.0/preview/scim/v2/ServicePrincipals?filter=applicationId%%20eq%%20%%27%s%%27", appID),
437+
Response: UserList{
438+
Resources: []User{
439+
{
440+
ID: "abc",
441+
},
442+
},
443+
},
444+
},
445+
{
446+
Method: "GET",
447+
Resource: "/api/2.0/preview/scim/v2/ServicePrincipals/abc",
448+
Response: User{
449+
ID: "abc",
450+
},
451+
},
452+
{
453+
Method: "PUT",
454+
Resource: "/api/2.0/preview/scim/v2/ServicePrincipals/abc",
455+
ExpectedRequest: User{
456+
Schemas: []URN{ServicePrincipalSchema},
457+
ApplicationID: appID,
458+
},
459+
},
460+
}, func(ctx context.Context, client *common.DatabricksClient) {
461+
d := ResourceUser().TestResourceData()
462+
d.Set("force", true)
463+
d.Set("application_id", appID)
464+
err := createForceOverridesManuallyAddedServicePrincipal(
465+
fmt.Errorf("Service principal with application ID %s already exists.", appID),
466+
d, NewServicePrincipalsAPI(ctx, client), User{
467+
ApplicationID: appID,
468+
})
469+
assert.NoError(t, err)
470+
assert.Equal(t, "abc", d.Id())
471+
})
472+
}

0 commit comments

Comments
 (0)