diff --git a/examples/okta_app_oauth_role_assignment/basic.tf b/examples/okta_app_oauth_role_assignment/basic.tf new file mode 100644 index 000000000..f5ac31ce3 --- /dev/null +++ b/examples/okta_app_oauth_role_assignment/basic.tf @@ -0,0 +1,12 @@ +resource "okta_app_oauth" "test" { + label = "testAcc_replace_with_uuid" + type = "service" + response_types = ["token"] + grant_types = ["client_credentials"] + jwks_uri = "https://example.com" +} + +resource "okta_app_oauth_role_assignment" "test" { + client_id = okta_app_oauth.test.client_id + type = "HELP_DESK_ADMIN" +} diff --git a/examples/okta_app_oauth_role_assignment/basic_updated.tf b/examples/okta_app_oauth_role_assignment/basic_updated.tf new file mode 100644 index 000000000..c246b6479 --- /dev/null +++ b/examples/okta_app_oauth_role_assignment/basic_updated.tf @@ -0,0 +1,12 @@ +resource "okta_app_oauth" "test" { + label = "testAcc_replace_with_uuid" + type = "service" + response_types = ["token"] + grant_types = ["client_credentials"] + jwks_uri = "https://example.com" +} + +resource "okta_app_oauth_role_assignment" "test" { + client_id = okta_app_oauth.test.client_id + type = "GROUP_MEMBERSHIP_ADMIN" +} diff --git a/examples/okta_app_oauth_role_assignment/custom.tf b/examples/okta_app_oauth_role_assignment/custom.tf new file mode 100644 index 000000000..4f12e0242 --- /dev/null +++ b/examples/okta_app_oauth_role_assignment/custom.tf @@ -0,0 +1,37 @@ +resource "okta_app_oauth" "test" { + label = "testAcc_replace_with_uuid" + type = "service" + response_types = ["token"] + grant_types = ["client_credentials"] + jwks_uri = "https://example.com" +} + +variable "hostname" { + type = string +} + +locals { + org_url = "https://${var.hostname}" +} + +resource "okta_admin_role_custom" "test" { + label = "testAcc_replace_with_uuid" + description = "testing, testing" + permissions = ["okta.apps.assignment.manage", "okta.users.manage", "okta.apps.manage"] +} + +resource "okta_resource_set" "test" { + label = "testAcc_replace_with_uuid" + description = "testing, testing" + resources = [ + format("%s/api/v1/users", local.org_url), + format("%s/api/v1/apps", local.org_url) + ] +} + +resource "okta_app_oauth_role_assignment" "test" { + client_id = okta_app_oauth.test.client_id + type = "CUSTOM" + role = okta_admin_role_custom.test.id + resource_set = okta_resource_set.test.id +} diff --git a/examples/okta_app_oauth_role_assignment/custom_updated.tf b/examples/okta_app_oauth_role_assignment/custom_updated.tf new file mode 100644 index 000000000..ead9a558e --- /dev/null +++ b/examples/okta_app_oauth_role_assignment/custom_updated.tf @@ -0,0 +1,37 @@ +resource "okta_app_oauth" "test" { + label = "testAcc_replace_with_uuid" + type = "service" + response_types = ["token"] + grant_types = ["client_credentials"] + jwks_uri = "https://example.com" +} + +variable "hostname" { + type = string +} + +locals { + org_url = "https://${var.hostname}" +} + +resource "okta_admin_role_custom" "test" { + label = "testAcc_replace_with_uuid" + description = "testing, testing" + permissions = ["okta.apps.assignment.manage", "okta.users.read"] +} + +resource "okta_resource_set" "test" { + label = "testAcc_replace_with_uuid" + description = "testing, testing" + resources = [ + format("%s/api/v1/users", local.org_url), + format("%s/api/v1/apps", local.org_url), + ] +} + +resource "okta_app_oauth_role_assignment" "test" { + client_id = okta_app_oauth.test.client_id + type = "CUSTOM" + role = okta_admin_role_custom.test.id + resource_set = okta_resource_set.test.id +} diff --git a/okta/framework_provider.go b/okta/framework_provider.go index 55604a8b6..3526c1ef3 100644 --- a/okta/framework_provider.go +++ b/okta/framework_provider.go @@ -249,10 +249,11 @@ func (p *FrameworkProvider) DataSources(_ context.Context) []func() datasource.D func (p *FrameworkProvider) Resources(_ context.Context) []func() resource.Resource { return []func() resource.Resource{ NewAppAccessPolicyAssignmentResource, + NewAppOAuthRoleAssignmentResource, NewBrandResource, NewPolicyDeviceAssuranceAndroidResource, - NewPolicyDeviceAssuranceIOSResource, NewPolicyDeviceAssuranceChromeOSResource, + NewPolicyDeviceAssuranceIOSResource, NewPolicyDeviceAssuranceMacOSResource, NewPolicyDeviceAssuranceWindowsResource, } diff --git a/okta/resource_okta_app_oauth_role_assignment.go b/okta/resource_okta_app_oauth_role_assignment.go new file mode 100644 index 000000000..7dc47ac0f --- /dev/null +++ b/okta/resource_okta_app_oauth_role_assignment.go @@ -0,0 +1,217 @@ +package okta + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/okta/terraform-provider-okta/sdk" +) + +var _ resource.ResourceWithValidateConfig = &appOAuthRoleAssignmentResource{} +var _ resource.ResourceWithImportState = &appOAuthRoleAssignmentResource{} + +func NewAppOAuthRoleAssignmentResource() resource.Resource { + return &appOAuthRoleAssignmentResource{} +} + +type appOAuthRoleAssignmentResource struct { + *Config +} + +type OAuthRoleAssignmentResourceModel struct { + ID types.String `tfsdk:"id"` + ClientID types.String `tfsdk:"client_id"` + Type types.String `tfsdk:"type"` + ResourceSet types.String `tfsdk:"resource_set"` + Role types.String `tfsdk:"role"` + Status types.String `tfsdk:"status"` + Label types.String `tfsdk:"label"` +} + +func (r *appOAuthRoleAssignmentResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_app_oauth_role_assignment" +} + +func (r *appOAuthRoleAssignmentResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + config, ok := req.ProviderData.(*Config) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *Config, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + r.Config = config +} + +func (r *appOAuthRoleAssignmentResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Manages assignment of an admin role to an OAuth application", + MarkdownDescription: "Manages assignment of an admin role to an OAuth application", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Role Assignment ID", + MarkdownDescription: "Role Assignment ID", + Computed: true, + }, + "client_id": schema.StringAttribute{ + Description: "Client ID for the role to be assigned to", + MarkdownDescription: "Client ID for the role to be assigned to", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "type": schema.StringAttribute{ + Description: "Role type to assign. This can be one of the standard Okta roles, such as `HELP_DESK_ADMIN`, or `CUSTOM`. Using custom requires the `resource_set` and `role` attributes to be set.", + MarkdownDescription: "Role type to assign. This can be one of the standard Okta roles, such as `HELP_DESK_ADMIN`, or `CUSTOM`. Using custom requires the `resource_set` and `role` attributes to be set.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "resource_set": schema.StringAttribute{ + Description: "Resource set for the custom role to assign, must be the ID of the created resource set.", + MarkdownDescription: "Resource set for the custom role to assign, must be the ID of the created resource set.", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "role": schema.StringAttribute{ + Description: "Custom Role ID", + MarkdownDescription: "Custom Role ID", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "status": schema.StringAttribute{ + Description: "Status of the role assignment", + MarkdownDescription: "Status of the role assignment", + Computed: true, + }, + "label": schema.StringAttribute{ + Description: "Label of the role assignment", + MarkdownDescription: "Label of the role assignment", + Computed: true, + }, + }, + } +} + +func (r *appOAuthRoleAssignmentResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + var data *OAuthRoleAssignmentResourceModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.Type.ValueString() == "CUSTOM" && (data.ResourceSet.IsNull() || data.Role.IsNull()) { + resp.Diagnostics.AddAttributeError( + path.Root("type"), + "Missing attribute configuration", + "When type is set to 'CUSTOM', the resource_set and role attributes must be set.", + ) + } +} + +func (r *appOAuthRoleAssignmentResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data *OAuthRoleAssignmentResourceModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + roleAssignmentRequest := &sdk.ClientRoleAssignment{ + Type: data.Type.ValueString(), + ResourceSet: data.ResourceSet.ValueStringPointer(), + Role: data.Role.ValueStringPointer(), + } + + role, _, err := r.Config.oktaSDKsupplementClient.AssignClientRole(ctx, data.ClientID.ValueString(), roleAssignmentRequest) + if err != nil { + resp.Diagnostics.AddError("Unable to assign role to client", err.Error()) + return + } + + data.ID = types.StringPointerValue(role.Id) + data.Status = types.StringPointerValue(role.Status) + data.Label = types.StringPointerValue(role.Label) + data.Type = types.StringPointerValue(role.Type) + data.ResourceSet = types.StringPointerValue(role.ResourceSet) + data.Role = types.StringPointerValue(role.Role) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *appOAuthRoleAssignmentResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data *OAuthRoleAssignmentResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + role, _, err := r.Config.oktaSDKsupplementClient.GetClientRole(ctx, data.ClientID.ValueString(), data.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to read role assignment", err.Error()) + return + } + + data.ID = types.StringPointerValue(role.Id) + data.Status = types.StringPointerValue(role.Status) + data.Label = types.StringPointerValue(role.Label) + data.Type = types.StringPointerValue(role.Type) + data.ResourceSet = types.StringPointerValue(role.ResourceSet) + data.Role = types.StringPointerValue(role.Role) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *appOAuthRoleAssignmentResource) Update(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError( + "Update not supported", + "OAuth Role Assignments cannot be updated. If you get to this contact the provider maintainers as this should not be hit.", + ) +} + +func (r *appOAuthRoleAssignmentResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data *OAuthRoleAssignmentResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + _, err := r.Config.oktaSDKsupplementClient.UnassignClientRole(ctx, data.ClientID.ValueString(), data.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to delete role assignment", err.Error()) + return + } +} + +func (r *appOAuthRoleAssignmentResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + idParts := strings.Split(req.ID, "/") + + if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + resp.Diagnostics.AddError("Unexpected Import Identifier", "Expected import identifier with format /") + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &OAuthRoleAssignmentResourceModel{ + ClientID: types.StringValue(idParts[0]), + ID: types.StringValue(idParts[1]), + })...) +} diff --git a/okta/resource_okta_app_oauth_role_assignment_test.go b/okta/resource_okta_app_oauth_role_assignment_test.go new file mode 100644 index 000000000..a39bd1e3f --- /dev/null +++ b/okta/resource_okta_app_oauth_role_assignment_test.go @@ -0,0 +1,89 @@ +package okta + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccResourceOktaAppOAuthRoleAssignment_basic(t *testing.T) { + mgr := newFixtureManager("okta_app_oauth_role_assignment", t.Name()) + + oktaResourceTest(t, resource.TestCase{ + PreCheck: testAccPreCheck(t), + ErrorCheck: testAccErrorChecks(t), + ProtoV5ProviderFactories: testAccMergeProvidersFactories, + Steps: []resource.TestStep{ + { + Config: mgr.GetFixtures("basic.tf", t), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("okta_app_oauth_role_assignment.test", "type", "HELP_DESK_ADMIN"), + resource.TestCheckResourceAttr("okta_app_oauth_role_assignment.test", "status", "ACTIVE"), + resource.TestCheckResourceAttrSet("okta_app_oauth_role_assignment.test", "id"), + resource.TestCheckResourceAttrSet("okta_app_oauth_role_assignment.test", "client_id"), + resource.TestCheckResourceAttrSet("okta_app_oauth_role_assignment.test", "label"), + ), + }, + { + ResourceName: "okta_app_oauth_role_assignment.test", + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["okta_app_oauth_role_assignment.test"] + if !ok { + return "", fmt.Errorf("Unable to find resource: %s:", "okta_app_oauth_role_assignment.test") + } + return fmt.Sprintf("%s/%s", r.Primary.Attributes["client_id"], r.Primary.Attributes["id"]), nil + }, + }, + { + Config: mgr.GetFixtures("basic_updated.tf", t), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("okta_app_oauth_role_assignment.test", "type", "GROUP_MEMBERSHIP_ADMIN"), + resource.TestCheckResourceAttr("okta_app_oauth_role_assignment.test", "status", "ACTIVE"), + resource.TestCheckResourceAttrSet("okta_app_oauth_role_assignment.test", "id"), + resource.TestCheckResourceAttrSet("okta_app_oauth_role_assignment.test", "client_id"), + resource.TestCheckResourceAttrSet("okta_app_oauth_role_assignment.test", "label"), + ), + }, + }, + }) +} + +func TestAccResourceOktaAppOAuthRoleAssignment_custom(t *testing.T) { + mgr := newFixtureManager("okta_app_oauth_role_assignment", t.Name()) + + oktaResourceTest(t, resource.TestCase{ + PreCheck: testAccPreCheck(t), + ErrorCheck: testAccErrorChecks(t), + ProtoV5ProviderFactories: testAccMergeProvidersFactories, + Steps: []resource.TestStep{ + { + Config: mgr.GetFixtures("custom.tf", t), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("okta_app_oauth_role_assignment.test", "type", "CUSTOM"), + resource.TestCheckResourceAttr("okta_app_oauth_role_assignment.test", "status", "ACTIVE"), + resource.TestCheckResourceAttrSet("okta_app_oauth_role_assignment.test", "id"), + resource.TestCheckResourceAttrSet("okta_app_oauth_role_assignment.test", "client_id"), + resource.TestCheckResourceAttrSet("okta_app_oauth_role_assignment.test", "label"), + resource.TestCheckResourceAttrSet("okta_app_oauth_role_assignment.test", "role"), + resource.TestCheckResourceAttrSet("okta_app_oauth_role_assignment.test", "resource_set"), + ), + }, + { + Config: mgr.GetFixtures("custom_updated.tf", t), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("okta_app_oauth_role_assignment.test", "type", "CUSTOM"), + resource.TestCheckResourceAttr("okta_app_oauth_role_assignment.test", "status", "ACTIVE"), + resource.TestCheckResourceAttrSet("okta_app_oauth_role_assignment.test", "id"), + resource.TestCheckResourceAttrSet("okta_app_oauth_role_assignment.test", "client_id"), + resource.TestCheckResourceAttrSet("okta_app_oauth_role_assignment.test", "label"), + resource.TestCheckResourceAttrSet("okta_app_oauth_role_assignment.test", "role"), + resource.TestCheckResourceAttrSet("okta_app_oauth_role_assignment.test", "resource_set"), + ), + }, + }, + }) +} diff --git a/sdk/client_role_assignment.go b/sdk/client_role_assignment.go new file mode 100644 index 000000000..edc642877 --- /dev/null +++ b/sdk/client_role_assignment.go @@ -0,0 +1,80 @@ +package sdk + +import ( + "context" + "fmt" + "net/http" + "time" +) + +// Regular roles struct is missing `resource-set` and `role` fields, from CUSTOM role response. +type ClientRole struct { + Embedded interface{} `json:"_embedded,omitempty"` + Links interface{} `json:"_links,omitempty"` + AssignmentType *string `json:"assignmentType,omitempty"` + Created *time.Time `json:"created,omitempty"` + Description *string `json:"description,omitempty"` + Id *string `json:"id,omitempty"` + Label *string `json:"label,omitempty"` + LastUpdated *time.Time `json:"lastUpdated,omitempty"` + Status *string `json:"status,omitempty"` + Type *string `json:"type,omitempty"` + ResourceSet *string `json:"resource-set,omitempty"` + Role *string `json:"role,omitempty"` +} + +func (m *APISupplement) ListClientRoles(ctx context.Context, clientID string) ([]*ClientRole, *Response, error) { + var roles []*ClientRole + + url := fmt.Sprintf("/oauth2/v1/clients/%s/roles", clientID) + re := m.cloneRequestExecutor() + req, err := re.WithAccept("application/json").WithContentType("application/json").NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, nil, err + } + resp, err := re.Do(ctx, req, &roles) + return roles, resp, err +} + +type ClientRoleAssignment struct { + ResourceSet *string `json:"resource-set,omitempty"` + Role *string `json:"role,omitempty"` + Type string `json:"type"` +} + +func (m *APISupplement) AssignClientRole(ctx context.Context, clientID string, assignment *ClientRoleAssignment) (*ClientRole, *Response, error) { + var role *ClientRole + + url := fmt.Sprintf("/oauth2/v1/clients/%s/roles", clientID) + re := m.cloneRequestExecutor() + req, err := re.WithAccept("application/json").WithContentType("application/json").NewRequest(http.MethodPost, url, assignment) + if err != nil { + return nil, nil, err + } + resp, err := re.Do(ctx, req, &role) + return role, resp, err +} + +func (m *APISupplement) GetClientRole(ctx context.Context, clientID, roleID string) (*ClientRole, *Response, error) { + var role *ClientRole + + url := fmt.Sprintf("/oauth2/v1/clients/%s/roles/%s", clientID, roleID) + re := m.cloneRequestExecutor() + req, err := re.WithAccept("application/json").WithContentType("application/json").NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, nil, err + } + resp, err := re.Do(ctx, req, &role) + return role, resp, err +} + +func (m *APISupplement) UnassignClientRole(ctx context.Context, clientID, roleID string) (*Response, error) { + url := fmt.Sprintf("/oauth2/v1/clients/%s/roles/%s", clientID, roleID) + re := m.cloneRequestExecutor() + req, err := re.WithAccept("application/json").WithContentType("application/json").NewRequest(http.MethodDelete, url, nil) + if err != nil { + return nil, err + } + resp, err := re.Do(ctx, req, nil) + return resp, err +} diff --git a/website/docs/r/app_oauth_role_assignment.html.markdown b/website/docs/r/app_oauth_role_assignment.html.markdown new file mode 100644 index 000000000..2938eaccf --- /dev/null +++ b/website/docs/r/app_oauth_role_assignment.html.markdown @@ -0,0 +1,92 @@ +--- +layout: 'okta' +page_title: 'Okta: okta_app_oauth_role_assignment' +sidebar_current: 'docs-okta-resource-okta-app-oauth-role-assignment' +description: |- + Manages assignment of an admin role to an OAuth application +--- + +# okta_app_oauth_role_assignment + +Manages assignment of an admin role to an OAuth application. + +This resource allows you to assign an Okta admin role to a OAuth service application. This requires the Okta tenant feature flag for this function to be enabled. + +## Example Usage + +Standard Role: + +```hcl +resource "okta_app_oauth" "test" { + label = "test" + type = "service" + response_types = ["token"] + grant_types = ["client_credentials"] + jwks_uri = "https://example.com" +} + +resource "okta_app_oauth_role_assignment" "test" { + client_id = okta_app_oauth.test.client_id + type = "HELP_DESK_ADMIN" +} +``` + +Custom Role: + +```hcl +resource "okta_app_oauth" "test" { + label = "test" + type = "service" + response_types = ["token"] + grant_types = ["client_credentials"] + jwks_uri = "https://example.com" +} + +resource "okta_admin_role_custom" "test" { + label = "test" + description = "testing, testing" + permissions = ["okta.apps.assignment.manage", "okta.users.manage", "okta.apps.manage"] +} + +resource "okta_resource_set" "test" { + label = "test" + description = "testing, testing" + resources = [ + format("%s/api/v1/users", "https://example.okta.com"), + format("%s/api/v1/apps", "https://example.okta.com") + ] +} + +resource "okta_app_oauth_role_assignment" "test" { + client_id = okta_app_oauth.test.client_id + type = "CUSTOM" + role = okta_admin_role_custom.test.id + resource_set = okta_resource_set.test.id +} +``` + +## Argument Reference + +The following arguments are supported: + +- `client_id` - (Required) Client ID for the role to be assigned to + +- `type` - (Required) Role type to assign. This can be one of the standard Okta roles, such as `HELP_DESK_ADMIN`, or `CUSTOM`. Using custom requires the `resource_set` and `role` attributes to be set. + +- `resource_set` - (Optional) Resource set for the custom role to assign, must be the ID of the created resource set. + +- `role` - (Optional) Custom Role ID + +## Attribute Reference + +- `id` - Role Assignment ID + +- `status` - Status of the role assignment + +- `label` - Label of the role assignment + +## Import + +OAuth Role assignment can be imported by passing the Client ID and Role Assignment ID for the specific client role. + +`$ terraform import okta_app_oauth_role_assignment.test /`