Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added databricks_service_principal_role resource that grants service principals access to instance profiles #1340

Merged
merged 6 commits into from
May 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions aws/resource_service_principal_role.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package aws

import (
"context"
"fmt"

"github.com/databrickslabs/terraform-provider-databricks/common"
"github.com/databrickslabs/terraform-provider-databricks/scim"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

// ResourceServicePrincipalRole binds service principal and instance profile
func ResourceServicePrincipalRole() *schema.Resource {
r := common.NewPairID("service_principal_id", "role").BindResource(common.BindResource{
CreateContext: func(ctx context.Context, servicePrincipalID, role string, c *common.DatabricksClient) error {
return scim.NewServicePrincipalsAPI(ctx, c).Patch(servicePrincipalID, scim.PatchRequest("add", "roles", role))
},
ReadContext: func(ctx context.Context, servicePrincipalID, roleARN string, c *common.DatabricksClient) error {
servicePrincipal, err := scim.NewServicePrincipalsAPI(ctx, c).Read(servicePrincipalID)
hasRole := scim.ComplexValues(servicePrincipal.Roles).HasValue(roleARN)
if err == nil && !hasRole {
return common.NotFound("Service Principal has no role")
}
return err
},
DeleteContext: func(ctx context.Context, servicePrincipalID, roleARN string, c *common.DatabricksClient) error {
return scim.NewServicePrincipalsAPI(ctx, c).Patch(servicePrincipalID, scim.PatchRequest(
"remove", fmt.Sprintf(`roles[value eq "%s"]`, roleARN), ""))
},
})
return r
}
134 changes: 134 additions & 0 deletions aws/resource_service_principal_role_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package aws

import (
"github.com/databrickslabs/terraform-provider-databricks/common"
"testing"

"github.com/databrickslabs/terraform-provider-databricks/scim"

"github.com/databrickslabs/terraform-provider-databricks/qa"
)

func TestResourceServicePrincipalRoleCreate(t *testing.T) {
qa.ResourceFixture{
Fixtures: []qa.HTTPFixture{
{
Method: "PATCH",
Resource: "/api/2.0/preview/scim/v2/ServicePrincipals/abc",
ExpectedRequest: scim.PatchRequest(
"add",
"roles",
"arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile"),
Response: scim.User{
ID: "abc",
},
},
{
Method: "GET",
Resource: "/api/2.0/preview/scim/v2/ServicePrincipals/abc",
Response: scim.User{
Schemas: []scim.URN{scim.ServicePrincipalSchema},
DisplayName: "ABC SP",
Roles: []scim.ComplexValue{
{
Value: "arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile",
},
},
ID: "abc",
},
},
},
Resource: ResourceServicePrincipalRole(),
State: map[string]interface{}{
"service_principal_id": "abc",
"role": "arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile",
},
Create: true,
}.ApplyAndExpectData(t, map[string]interface{}{"id": "abc|arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile"})
}

func TestResourceServicePrincipalRoleCreate_Error(t *testing.T) {
qa.ResourceFixture{
Fixtures: []qa.HTTPFixture{
{
Method: "PATCH",
Resource: "/api/2.0/preview/scim/v2/ServicePrincipals/abc",
Response: common.APIErrorBody{
ErrorCode: "INVALID_REQUEST",
Message: "Internal error happened",
},
Status: 400,
},
},
Resource: ResourceServicePrincipalRole(),
State: map[string]interface{}{
"service_principal_id": "abc",
"role": "arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile",
},
Create: true,
}.ExpectError(t, "Internal error happened")
}

func TestResourceServicePrincipalRoleRead(t *testing.T) {
qa.ResourceFixture{
Fixtures: []qa.HTTPFixture{
{
Method: "GET",
Resource: "/api/2.0/preview/scim/v2/ServicePrincipals/abc",
Response: scim.User{
Schemas: []scim.URN{scim.ServicePrincipalSchema},
DisplayName: "ABC SP",
Roles: []scim.ComplexValue{
{
Value: "arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile",
},
},
ID: "abc",
},
},
},
Resource: ResourceServicePrincipalRole(),
Read: true,
ID: "abc|arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile",
}.ApplyAndExpectData(t, map[string]interface{}{"id": "abc|arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile"})
}

func TestResourceServicePrincipalRoleRead_NoRole(t *testing.T) {
qa.ResourceFixture{
Fixtures: []qa.HTTPFixture{
{
Method: "GET",
Resource: "/api/2.0/preview/scim/v2/ServicePrincipals/abc",
Response: scim.User{
Schemas: []scim.URN{scim.ServicePrincipalSchema},
DisplayName: "ABC SP",
ID: "abc",
},
},
},
Resource: ResourceServicePrincipalRole(),
Read: true,
Removed: true,
ID: "abc|arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile",
}.ApplyNoError(t)
}

func TestResourceServicePrincipalRoleRead_NotFound(t *testing.T) {
qa.ResourceFixture{
Fixtures: []qa.HTTPFixture{
{
Method: "GET",
Resource: "/api/2.0/preview/scim/v2/ServicePrincipals/abc",
Response: common.APIErrorBody{
ErrorCode: "NOT_FOUND",
Message: "Item not found",
},
Status: 404,
},
},
Resource: ResourceServicePrincipalRole(),
Read: true,
Removed: true,
ID: "abc|arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile",
}.ApplyNoError(t)
}
51 changes: 51 additions & 0 deletions docs/resources/service_principal_role.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
subcategory: "Security"
---
# databricks_service_principal_role Resource

This resource allows you to attach a role or [databricks_instance_profile](instance_profile.md) (AWS) to a [databricks_service_principal](service_principal.md).

## Example Usage

Granting a service principal access to an instance profile

```hcl
resource "databricks_instance_profile" "instance_profile" {
instance_profile_arn = "my_instance_profile_arn"
}

resource "databricks_service_principal" "this" {
display_name = "My Service Principal"
}

resource "databricks_service_principal_role" "my_service_principal_instance_profile" {
service_principal_id = databricks_service_principal.this.id
role = databricks_instance_profile.instance_profile.id
}
```
## Argument Reference

The following arguments are supported:

* `service_principal_id` - (Required) This is the id of the [service principal](service_principal.md) resource.
* `role` - (Required) This is the id of the role or [instance profile](instance_profile.md) resource.

## Attribute Reference

In addition to all arguments above, the following attributes are exported:

* `id` - The id in the format `<service_principal_id>|<role>`.

## Import

-> **Note** Importing this resource is not currently supported.

## Related Resources

The following resources are often used in the same context:

* [End to end workspace management](../guides/workspace-management.md) guide.
* [databricks_user_role](user_instance_profile.md) to attach role or [databricks_instance_profile](instance_profile.md) (AWS) to [databricks_user](user.md).
* [databricks_group_instance_profile](group_instance_profile.md) to attach [databricks_instance_profile](instance_profile.md) (AWS) to [databricks_group](group.md).
* [databricks_group_member](group_member.md) to attach [users](user.md) and [groups](group.md) as group members.
* [databricks_instance_profile](instance_profile.md) to manage AWS EC2 instance profiles that users can launch [databricks_cluster](cluster.md) and access data, like [databricks_mount](mount.md).
1 change: 1 addition & 0 deletions provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ func DatabricksProvider() *schema.Provider {
"databricks_secret_scope": secrets.ResourceSecretScope(),
"databricks_secret_acl": secrets.ResourceSecretACL(),
"databricks_service_principal": scim.ResourceServicePrincipal(),
"databricks_service_principal_role": aws.ResourceServicePrincipalRole(),
"databricks_sql_dashboard": sql.ResourceDashboard(),
"databricks_sql_endpoint": sql.ResourceSQLEndpoint(),
"databricks_sql_global_config": sql.ResourceSQLGlobalConfig(),
Expand Down
11 changes: 8 additions & 3 deletions scim/resource_service_principal.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func (a ServicePrincipalsAPI) Create(rsp User) (sp User, err error) {
return sp, err
}

func (a ServicePrincipalsAPI) read(servicePrincipalID string) (sp User, err error) {
func (a ServicePrincipalsAPI) Read(servicePrincipalID string) (sp User, err error) {
servicePrincipalPath := fmt.Sprintf("/preview/scim/v2/ServicePrincipals/%v", servicePrincipalID)
err = a.client.Scim(a.context, "GET", servicePrincipalPath, nil, &sp)
return
Expand All @@ -51,9 +51,14 @@ func (a ServicePrincipalsAPI) filter(filter string) (u []User, err error) {
return
}

// Patch updates resource-friendly entity
func (a ServicePrincipalsAPI) Patch(servicePrincipalID string, r patchRequest) error {
return a.client.Scim(a.context, http.MethodPatch, fmt.Sprintf("/preview/scim/v2/ServicePrincipals/%v", servicePrincipalID), r, nil)
}

// Update replaces resource-friendly-entity
func (a ServicePrincipalsAPI) Update(servicePrincipalID string, updateRequest User) error {
servicePrincipal, err := a.read(servicePrincipalID)
servicePrincipal, err := a.Read(servicePrincipalID)
if err != nil {
return err
}
Expand Down Expand Up @@ -114,7 +119,7 @@ func ResourceServicePrincipal() *schema.Resource {
return nil
},
Read: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
sp, err := NewServicePrincipalsAPI(ctx, c).read(d.Id())
sp, err := NewServicePrincipalsAPI(ctx, c).Read(d.Id())
if err != nil {
return err
}
Expand Down