From b0244eca8fe63d9f27e3d86ee1d0b459362ef6cd Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Fri, 28 May 2021 00:46:33 +0100 Subject: [PATCH] Bugfix: enable specifying metadata for app and SP passwords - Can now set display_name, start_date, end_date and end_date_relative - Update Hamilton for this, and another fix to mitigate access token refresh race --- docs/guides/microsoft-graph.md | 12 -- docs/resources/application_password.md | 2 +- docs/resources/service_principal_password.md | 2 +- go.mod | 2 +- go.sum | 4 +- .../application_password_resource_msgraph.go | 20 --- .../application_password_resource_test.go | 64 +++++++ ...ice_principal_password_resource_msgraph.go | 20 --- ...ervice_principal_password_resource_test.go | 64 +++++++ .../manicminer/hamilton/auth/cache.go | 9 +- .../hamilton/msgraph/app_role_assignments.go | 52 ++++-- .../hamilton/msgraph/applications.go | 59 ++++++- .../manicminer/hamilton/msgraph/groups.go | 71 +++++++- .../manicminer/hamilton/msgraph/models.go | 160 ++++++++++-------- .../hamilton/msgraph/serviceprincipals.go | 94 +++++++++- .../manicminer/hamilton/msgraph/users.go | 52 ++++++ vendor/modules.txt | 2 +- 17 files changed, 537 insertions(+), 152 deletions(-) diff --git a/docs/guides/microsoft-graph.md b/docs/guides/microsoft-graph.md index 73cf4c8e6..66c545131 100644 --- a/docs/guides/microsoft-graph.md +++ b/docs/guides/microsoft-graph.md @@ -180,14 +180,8 @@ The deprecated field `description` has been replaced by the `display_name` field -> The following also applies when the Microsoft Graph beta is enabled in version 1.5 or later -The `display_name` field will become read-only as Azure Active Directory no longer respects user-supplied display names for passwords. - The `key_id` field will become read-only as Azure Active Directory no longer allows user-specified key IDs for passwords. This also means that the `azuread_application_password` resource no longer supports importing in version 2.0 of the provider. -The `start_date` field will become read-only as Azure Active Directory no longer respects user-supplied start dates for passwords. Passwords will be valid immediately on creation. - -The `end_date` and `end_date_relative` fields will become read-only as Azure Active Directory no longer respects user-supplied end dates for passwords. Passwords will be valid for a period of 2 years, or whatever period is automatically deemed when creating the password. - The `value` field will become read-only as Azure Active Directory no longer accepts user-supplied password values. Passwords will instead be auto-generated by Azure and will be exported as attributes by the resource. ### Resource: `azuread_group` @@ -200,14 +194,8 @@ The deprecated field `description` has been replaced by the `display_name` field -> The following also applies when the Microsoft Graph beta is enabled in version 1.5 or later -The `display_name` field will become read-only as Azure Active Directory no longer respects user-supplied display names for passwords. - The `key_id` field will become read-only as Azure Active Directory no longer allows user-specified key IDs for passwords. This also means that the `azuread_service_principal_password` resource no longer supports importing in version 2.0 of the provider. -The `start_date` field will become read-only as Azure Active Directory no longer respects user-supplied start dates for passwords. Passwords will be valid immediately on creation. - -The `end_date` and `end_date_relative` fields will become read-only as Azure Active Directory no longer respects user-supplied end dates for passwords. Passwords will be valid for a period of 2 years, or whatever period is automatically deemed when creating the password. - The `value` field will become read-only as Azure Active Directory no longer accepts user-supplied password values. Passwords will instead be auto-generated by Azure and will be exported as attributes by the resource. ### Resource: `azuread_user` diff --git a/docs/resources/application_password.md b/docs/resources/application_password.md index 30eaeffc5..83ef2c4f4 100644 --- a/docs/resources/application_password.md +++ b/docs/resources/application_password.md @@ -22,7 +22,7 @@ resource "azuread_application_password" "example" { ## Argument Reference -~> **IMPORTANT:** In version 2.0 of the provider, or when using the Microsoft Graph beta in version 1.5 or later, the `key_id`, `display_name` / `description`, `start_date`, `end_date` / `end_date_relative` and `value` properties will all become read-only and should not be specified. For more information, see the [Upgrade Guide for v2.0](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/guides/microsoft-graph#resource-azuread_application_password). +~> **IMPORTANT:** In version 2.0 of the provider, or when using the Microsoft Graph beta in version 1.5 or later, the `key_id` and `value` properties will become read-only and should not be specified. For more information, see the [Upgrade Guide for v2.0](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/guides/microsoft-graph#resource-azuread_application_password). The following arguments are supported: diff --git a/docs/resources/service_principal_password.md b/docs/resources/service_principal_password.md index 351760fd1..b0d7abc3a 100644 --- a/docs/resources/service_principal_password.md +++ b/docs/resources/service_principal_password.md @@ -26,7 +26,7 @@ resource "azuread_service_principal_password" "example" { ## Argument Reference -~> **IMPORTANT:** In version 2.0 of the provider, or when using the Microsoft Graph beta in version 1.5 or later, the `key_id`, `display_name` / `description`, `start_date`, `end_date` / `end_date_relative` and `value` properties will all become read-only and should not be specified. For more information, see the [Upgrade Guide for v2.0](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/guides/microsoft-graph#resource-azuread_service_principal_password). +~> **IMPORTANT:** In version 2.0 of the provider, or when using the Microsoft Graph beta in version 1.5 or later, the `key_id` and `value` properties will become read-only and should not be specified. For more information, see the [Upgrade Guide for v2.0](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/guides/microsoft-graph#resource-azuread_service_principal_password). The following arguments are supported: diff --git a/go.mod b/go.mod index 12eb66cbc..ad9dcea2f 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/hashicorp/terraform-plugin-sdk/v2 v2.6.1 github.com/hashicorp/yamux v0.0.0-20210316155119-a95892c5f864 // indirect github.com/klauspost/compress v1.12.2 // indirect - github.com/manicminer/hamilton v0.13.0 + github.com/manicminer/hamilton v0.14.1 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 diff --git a/go.sum b/go.sum index c06918dc5..5356394b5 100644 --- a/go.sum +++ b/go.sum @@ -314,8 +314,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.13.0 h1:aMYcFgHp/+Ph++1MYUaAxsXtrb9ZlN3Lm/mQpVdhUlc= -github.com/manicminer/hamilton v0.13.0/go.mod h1:j/n0It21FsOl/7JJQiJspQT1jw/gpcbnUh/A194j3HU= +github.com/manicminer/hamilton v0.14.1 h1:VbervWkDvX42MifF5gr0qPUPOyWXu3ih3oXt5FvqM6E= +github.com/manicminer/hamilton v0.14.1/go.mod h1:j/n0It21FsOl/7JJQiJspQT1jw/gpcbnUh/A194j3HU= 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= github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= diff --git a/internal/services/applications/application_password_resource_msgraph.go b/internal/services/applications/application_password_resource_msgraph.go index 043d24a70..301d97519 100644 --- a/internal/services/applications/application_password_resource_msgraph.go +++ b/internal/services/applications/application_password_resource_msgraph.go @@ -22,30 +22,10 @@ func applicationPasswordResourceCreateMsGraph(ctx context.Context, d *schema.Res client := meta.(*clients.Client).Applications.MsClient objectId := d.Get("application_object_id").(string) - if val, ok := d.GetOk("description"); ok && val.(string) != "" { - return tf.ErrorDiagPathF(fmt.Errorf("`description` is a read-only field when using Microsoft Graph. Please remove the `description` field from your configuration"), "description", "Creating application password") - } - - if val, ok := d.GetOk("display_name"); ok && val.(string) != "" { - return tf.ErrorDiagPathF(fmt.Errorf("`display_name` is a read-only field when using Microsoft Graph. Please remove the `display_name` field from your configuration"), "display_name", "Creating application password") - } - - if val, ok := d.GetOk("end_date"); ok && val.(string) != "" { - return tf.ErrorDiagPathF(fmt.Errorf("`end_date` is a read-only field when using Microsoft Graph. Please remove the `end_date` field from your configuration"), "end_date", "Creating application password") - } - - if val, ok := d.GetOk("end_date_relative"); ok && val.(string) != "" { - return tf.ErrorDiagPathF(fmt.Errorf("`end_date_relative` is a read-only field when using Microsoft Graph. Please remove the `end_date_relative` field from your configuration"), "end_date_relative", "Creating application password") - } - if val, ok := d.GetOk("key_id"); ok && val.(string) != "" { return tf.ErrorDiagPathF(fmt.Errorf("`key_id` is a read-only field when using Microsoft Graph. Please remove the `key_id` field from your configuration"), "key_id", "Creating application password") } - if val, ok := d.GetOk("start_date"); ok && val.(string) != "" { - return tf.ErrorDiagPathF(fmt.Errorf("`start_date` is a read-only field when using Microsoft Graph. Please remove the `start_date` field from your configuration"), "start_date", "Creating application password") - } - if val, ok := d.GetOk("value"); ok && val.(string) != "" { return tf.ErrorDiagPathF(fmt.Errorf("`value` is a read-only field when using Microsoft Graph. Please remove the `value` field from your configuration"), "value", "Creating application password") } diff --git a/internal/services/applications/application_password_resource_test.go b/internal/services/applications/application_password_resource_test.go index 40afa3e38..32a2474ab 100644 --- a/internal/services/applications/application_password_resource_test.go +++ b/internal/services/applications/application_password_resource_test.go @@ -39,6 +39,45 @@ func TestAccApplicationPassword_basic(t *testing.T) { }) } +func TestAccApplicationPassword_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_application_password", "test") + startDate := time.Now().AddDate(0, 0, 7).UTC().Format(time.RFC3339) + endDate := time.Now().AddDate(0, 5, 27).UTC().Format(time.RFC3339) + r := ApplicationPasswordResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.complete(data, startDate, endDate), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("end_date").Exists(), + check.That(data.ResourceName).Key("key_id").Exists(), + check.That(data.ResourceName).Key("start_date").Exists(), + check.That(data.ResourceName).Key("value").Exists(), + ), + }, + }) +} + +func TestAccApplicationPassword_relativeEndDate(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_application_password", "test") + r := ApplicationPasswordResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.relativeEndDate(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("end_date").Exists(), + check.That(data.ResourceName).Key("end_date_relative").HasValue("8760h"), + check.That(data.ResourceName).Key("key_id").Exists(), + check.That(data.ResourceName).Key("start_date").Exists(), + check.That(data.ResourceName).Key("value").Exists(), + ), + }, + }) +} + func TestAccApplicationPassword_updateDeprecated(t *testing.T) { // TODO: remove this test in v2.0 if v := os.Getenv("AAD_USE_MICROSOFT_GRAPH"); v != "" { @@ -208,6 +247,31 @@ resource "azuread_application_password" "test" { `, r.template(data)) } +func (r ApplicationPasswordResource) complete(data acceptance.TestData, startDate, endDate string) string { + return fmt.Sprintf(` +%[1]s + +resource "azuread_application_password" "test" { + application_object_id = azuread_application.test.object_id + display_name = "terraform-%[2]s" + start_date = "%[3]s" + end_date = "%[4]s" +} +`, r.template(data), data.RandomString, startDate, endDate) +} + +func (r ApplicationPasswordResource) relativeEndDate(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]s + +resource "azuread_application_password" "test" { + application_object_id = azuread_application.test.id + display_name = "terraform-%[2]s" + end_date_relative = "8760h" +} +`, r.template(data), data.RandomString) +} + func (r ApplicationPasswordResource) basicAadGraph(data acceptance.TestData, endDate string) string { // TODO: remove this config in v2.0 return fmt.Sprintf(` diff --git a/internal/services/serviceprincipals/service_principal_password_resource_msgraph.go b/internal/services/serviceprincipals/service_principal_password_resource_msgraph.go index 5ca224f03..abd5bdb84 100644 --- a/internal/services/serviceprincipals/service_principal_password_resource_msgraph.go +++ b/internal/services/serviceprincipals/service_principal_password_resource_msgraph.go @@ -22,30 +22,10 @@ func servicePrincipalPasswordResourceCreateMsGraph(ctx context.Context, d *schem client := meta.(*clients.Client).ServicePrincipals.MsClient objectId := d.Get("service_principal_id").(string) - if val, ok := d.GetOk("description"); ok && val.(string) != "" { - return tf.ErrorDiagPathF(fmt.Errorf("`description` is a read-only field when using Microsoft Graph. Please remove the `description` field from your configuration"), "description", "Creating service principal password") - } - - if val, ok := d.GetOk("display_name"); ok && val.(string) != "" { - return tf.ErrorDiagPathF(fmt.Errorf("`display_name` is a read-only field when using Microsoft Graph. Please remove the `display_name` field from your configuration"), "display_name", "Creating service principal password") - } - - if val, ok := d.GetOk("end_date"); ok && val.(string) != "" { - return tf.ErrorDiagPathF(fmt.Errorf("`end_date` is a read-only field when using Microsoft Graph. Please remove the `end_date` field from your configuration"), "end_date", "Creating service principal password") - } - - if val, ok := d.GetOk("end_date_relative"); ok && val.(string) != "" { - return tf.ErrorDiagPathF(fmt.Errorf("`end_date_relative` is a read-only field when using Microsoft Graph. Please remove the `end_date_relative` field from your configuration"), "end_date_relative", "Creating service principal password") - } - if val, ok := d.GetOk("key_id"); ok && val.(string) != "" { return tf.ErrorDiagPathF(fmt.Errorf("`key_id` is a read-only field when using Microsoft Graph. Please remove the `key_id` field from your configuration"), "key_id", "Creating service principal password") } - if val, ok := d.GetOk("start_date"); ok && val.(string) != "" { - return tf.ErrorDiagPathF(fmt.Errorf("`start_date` is a read-only field when using Microsoft Graph. Please remove the `start_date` field from your configuration"), "start_date", "Creating service principal password") - } - if val, ok := d.GetOk("value"); ok && val.(string) != "" { return tf.ErrorDiagPathF(fmt.Errorf("`value` is a read-only field when using Microsoft Graph. Please remove the `value` field from your configuration"), "value", "Creating service principal password") } diff --git a/internal/services/serviceprincipals/service_principal_password_resource_test.go b/internal/services/serviceprincipals/service_principal_password_resource_test.go index f856adc65..2cff3e16a 100644 --- a/internal/services/serviceprincipals/service_principal_password_resource_test.go +++ b/internal/services/serviceprincipals/service_principal_password_resource_test.go @@ -39,6 +39,45 @@ func TestAccServicePrincipalPassword_basic(t *testing.T) { }) } +func TestAccServicePrincipalPassword_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_service_principal_password", "test") + startDate := time.Now().AddDate(0, 0, 7).UTC().Format(time.RFC3339) + endDate := time.Now().AddDate(0, 5, 27).UTC().Format(time.RFC3339) + r := ServicePrincipalPasswordResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.complete(data, startDate, endDate), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("key_id").Exists(), + check.That(data.ResourceName).Key("start_date").Exists(), + check.That(data.ResourceName).Key("end_date").Exists(), + check.That(data.ResourceName).Key("value").Exists(), + ), + }, + }) +} + +func TestAccServicePrincipalPassword_relativeEndDate(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_service_principal_password", "test") + r := ServicePrincipalPasswordResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.relativeEndDate(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("key_id").Exists(), + check.That(data.ResourceName).Key("start_date").Exists(), + check.That(data.ResourceName).Key("end_date").Exists(), + check.That(data.ResourceName).Key("end_date_relative").HasValue("8760h"), + check.That(data.ResourceName).Key("value").Exists(), + ), + }, + }) +} + func TestAccServicePrincipalPassword_updateDeprecated(t *testing.T) { // TODO: remove this test in v2.0 if v := os.Getenv("AAD_USE_MICROSOFT_GRAPH"); v != "" { @@ -211,6 +250,31 @@ resource "azuread_service_principal_password" "test" { `, r.template(data)) } +func (r ServicePrincipalPasswordResource) complete(data acceptance.TestData, startDate, endDate string) string { + return fmt.Sprintf(` +%[1]s + +resource "azuread_service_principal_password" "test" { + service_principal_id = azuread_service_principal.test.object_id + display_name = "terraform-%[2]s" + start_date = "%[3]s" + end_date = "%[4]s" +} +`, r.template(data), data.RandomString, startDate, endDate) +} + +func (r ServicePrincipalPasswordResource) relativeEndDate(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]s + +resource "azuread_service_principal_password" "test" { + service_principal_id = azuread_service_principal.test.object_id + display_name = "terraform-%[2]s" + end_date_relative = "8760h" +} +`, r.template(data), data.RandomString) +} + func (r ServicePrincipalPasswordResource) basicAadGraph(data acceptance.TestData, endDate string) string { // TODO: remove this config in v2.0 return fmt.Sprintf(` diff --git a/vendor/github.com/manicminer/hamilton/auth/cache.go b/vendor/github.com/manicminer/hamilton/auth/cache.go index 3888cf350..b981dda6b 100644 --- a/vendor/github.com/manicminer/hamilton/auth/cache.go +++ b/vendor/github.com/manicminer/hamilton/auth/cache.go @@ -9,13 +9,17 @@ import ( // cachedAuthorizer caches a token until it expires, then acquires a new token from source type cachedAuthorizer struct { source Authorizer - mutex sync.Mutex + mutex sync.RWMutex token *oauth2.Token } // Token returns the current token if it's still valid, else will acquire a new token func (c *cachedAuthorizer) Token() (*oauth2.Token, error) { - if c.token == nil || !c.token.Valid() { + c.mutex.RLock() + valid := c.token != nil && c.token.Valid() + c.mutex.RUnlock() + + if !valid { c.mutex.Lock() defer c.mutex.Unlock() token, err := c.source.Token() @@ -24,6 +28,7 @@ func (c *cachedAuthorizer) Token() (*oauth2.Token, error) { } c.token = token } + return c.token, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/app_role_assignments.go b/vendor/github.com/manicminer/hamilton/msgraph/app_role_assignments.go index da34cea01..96569d305 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/app_role_assignments.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/app_role_assignments.go @@ -8,24 +8,50 @@ import ( "net/http" ) +type appRoleAssignmentsResourceType string + +const ( + groupsAppRoleAssignmentsResource appRoleAssignmentsResourceType = "groups" + usersAppRoleAssignmentsResource appRoleAssignmentsResourceType = "users" + servicePrincipalsAppRoleAssignmentsResource appRoleAssignmentsResourceType = "servicePrincipals" +) + // AppRoleAssignmentsClient performs operations on AppRoleAssignments. type AppRoleAssignmentsClient struct { - BaseClient Client + BaseClient Client + resourceType appRoleAssignmentsResourceType +} + +// NewUsersAppRoleAssignmentsClient returns a new AppRoleAssignmentsClient for users assignments +func NewUsersAppRoleAssignmentsClient(tenantId string) *AppRoleAssignmentsClient { + return &AppRoleAssignmentsClient{ + BaseClient: NewClient(Version10, tenantId), + resourceType: usersAppRoleAssignmentsResource, + } +} + +// NewGroupsAppRoleAssignmentsClient returns a new AppRoleAssignmentsClient for groups assignments +func NewGroupsAppRoleAssignmentsClient(tenantId string) *AppRoleAssignmentsClient { + return &AppRoleAssignmentsClient{ + BaseClient: NewClient(Version10, tenantId), + resourceType: groupsAppRoleAssignmentsResource, + } } -// NewAppRoleAssignmentsClient returns a new AppRoleAssignmentsClient -func NewAppRoleAssignmentsClient(tenantId string) *AppRoleAssignmentsClient { +// NewServicePrincipalsAppRoleAssignmentsClient returns a new AppRoleAssignmentsClient for service principal assignments +func NewServicePrincipalsAppRoleAssignmentsClient(tenantId string) *AppRoleAssignmentsClient { return &AppRoleAssignmentsClient{ - BaseClient: NewClient(Version10, tenantId), + BaseClient: NewClient(Version10, tenantId), + resourceType: servicePrincipalsAppRoleAssignmentsResource, } } // List returns a list of app role assignments. -func (c *AppRoleAssignmentsClient) List(ctx context.Context, groupId string) (*[]AppRoleAssignment, int, error) { +func (c *AppRoleAssignmentsClient) List(ctx context.Context, id string) (*[]AppRoleAssignment, int, error) { resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ ValidStatusCodes: []int{http.StatusOK}, Uri: Uri{ - Entity: fmt.Sprintf("/groups/%s/appRoleAssignments", groupId), + Entity: fmt.Sprintf("/%s/%s/appRoleAssignments", c.resourceType, id), HasTenantId: true, }, }) @@ -47,11 +73,11 @@ func (c *AppRoleAssignmentsClient) List(ctx context.Context, groupId string) (*[ } // Remove removes a app role assignment. -func (c *AppRoleAssignmentsClient) Remove(ctx context.Context, groupId, appRoleAssignmentId string) (int, error) { +func (c *AppRoleAssignmentsClient) Remove(ctx context.Context, id, appRoleAssignmentId string) (int, error) { _, status, _, err := c.BaseClient.Delete(ctx, DeleteHttpRequestInput{ ValidStatusCodes: []int{http.StatusNoContent}, Uri: Uri{ - Entity: fmt.Sprintf("/groups/%s/appRoleAssignments/%s", groupId, appRoleAssignmentId), + Entity: fmt.Sprintf("/%s/%s/appRoleAssignments/%s", c.resourceType, id, appRoleAssignmentId), HasTenantId: true, }, }) @@ -61,16 +87,16 @@ func (c *AppRoleAssignmentsClient) Remove(ctx context.Context, groupId, appRoleA return status, nil } -// Assign assigns an app role to a group. -func (c *AppRoleAssignmentsClient) Assign(ctx context.Context, groupId, resourceId, appRoleId string) (*AppRoleAssignment, int, error) { +// Assign assigns an app role to a user, group or service principal depending on client resource type. +func (c *AppRoleAssignmentsClient) Assign(ctx context.Context, clientServicePrincipalId, resourceServicePrincipalId, appRoleId string) (*AppRoleAssignment, int, error) { var status int data := struct { PrincipalId string `json:"principalId"` ResourceId string `json:"resourceId"` AppRoleId string `json:"appRoleId"` }{ - PrincipalId: groupId, - ResourceId: resourceId, + PrincipalId: clientServicePrincipalId, + ResourceId: resourceServicePrincipalId, AppRoleId: appRoleId, } @@ -82,7 +108,7 @@ func (c *AppRoleAssignmentsClient) Assign(ctx context.Context, groupId, resource Body: body, ValidStatusCodes: []int{http.StatusCreated}, Uri: Uri{ - Entity: fmt.Sprintf("/groups/%s/appRoleAssignments", groupId), + Entity: fmt.Sprintf("/%s/%s/appRoleAssignments", c.resourceType, clientServicePrincipalId), HasTenantId: true, }, }) diff --git a/vendor/github.com/manicminer/hamilton/msgraph/applications.go b/vendor/github.com/manicminer/hamilton/msgraph/applications.go index 5abcc66c4..405b60956 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/applications.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/applications.go @@ -109,6 +109,31 @@ func (c *ApplicationsClient) Get(ctx context.Context, id string) (*Application, return &application, status, nil } +// GetDeleted retrieves a deleted Application manifest. +// id is the object ID of the application. +func (c *ApplicationsClient) GetDeleted(ctx context.Context, id string) (*Application, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/directory/deletedItems/%s", id), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("ApplicationsClient.BaseClient.Get(): %v", err) + } + defer resp.Body.Close() + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + } + var application Application + if err := json.Unmarshal(respBody, &application); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + return &application, status, nil +} + // Update amends the manifest of an existing Application. func (c *ApplicationsClient) Update(ctx context.Context, application Application) (int, error) { var status int @@ -148,10 +173,42 @@ func (c *ApplicationsClient) Delete(ctx context.Context, id string) (int, error) return status, nil } +// ListDeleted retrieves a list of recently deleted applications, optionally filtered using OData. +func (c *ApplicationsClient) ListDeleted(ctx context.Context, filter string) (*[]Application, int, error) { + params := url.Values{} + if filter != "" { + params.Add("$filter", filter) + } + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: "/directory/deleteditems/microsoft.graph.application", + Params: params, + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, err + } + defer resp.Body.Close() + respBody, _ := ioutil.ReadAll(resp.Body) + var data struct { + DeletedApps []Application `json:"value"` + } + if err = json.Unmarshal(respBody, &data); err != nil { + return nil, status, err + } + return &data.DeletedApps, status, nil +} + // AddPassword appends a new password credential to an Application. func (c *ApplicationsClient) AddPassword(ctx context.Context, applicationId string, passwordCredential PasswordCredential) (*PasswordCredential, int, error) { var status int - body, err := json.Marshal(passwordCredential) + body, err := json.Marshal(struct { + PwdCredential PasswordCredential `json:"passwordCredential"` + }{ + PwdCredential: passwordCredential, + }) if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/groups.go b/vendor/github.com/manicminer/hamilton/msgraph/groups.go index df47c0061..a31f227f8 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/groups.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/groups.go @@ -108,6 +108,30 @@ func (c *GroupsClient) Get(ctx context.Context, id string) (*Group, int, error) return &group, status, nil } +// GetDeleted retrieves a deleted O365 Group. +func (c *GroupsClient) GetDeleted(ctx context.Context, id string) (*Group, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/directory/deletedItems/%s", id), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("GroupsClient.BaseClient.Get(): %v", err) + } + defer resp.Body.Close() + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + } + var group Group + if err := json.Unmarshal(respBody, &group); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + return &group, status, nil +} + // Update amends an existing Group. func (c *GroupsClient) Update(ctx context.Context, group Group) (int, error) { var status int @@ -144,6 +168,35 @@ func (c *GroupsClient) Delete(ctx context.Context, id string) (int, error) { return status, nil } +// ListDeleted retrieves a list of recently deleted O365 groups, optionally filtered using OData. +// TODO: add test coverage once API supports creating O365 groups +func (c *GroupsClient) ListDeleted(ctx context.Context, filter string) (*[]Group, int, error) { + params := url.Values{} + if filter != "" { + params.Add("$filter", filter) + } + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: "/directory/deleteditems/microsoft.graph.group", + Params: params, + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, err + } + defer resp.Body.Close() + respBody, _ := ioutil.ReadAll(resp.Body) + var data struct { + DeletedGroups []Group `json:"value"` + } + if err = json.Unmarshal(respBody, &data); err != nil { + return nil, status, err + } + return &data.DeletedGroups, status, nil +} + // ListMembers retrieves the members of the specified Group. // id is the object ID of the group. func (c *GroupsClient) ListMembers(ctx context.Context, id string) (*[]string, int, error) { @@ -217,6 +270,9 @@ func (c *GroupsClient) AddMembers(ctx context.Context, group *Group) (int, error var status int // Patching group members support up to 20 members per request var memberChunks [][]string + if group.Members == nil || len(*group.Members) == 0 { + return status, fmt.Errorf("no members specified") + } members := *group.Members max := len(members) // Chunk into slices of 20 for batching @@ -238,10 +294,8 @@ func (c *GroupsClient) AddMembers(ctx context.Context, group *Group) (int, error return false } - data := struct { - Members []string `json:"members@odata.bind"` - }{ - Members: members, + data := Group{ + Members: &members, } body, err := json.Marshal(data) if err != nil { @@ -268,6 +322,9 @@ func (c *GroupsClient) AddMembers(ctx context.Context, group *Group) (int, error // memberIds is a *[]string containing object IDs of members to remove. func (c *GroupsClient) RemoveMembers(ctx context.Context, id string, memberIds *[]string) (int, error) { var status int + if memberIds == nil || len(*memberIds) == 0 { + return status, fmt.Errorf("no members specified") + } for _, memberId := range *memberIds { // check for membership before attempting deletion if _, status, err := c.GetMember(ctx, id, memberId); err != nil { @@ -374,6 +431,9 @@ func (c *GroupsClient) GetOwner(ctx context.Context, groupId, ownerId string) (* // First populate the Owners field of the Group using the AppendOwner method of the model, then call this method. func (c *GroupsClient) AddOwners(ctx context.Context, group *Group) (int, error) { var status int + if group.Owners == nil || len(*group.Owners) == 0 { + return status, fmt.Errorf("no owners specified") + } for _, owner := range *group.Owners { // don't fail if an owner already exists checkOwnerAlreadyExists := func(resp *http.Response, o *odata.OData) bool { @@ -415,6 +475,9 @@ func (c *GroupsClient) AddOwners(ctx context.Context, group *Group) (int, error) // ownerIds is a *[]string containing object IDs of owners to remove. func (c *GroupsClient) RemoveOwners(ctx context.Context, id string, ownerIds *[]string) (int, error) { var status int + if ownerIds == nil || len(*ownerIds) == 0 { + return status, fmt.Errorf("no owners specified") + } for _, ownerId := range *ownerIds { // check for ownership before attempting deletion if _, status, err := c.GetOwner(ctx, id, ownerId); err != nil { diff --git a/vendor/github.com/manicminer/hamilton/msgraph/models.go b/vendor/github.com/manicminer/hamilton/msgraph/models.go index 4fd0d06e8..e0bd050f3 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/models.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/models.go @@ -29,31 +29,38 @@ type ApiPreAuthorizedApplication struct { // Application describes an Application object. type Application struct { - ID *string `json:"id,omitempty"` - AddIns *[]AddIn `json:"addIns,omitempty"` - Api *ApplicationApi `json:"api,omitempty"` - AppId *string `json:"appId,omitempty"` - AppRoles *[]AppRole `json:"appRoles,omitempty"` - CreatedDateTime *time.Time `json:"createdDateTime,omitempty"` - DeletedDateTime *time.Time `json:"deletedDateTime,omitempty"` - DisplayName *string `json:"displayName,omitempty"` - GroupMembershipClaims *[]GroupMembershipClaim `json:"groupMembershipClaims,omitempty"` - IdentifierUris *[]string `json:"identifierUris,omitempty"` - Info *InformationalUrl `json:"info,omitempty"` - IsFallbackPublicClient *bool `json:"isFallbackPublicCLient,omitempty"` - KeyCredentials *[]KeyCredential `json:"keyCredentials,omitempty"` - Oauth2RequiredPostResponse *bool `json:"oauth2RequiredPostResponse,omitempty"` - OnPremisesPublishing *OnPremisesPublishing `json:"onPremisePublishing,omitempty"` - OptionalClaims *OptionalClaims `json:"optionalClaims,omitempty"` - ParentalControlSettings *ParentalControlSettings `json:"parentalControlSettings,omitempty"` - PasswordCredentials *[]PasswordCredential `json:"passwordCredentials,omitempty"` - PublicClient *PublicClient `json:"publicClient,omitempty"` - PublisherDomain *string `json:"publisherDomain,omitempty"` - RequiredResourceAccess *[]RequiredResourceAccess `json:"requiredResourceAccess,omitempty"` - SignInAudience SignInAudience `json:"signInAudience,omitempty"` - Tags *[]string `json:"tags,omitempty"` - TokenEncryptionKeyId *string `json:"tokenEncryptionKeyId,omitempty"` - Web *ApplicationWeb `json:"web,omitempty"` + ID *string `json:"id,omitempty"` + AddIns *[]AddIn `json:"addIns,omitempty"` + Api *ApplicationApi `json:"api,omitempty"` + AppId *string `json:"appId,omitempty"` + AppRoles *[]AppRole `json:"appRoles,omitempty"` + CreatedDateTime *time.Time `json:"createdDateTime,omitempty"` + DefaultRedirectUri *string `json:"defaultRedirectUri,omitempty"` + DeletedDateTime *time.Time `json:"deletedDateTime,omitempty"` + DisabledByMicrosoftStatus interface{} `json:"disabledByMicrosoftStatus,omitempty"` + DisplayName *string `json:"displayName,omitempty"` + GroupMembershipClaims *[]GroupMembershipClaim `json:"groupMembershipClaims,omitempty"` + IdentifierUris *[]string `json:"identifierUris,omitempty"` + Info *InformationalUrl `json:"info,omitempty"` + IsAuthorizationServiceEnabled *bool `json:"isAuthorizationServiceEnabled,omitempty"` + IsDeviceOnlyAuthSupported *bool `json:"isDeviceOnlyAuthSupported,omitempty"` + IsFallbackPublicClient *bool `json:"isFallbackPublicClient,omitempty"` + IsManagementRestricted *bool `json:"isManagementRestricted,omitempty"` + KeyCredentials *[]KeyCredential `json:"keyCredentials,omitempty"` + Oauth2RequirePostResponse *bool `json:"oauth2RequirePostResponse,omitempty"` + OnPremisesPublishing *OnPremisesPublishing `json:"onPremisePublishing,omitempty"` + OptionalClaims *OptionalClaims `json:"optionalClaims,omitempty"` + ParentalControlSettings *ParentalControlSettings `json:"parentalControlSettings,omitempty"` + PasswordCredentials *[]PasswordCredential `json:"passwordCredentials,omitempty"` + PublicClient *PublicClient `json:"publicClient,omitempty"` + PublisherDomain *string `json:"publisherDomain,omitempty"` + RequiredResourceAccess *[]RequiredResourceAccess `json:"requiredResourceAccess,omitempty"` + SignInAudience SignInAudience `json:"signInAudience,omitempty"` + Tags *[]string `json:"tags,omitempty"` + TokenEncryptionKeyId *string `json:"tokenEncryptionKeyId,omitempty"` + UniqueName *string `json:"uniqueName,omitempty"` + VerifiedPublisher *VerifiedPublisher `json:"verifiedPublisher,omitempty"` + Web *ApplicationWeb `json:"web,omitempty"` Owners *[]string `json:"owners@odata.bind,omitempty"` } @@ -819,53 +826,62 @@ type SingleSignOnField struct { // User describes a User object. type User struct { - ID *string `json:"id,omitempty"` - AboutMe *string `json:"aboutMe,omitempty"` - AccountEnabled *bool `json:"accountEnabled,omitempty"` - BusinessPhones *[]string `json:"businessPhones,omitempty"` - City *string `json:"city,omitempty"` - CompanyName *string `json:"companyName,omitempty"` - Country *string `json:"country,omitempty"` - CreationType *string `json:"creationType,omitempty"` - Department *string `json:"department,omitempty"` - DisplayName *string `json:"displayName,omitempty"` - EmployeeId *string `json:"employeeId,omitempty"` - ExternalUserState *string `json:"externalUserState,omitempty"` - FaxNumber *string `json:"faxNumber,omitempty"` - GivenName *string `json:"givenName,omitempty"` - ImAddresses *[]string `json:"imAddresses,omitempty"` - Interests *[]string `json:"interests,omitempty"` - JobTitle *string `json:"jobTitle,omitempty"` - Mail *string `json:"mail,omitempty"` - MailNickname *string `json:"mailNickname,omitempty"` - MobilePhone *string `json:"mobilePhone,omitempty"` - MySite *string `json:"mySite,omitempty"` - OfficeLocation *string `json:"officeLocation,omitempty"` - OnPremisesDistinguishedName *string `json:"onPremisesDistinguishedName,omitempty"` - OnPremisesDomainName *string `json:"onPremisesDomainName,omitempty"` - OnPremisesImmutableId *string `json:"onPremisesImmutableId,omitempty"` - OnPremisesSamAccountName *string `json:"onPremisesSamAccountName,omitempty"` - OnPremisesSecurityIdentifier *string `json:"onPremisesSecurityIdentifier,omitempty"` - OnPremisesSyncEnabled *bool `json:"onPremisesSyncEnabled,omitempty"` - OnPremisesUserPrincipalName *string `json:"onPremisesUserPrincipalName,omitempty"` - OtherMails *[]string `json:"otherMails,omitempty"` - PasswordPolicies *string `json:"passwordPolicies,omitempty"` - PastProjects *[]string `json:"pastProjects,omitempty"` - PostalCode *string `json:"postalCode,omitempty"` - PreferredDataLocation *string `json:"preferredDataLocation,omitempty"` - PreferredLanguage *string `json:"preferredLanguage,omitempty"` - PreferredName *string `json:"preferredName,omitempty"` - ProxyAddresses *[]string `json:"proxyAddresses,omitempty"` - Responsibilities *[]string `json:"responsibilities,omitempty"` - Schools *[]string `json:"schools,omitempty"` - ShowInAddressList *bool `json:"showInAddressList,omitempty"` - Skills *[]string `json:"skills,omitempty"` - State *string `json:"state,omitempty"` - StreetAddress *string `json:"streetAddress,omitempty"` - Surname *string `json:"surname,omitempty"` - UsageLocation *string `json:"usageLocation,omitempty"` - UserPrincipalName *string `json:"userPrincipalName,omitempty"` - UserType *string `json:"userType,omitempty"` + ID *string `json:"id,omitempty"` + AboutMe *string `json:"aboutMe,omitempty"` + AccountEnabled *bool `json:"accountEnabled,omitempty"` + BusinessPhones *[]string `json:"businessPhones,omitempty"` + City *string `json:"city,omitempty"` + CompanyName *string `json:"companyName,omitempty"` + Country *string `json:"country,omitempty"` + CreatedDateTime *time.Time `json:"createdDateTime,omitempty"` + CreationType *string `json:"creationType,omitempty"` + DeletedDateTime *time.Time `json:"deletedDateTime,omitempty"` + Department *string `json:"department,omitempty"` + DisplayName *string `json:"displayName,omitempty"` + EmployeeHireDate *time.Time `json:"employeeHireDate,omitempty"` + EmployeeId *string `json:"employeeId,omitempty"` + EmployeeType *string `json:"employeeType,omitempty"` + ExternalUserState *string `json:"externalUserState,omitempty"` + FaxNumber *string `json:"faxNumber,omitempty"` + GivenName *string `json:"givenName,omitempty"` + ImAddresses *[]string `json:"imAddresses,omitempty"` + Interests *[]string `json:"interests,omitempty"` + IsManagementRestricted *bool `json:"isManagementRestricted,omitempty"` + IsResourceAccount *bool `json:"isResourceAccount,omitempty"` + JobTitle *string `json:"jobTitle,omitempty"` + Mail *string `json:"mail,omitempty"` + MailNickname *string `json:"mailNickname,omitempty"` + MobilePhone *string `json:"mobilePhone,omitempty"` + MySite *string `json:"mySite,omitempty"` + OfficeLocation *string `json:"officeLocation,omitempty"` + OnPremisesDistinguishedName *string `json:"onPremisesDistinguishedName,omitempty"` + OnPremisesDomainName *string `json:"onPremisesDomainName,omitempty"` + OnPremisesImmutableId *string `json:"onPremisesImmutableId,omitempty"` + OnPremisesLastSyncDateTime *string `json:"onPremisesLastSyncDateTime,omitempty"` + OnPremisesSamAccountName *string `json:"onPremisesSamAccountName,omitempty"` + OnPremisesSecurityIdentifier *string `json:"onPremisesSecurityIdentifier,omitempty"` + OnPremisesSyncEnabled *bool `json:"onPremisesSyncEnabled,omitempty"` + OnPremisesUserPrincipalName *string `json:"onPremisesUserPrincipalName,omitempty"` + OtherMails *[]string `json:"otherMails,omitempty"` + PasswordPolicies *string `json:"passwordPolicies,omitempty"` + PastProjects *[]string `json:"pastProjects,omitempty"` + PostalCode *string `json:"postalCode,omitempty"` + PreferredDataLocation *string `json:"preferredDataLocation,omitempty"` + PreferredLanguage *string `json:"preferredLanguage,omitempty"` + PreferredName *string `json:"preferredName,omitempty"` + ProxyAddresses *[]string `json:"proxyAddresses,omitempty"` + RefreshTokensValidFromDateTime *time.Time `json:"refreshTokensValidFromDateTime,omitempty"` + Responsibilities *[]string `json:"responsibilities,omitempty"` + Schools *[]string `json:"schools,omitempty"` + ShowInAddressList *bool `json:"showInAddressList,omitempty"` + SignInSessionsValidFromDateTime *time.Time `json:"signInSessionsValidFromDateTime,omitempty"` + Skills *[]string `json:"skills,omitempty"` + State *string `json:"state,omitempty"` + StreetAddress *string `json:"streetAddress,omitempty"` + Surname *string `json:"surname,omitempty"` + UsageLocation *string `json:"usageLocation,omitempty"` + UserPrincipalName *string `json:"userPrincipalName,omitempty"` + UserType *string `json:"userType,omitempty"` PasswordProfile *UserPasswordProfile `json:"passwordProfile,omitempty"` } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/serviceprincipals.go b/vendor/github.com/manicminer/hamilton/msgraph/serviceprincipals.go index 389b1de75..95b4b15c5 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/serviceprincipals.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/serviceprincipals.go @@ -337,7 +337,11 @@ func (c *ServicePrincipalsClient) ListGroupMemberships(ctx context.Context, id s // AddPassword appends a new password credential to a Service Principal. func (c *ServicePrincipalsClient) AddPassword(ctx context.Context, servicePrincipalId string, passwordCredential PasswordCredential) (*PasswordCredential, int, error) { var status int - body, err := json.Marshal(passwordCredential) + body, err := json.Marshal(struct { + PwdCredential PasswordCredential `json:"passwordCredential"` + }{ + PwdCredential: passwordCredential, + }) if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } @@ -419,4 +423,90 @@ func (c *ServicePrincipalsClient) ListOwnedObjects(ctx context.Context, id strin ret[i] = v.Id } return &ret, status, nil -} \ No newline at end of file +} + +// ListAppRoleAssignments retrieves a list of appRoleAssignment that users, groups, or client service principals have been granted for the given resource service principal. +func (c *ServicePrincipalsClient) ListAppRoleAssignments(ctx context.Context, resourceId string) (*[]AppRoleAssignment, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/servicePrincipals/%s/appRoleAssignedTo", resourceId), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Get(): %v", err) + } + defer resp.Body.Close() + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + } + var data struct { + AppRoleAssignments []AppRoleAssignment `json:"value"` + } + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + return &data.AppRoleAssignments, status, nil +} + +// RemoveAppRoleAssignment deletes an appRoleAssignment that a user, group, or client service principal has been granted for a resource service principal. +func (c *ServicePrincipalsClient) RemoveAppRoleAssignment(ctx context.Context, resourceId, appRoleAssignmentId string) (int, error) { + _, status, _, err := c.BaseClient.Delete(ctx, DeleteHttpRequestInput{ + ValidStatusCodes: []int{http.StatusNoContent}, + Uri: Uri{ + Entity: fmt.Sprintf("/servicePrincipals/%s/appRoleAssignedTo/%s", resourceId, appRoleAssignmentId), + HasTenantId: true, + }, + }) + if err != nil { + return status, fmt.Errorf("AppRoleAssignmentsClient.BaseClient.Delete(): %v", err) + } + return status, nil +} + +// AssignAppRoleForResource assigns an app role for a resource service principal, to a user, group, or client service principal. +// To grant an app role assignment, you need three identifiers: +// +// principalId: The id of the user, group or client servicePrincipal to which you are assigning the app role. +// resourceId: The id of the resource servicePrincipal which has defined the app role. +// appRoleId: The id of the appRole (defined on the resource service principal) to assign to a user, group, or service principal. +func (c *ServicePrincipalsClient) AssignAppRoleForResource(ctx context.Context, principalId, resourceId, appRoleId string) (*AppRoleAssignment, int, error) { + var status int + data := struct { + PrincipalId string `json:"principalId"` + ResourceId string `json:"resourceId"` + AppRoleId string `json:"appRoleId"` + }{ + PrincipalId: principalId, + ResourceId: resourceId, + AppRoleId: appRoleId, + } + + body, err := json.Marshal(data) + if err != nil { + return nil, status, fmt.Errorf("json.Marshal(): %v", err) + } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ + Body: body, + ValidStatusCodes: []int{http.StatusCreated}, + Uri: Uri{ + Entity: fmt.Sprintf("/servicePrincipals/%s/appRoleAssignedTo", resourceId), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Post(): %v", err) + } + defer resp.Body.Close() + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + } + var appRoleAssignment AppRoleAssignment + if err := json.Unmarshal(respBody, &appRoleAssignment); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + return &appRoleAssignment, status, nil +} diff --git a/vendor/github.com/manicminer/hamilton/msgraph/users.go b/vendor/github.com/manicminer/hamilton/msgraph/users.go index b125af3b8..6b051a657 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/users.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/users.go @@ -106,6 +106,30 @@ func (c *UsersClient) Get(ctx context.Context, id string) (*User, int, error) { return &user, status, nil } +// GetDeleted retrieves a deleted User. +func (c *UsersClient) GetDeleted(ctx context.Context, id string) (*User, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/directory/deletedItems/%s", id), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("UsersClient.BaseClient.Get(): %v", err) + } + defer resp.Body.Close() + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + } + var user User + if err := json.Unmarshal(respBody, &user); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + return &user, status, nil +} + // Update amends an existing User. func (c *UsersClient) Update(ctx context.Context, user User) (int, error) { var status int @@ -142,6 +166,34 @@ func (c *UsersClient) Delete(ctx context.Context, id string) (int, error) { return status, nil } +// ListDeleted retrieves a list of recently deleted users, optionally filtered using OData. +func (c *UsersClient) ListDeleted(ctx context.Context, filter string) (*[]User, int, error) { + params := url.Values{} + if filter != "" { + params.Add("$filter", filter) + } + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: "/directory/deleteditems/microsoft.graph.user", + Params: params, + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, err + } + defer resp.Body.Close() + respBody, _ := ioutil.ReadAll(resp.Body) + var data struct { + DeletedUsers []User `json:"value"` + } + if err = json.Unmarshal(respBody, &data); err != nil { + return nil, status, err + } + return &data.DeletedUsers, status, nil +} + // ListGroupMemberships returns a list of Groups the user is member of, optionally filtered using OData. func (c *UsersClient) ListGroupMemberships(ctx context.Context, id string, filter string) (*[]Group, int, error) { params := url.Values{} diff --git a/vendor/modules.txt b/vendor/modules.txt index a716e2fc6..5097f91c3 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -222,7 +222,7 @@ github.com/klauspost/compress/fse github.com/klauspost/compress/huff0 github.com/klauspost/compress/zstd github.com/klauspost/compress/zstd/internal/xxhash -# github.com/manicminer/hamilton v0.13.0 +# github.com/manicminer/hamilton v0.14.1 ## explicit github.com/manicminer/hamilton/auth github.com/manicminer/hamilton/environments