Skip to content

Commit

Permalink
Merge pull request #676 from hashicorp/r/delegated-permission-grant
Browse files Browse the repository at this point in the history
New resource: azuread_service_principal_delegated_permission_grant
  • Loading branch information
manicminer authored Nov 25, 2021
2 parents d17fa04 + 786d21f commit cd5808e
Show file tree
Hide file tree
Showing 13 changed files with 1,274 additions and 29 deletions.
128 changes: 128 additions & 0 deletions docs/resources/service_principal_delegated_permission_grant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
---
subcategory: "Delegated Permission Grants"
---

# Resource: azuread_service_principal_delegated_permission_grant

Manages a delegated permission grant for a service principal, on behalf of a single user, or all users.

## API Permissions

The following API permissions are required in order to use this resource.

When authenticated with a service principal, this resource requires the following application role: `Directory.ReadWrite.All`

When authenticated with a user principal, this resource requires one the following directory role: `Global Administrator`

## Example Usage

*Delegated permission grant for all users*

```terraform
data "azuread_application_published_app_ids" "well_known" {}
resource "azuread_service_principal" "msgraph" {
application_id = data.azuread_application_published_app_ids.well_known.result.MicrosoftGraph
use_existing = true
}
resource "azuread_application" "example" {
display_name = "example"
required_resource_access {
resource_app_id = data.azuread_application_published_app_ids.well_known.result.MicrosoftGraph
resource_access {
id = azuread_service_principal.msgraph.oauth2_permission_scope_ids["openid"]
type = "Scope"
}
resource_access {
id = azuread_service_principal.msgraph.oauth2_permission_scope_ids["User.Read"]
type = "Scope"
}
}
}
resource "azuread_service_principal" "example" {
application_id = azuread_application.example.application_id
}
resource "azuread_service_principal_delegated_permission_grant" "example" {
service_principal_object_id = azuread_service_principal.example.object_id
resource_service_principal_object_id = azuread_service_principal.msgraph.object_id
claim_values = ["openid", "User.Read.All"]
}
```

*Delegated permission grant for a single user*

```terraform
data "azuread_application_published_app_ids" "well_known" {}
resource "azuread_service_principal" "msgraph" {
application_id = data.azuread_application_published_app_ids.well_known.result.MicrosoftGraph
use_existing = true
}
resource "azuread_application" "example" {
display_name = "example"
required_resource_access {
resource_app_id = data.azuread_application_published_app_ids.well_known.result.MicrosoftGraph
resource_access {
id = azuread_service_principal.msgraph.oauth2_permission_scope_ids["openid"]
type = "Scope"
}
resource_access {
id = azuread_service_principal.msgraph.oauth2_permission_scope_ids["User.Read"]
type = "Scope"
}
}
}
resource "azuread_service_principal" "example" {
application_id = azuread_application.example.application_id
}
resource "azuread_user" "example" {
display_name = "J. Doe"
user_principal_name = "[email protected]"
mail_nickname = "jdoe"
password = "SecretP@sswd99!"
}
resource "azuread_service_principal_delegated_permission_grant" "example" {
service_principal_object_id = azuread_service_principal.example.object_id
resource_service_principal_object_id = azuread_service_principal.msgraph.object_id
claim_values = ["openid", "User.Read.All"]
user_object_id = azuread_user.example.object_id
}
```

## Argument Reference

The following arguments are supported:

* `claim_values` - (Required) - A set of claim values for delegated permission scopes which should be included in access tokens for the resource.
* `resource_service_principal_object_id` - (Required) The object ID of the service principal representing the resource to be accessed. Changing this forces a new resource to be created.
* `service_principal_object_id` - (Required) The object ID of the service principal for which this delegated permission grant should be created. Changing this forces a new resource to be created.
* `user_object_id` - (Optional) - The object ID of the user on behalf of whom the service principal is authorized to access the resource. When omitted, the delegated permission grant will be consented for all users. Changing this forces a new resource to be created.

-> **Granting Admin Consent** To grant admin consent for the service principal to impersonate all users, just omit the `user_object_id` property.

## Attributes Reference

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

* `id` - The ID of the delegated permission grant.

## Import

Delegated permission grants can be imported using their ID, e.g.

```shell
terraform import azuread_service_principal_delegated_permission_grant.example aaBBcDDeFG6h5JKLMN2PQrrssTTUUvWWxxxxxyyyzzz
```
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ require (
github.com/hashicorp/terraform-plugin-sdk/v2 v2.8.0
github.com/hashicorp/yamux v0.0.0-20210316155119-a95892c5f864 // indirect
github.com/klauspost/compress v1.12.2 // indirect
github.com/manicminer/hamilton v0.35.0
github.com/manicminer/hamilton v0.36.0
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -299,8 +299,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/manicminer/hamilton v0.35.0 h1:K72BHXLhlO+H/evI5iburLDfVe19bDaXO+hEIQEVkdg=
github.com/manicminer/hamilton v0.35.0/go.mod h1:IOYn2Dc9SUiZ7Ryw6c8Ay795vPPMnrCZe3MktS447dc=
github.com/manicminer/hamilton v0.36.0 h1:HBH1yJB2nA0d4ZebF9R8LSZMwkyujNUQr4mnIthUKE4=
github.com/manicminer/hamilton v0.36.0/go.mod h1:IOYn2Dc9SUiZ7Ryw6c8Ay795vPPMnrCZe3MktS447dc=
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func appRoleAssignmentResourceCreate(ctx context.Context, d *schema.ResourceData
if status == http.StatusNotFound {
return tf.ErrorDiagPathF(err, "principal_object_id", "Service principal not found for resource (Object ID: %q)", resourceId)
}
return tf.ErrorDiagF(err, "Could not retrieve service principal for resource (Object ID: %q)", principalId)
return tf.ErrorDiagF(err, "Could not retrieve service principal for resource (Object ID: %q)", resourceId)
}
properties := msgraph.AppRoleAssignment{
AppRoleId: utils.String(appRoleId),
Expand Down
13 changes: 9 additions & 4 deletions internal/services/serviceprincipals/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,24 @@ import (
)

type Client struct {
DirectoryObjectsClient *msgraph.DirectoryObjectsClient
ServicePrincipalsClient *msgraph.ServicePrincipalsClient
DelegatedPermissionGrantsClient *msgraph.DelegatedPermissionGrantsClient
DirectoryObjectsClient *msgraph.DirectoryObjectsClient
ServicePrincipalsClient *msgraph.ServicePrincipalsClient
}

func NewClient(o *common.ClientOptions) *Client {
delegatedPermissionGrantsClient := msgraph.NewDelegatedPermissionGrantsClient(o.TenantID)
o.ConfigureClient(&delegatedPermissionGrantsClient.BaseClient)

directoryObjectsClient := msgraph.NewDirectoryObjectsClient(o.TenantID)
o.ConfigureClient(&directoryObjectsClient.BaseClient)

servicePrincipalsClient := msgraph.NewServicePrincipalsClient(o.TenantID)
o.ConfigureClient(&servicePrincipalsClient.BaseClient)

return &Client{
DirectoryObjectsClient: directoryObjectsClient,
ServicePrincipalsClient: servicePrincipalsClient,
DelegatedPermissionGrantsClient: delegatedPermissionGrantsClient,
DirectoryObjectsClient: directoryObjectsClient,
ServicePrincipalsClient: servicePrincipalsClient,
}
}
7 changes: 4 additions & 3 deletions internal/services/serviceprincipals/registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ 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_service_principal": servicePrincipalResource(),
"azuread_service_principal_certificate": servicePrincipalCertificateResource(),
"azuread_service_principal_password": servicePrincipalPasswordResource(),
"azuread_service_principal": servicePrincipalResource(),
"azuread_service_principal_certificate": servicePrincipalCertificateResource(),
"azuread_service_principal_delegated_permission_grant": servicePrincipalDelegatedPermissionGrantResource(),
"azuread_service_principal_password": servicePrincipalPasswordResource(),
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package serviceprincipals

import (
"context"
"errors"
"log"
"net/http"
"time"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/manicminer/hamilton/msgraph"
"github.com/manicminer/hamilton/odata"

"github.com/hashicorp/terraform-provider-azuread/internal/clients"
"github.com/hashicorp/terraform-provider-azuread/internal/tf"
"github.com/hashicorp/terraform-provider-azuread/internal/utils"
"github.com/hashicorp/terraform-provider-azuread/internal/validate"
)

func servicePrincipalDelegatedPermissionGrantResource() *schema.Resource {
return &schema.Resource{
CreateContext: servicePrincipalDelegatedPermissionGrantResourceCreate,
UpdateContext: servicePrincipalDelegatedPermissionGrantResourceUpdate,
ReadContext: servicePrincipalDelegatedPermissionGrantResourceRead,
DeleteContext: servicePrincipalDelegatedPermissionGrantResourceDelete,

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),
},

Schema: map[string]*schema.Schema{
"claim_values": {
Description: "A set of claim values for delegated permission scopes which should be included in access tokens for the resource",
Type: schema.TypeSet,
Required: true,
MinItems: 1,
Elem: &schema.Schema{
Type: schema.TypeString,
ValidateDiagFunc: validate.NoEmptyStrings,
},
},

"resource_service_principal_object_id": {
Description: "The object ID of the service principal representing the resource to be accessed",
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateDiagFunc: validate.UUID,
},

"service_principal_object_id": {
Description: "The object ID of the service principal for which this delegated permission grant should be created",
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateDiagFunc: validate.UUID,
},

"user_object_id": {
Description: "The object ID of the user on behalf of whom the service principal is authorized to access the resource",
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateDiagFunc: validate.UUID,
},
},
}
}

func servicePrincipalDelegatedPermissionGrantResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*clients.Client).ServicePrincipals.DelegatedPermissionGrantsClient
servicePrincipalsClient := meta.(*clients.Client).ServicePrincipals.ServicePrincipalsClient

servicePrincipalId := d.Get("service_principal_object_id").(string)
resourceId := d.Get("resource_service_principal_object_id").(string)

if _, status, err := servicePrincipalsClient.Get(ctx, servicePrincipalId, odata.Query{}); err != nil {
if status == http.StatusNotFound {
return tf.ErrorDiagPathF(err, "principal_object_id", "Service principal with object ID %q was not found)", servicePrincipalId)
}
return tf.ErrorDiagF(err, "Could not retrieve service principal with object ID %q", servicePrincipalId)
}

if _, status, err := servicePrincipalsClient.Get(ctx, resourceId, odata.Query{}); err != nil {
if status == http.StatusNotFound {
return tf.ErrorDiagPathF(err, "principal_object_id", "Service principal not found for resource (Object ID: %q)", resourceId)
}
return tf.ErrorDiagF(err, "Could not retrieve service principal for resource (Object ID: %q)", resourceId)
}

properties := msgraph.DelegatedPermissionGrant{
ClientId: utils.String(servicePrincipalId),
ResourceId: utils.String(resourceId),
Scopes: tf.ExpandStringSlicePtr(d.Get("claim_values").(*schema.Set).List()),
}

if v, ok := d.GetOk("user_object_id"); ok && v.(string) != "" {
properties.PrincipalId = utils.String(v.(string))
properties.ConsentType = utils.String(msgraph.DelegatedPermissionGrantConsentTypePrincipal)
} else {
properties.ConsentType = utils.String(msgraph.DelegatedPermissionGrantConsentTypeAllPrincipals)
}

delegatedPermissionGrant, _, err := client.Create(ctx, properties)
if err != nil {
return tf.ErrorDiagF(err, "Could not create delegated permission grant")
}

if delegatedPermissionGrant.Id == nil || *delegatedPermissionGrant.Id == "" {
return tf.ErrorDiagF(errors.New("ID returned for delegated permission grant is nil"), "Bad API response")
}

d.SetId(*delegatedPermissionGrant.Id)

return servicePrincipalDelegatedPermissionGrantResourceRead(ctx, d, meta)
}

func servicePrincipalDelegatedPermissionGrantResourceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*clients.Client).ServicePrincipals.DelegatedPermissionGrantsClient

properties := msgraph.DelegatedPermissionGrant{
Id: utils.String(d.Id()),
Scopes: tf.ExpandStringSlicePtr(d.Get("claim_values").(*schema.Set).List()),
}

if _, err := client.Update(ctx, properties); err != nil {
return tf.ErrorDiagF(err, "Could not update delegated permission grant")
}

return servicePrincipalDelegatedPermissionGrantResourceRead(ctx, d, meta)
}

func servicePrincipalDelegatedPermissionGrantResourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*clients.Client).ServicePrincipals.DelegatedPermissionGrantsClient

delegatedPermissionGrant, status, err := client.Get(ctx, d.Id(), odata.Query{})
if err != nil {
if status == http.StatusNoContent {
log.Printf("[DEBUG] Delegated Permission Grant with ID %q was not found - removing from state", d.Id())
d.SetId("")
return nil
}
return tf.ErrorDiagPathF(err, "id", "Retrieving Delegated Permission Grant with ID %q", d.Id())
}

tf.Set(d, "claim_values", delegatedPermissionGrant.Scopes)
tf.Set(d, "resource_service_principal_object_id", delegatedPermissionGrant.ResourceId)
tf.Set(d, "service_principal_object_id", delegatedPermissionGrant.ClientId)
tf.Set(d, "user_object_id", delegatedPermissionGrant.PrincipalId)

return nil
}

func servicePrincipalDelegatedPermissionGrantResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*clients.Client).ServicePrincipals.DelegatedPermissionGrantsClient

id := d.Id()

if status, err := client.Delete(ctx, id); err != nil {
return tf.ErrorDiagPathF(err, "id", "Deleting delegated permission grant with ID %q, got status %d", id, status)
}

return nil
}
Loading

0 comments on commit cd5808e

Please sign in to comment.