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 okta_behavior resource and okta_user_security_questions data source #552

Merged
merged 1 commit into from
Jul 29, 2021
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
7 changes: 7 additions & 0 deletions examples/okta_behavior/basic.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
resource "okta_behavior" "test" {
name = "testAcc_replace_with_uuid"
type = "ANOMALOUS_LOCATION"
number_of_authentications = 50
location_granularity_type = "LAT_LONG"
radius_from_location = 20
}
8 changes: 8 additions & 0 deletions examples/okta_behavior/inactive.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
resource "okta_behavior" "test" {
name = "testAcc_replace_with_uuid_updated"
type = "ANOMALOUS_LOCATION"
number_of_authentications = 100
location_granularity_type = "LAT_LONG"
radius_from_location = 5
status = "INACTIVE"
}
7 changes: 7 additions & 0 deletions examples/okta_behavior/updated.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
resource "okta_behavior" "test" {
name = "testAcc_replace_with_uuid_updated"
type = "ANOMALOUS_LOCATION"
number_of_authentications = 100
location_granularity_type = "LAT_LONG"
radius_from_location = 5
}
54 changes: 54 additions & 0 deletions okta/data_source_okta_user_factor_questions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package okta

import (
"context"

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

func dataSourceUserSecurityQuestions() *schema.Resource {
return &schema.Resource{
ReadContext: dataSourceUserSecurityQuestionsQuestionsRead,
Schema: map[string]*schema.Schema{
"user_id": {
Type: schema.TypeString,
Required: true,
Description: "ID of a Okta User",
},
"questions": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"key": {
Type: schema.TypeString,
Computed: true,
},
"text": {
Type: schema.TypeString,
Computed: true,
},
},
},
},
},
}
}

func dataSourceUserSecurityQuestionsQuestionsRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
sq, _, err := getOktaClientFromMetadata(m).UserFactor.ListSupportedSecurityQuestions(ctx, d.Get("user_id").(string))
if err != nil {
return diag.Errorf("failed to list security questions for '%s' user: %v", d.Get("user_id").(string), err)
}
arr := make([]map[string]interface{}, len(sq))
for i := range sq {
arr[i] = map[string]interface{}{
"key": sq[i].Question,
"text": sq[i].QuestionText,
}
}
d.SetId(d.Get("user_id").(string))
_ = d.Set("questions", arr)
return nil
}
9 changes: 6 additions & 3 deletions okta/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,13 @@ const (
templateEmail = "okta_template_email"
templateSms = "okta_template_sms"
trustedOrigin = "okta_trusted_origin"
userAdminRoles = "okta_user_admin_roles"
userBaseSchemaProperty = "okta_user_base_schema_property"
userFactorQuestion = "okta_user_factor_question"
userGroupMemberships = "okta_user_group_memberships"
userSecurityQuestions = "okta_user_security_questions"
userSchemaProperty = "okta_user_schema_property"
userType = "okta_user_type"
userGroupMemberships = "okta_user_group_memberships"
userAdminRoles = "okta_user_admin_roles"
userFactorQuestion = "okta_user_factor_question"
)

// Provider establishes a client connection to an okta site
Expand Down Expand Up @@ -199,6 +200,7 @@ func Provider() *schema.Provider {
authServerPolicy: resourceAuthServerPolicy(),
authServerPolicyRule: resourceAuthServerPolicyRule(),
authServerScope: resourceAuthServerScope(),
behavior: resourceBehavior(),
domain: resourceDomain(),
eventHook: resourceEventHook(),
factor: resourceFactor(),
Expand Down Expand Up @@ -286,6 +288,7 @@ func Provider() *schema.Provider {
authServer: dataSourceAuthServer(),
"okta_auth_server_scopes": dataSourceAuthServerScopes(),
userType: dataSourceUserType(),
userSecurityQuestions: dataSourceUserSecurityQuestions(),
},
ConfigureContextFunc: providerConfigure,
}
Expand Down
216 changes: 216 additions & 0 deletions okta/resource_okta_behavior.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
package okta

import (
"context"
"errors"
"fmt"

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

const (
behaviorAnomalousLocation = "ANOMALOUS_LOCATION"
behaviorAnomalousDevice = "ANOMALOUS_DEVICE"
behaviorAnomalousIP = "ANOMALOUS_IP"
behaviorVelocity = "VELOCITY"
)

func resourceBehavior() *schema.Resource {
return &schema.Resource{
CreateContext: resourceBehaviorCreate,
ReadContext: resourceBehaviorRead,
UpdateContext: resourceBehaviorUpdate,
DeleteContext: resourceBehaviorDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
Description: "Name of the behavior",
},
"type": {
Type: schema.TypeString,
Required: true,
Description: "Behavior type",
ForceNew: true,
ValidateDiagFunc: elemInSlice([]string{behaviorAnomalousLocation, behaviorAnomalousDevice, behaviorAnomalousIP, behaviorVelocity}),
},
"status": {
Type: schema.TypeString,
Optional: true,
Default: statusActive,
ValidateDiagFunc: elemInSlice([]string{statusActive, statusInactive}),
Description: "Behavior status: ACTIVE or INACTIVE.",
},
"location_granularity_type": {
Type: schema.TypeString,
Optional: true,
Description: "Determines the method and level of detail used to evaluate the behavior.",
ValidateDiagFunc: elemInSlice([]string{"LAT_LONG", "CITY", "COUNTRY", "SUBDIVISION"}),
},
"radius_from_location": {
Type: schema.TypeInt,
Optional: true,
Description: "Radius from location (in kilometers)",
ValidateDiagFunc: intAtLeast(5),
},
"number_of_authentications": {
Type: schema.TypeInt,
Optional: true,
Description: "The number of recent authentications used to evaluate the behavior.",
},
"velocity": {
Type: schema.TypeInt,
Optional: true,
Description: "Velocity (in kilometers per hour).",
ValidateDiagFunc: intAtLeast(1),
ConflictsWith: []string{"number_of_authentications", "radius_from_location", "location_granularity_type"},
},
},
}
}

func resourceBehaviorCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
logger(m).Info("creating location behavior", "name", d.Get("name").(string))
err := validateBehavior(d)
if err != nil {
return diag.FromErr(err)
}
behavior, _, err := getSupplementFromMetadata(m).CreateBehavior(ctx, buildBehavior(d))
if err != nil {
return diag.Errorf("failed to create location behavior: %v", err)
}
d.SetId(behavior.ID)
return resourceBehaviorRead(ctx, d, m)
}

func resourceBehaviorRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
logger(m).Info("getting behavior", "id", d.Id())
behavior, resp, err := getSupplementFromMetadata(m).GetBehavior(ctx, d.Id())
if err := suppressErrorOn404(resp, err); err != nil {
return diag.Errorf("failed to find behavior: %v", err)
}
if behavior == nil {
d.SetId("")
return nil
}
_ = d.Set("name", behavior.Name)
_ = d.Set("type", behavior.Type)
_ = d.Set("status", behavior.Status)
setSettings(d, behavior.Type, behavior.Settings)
return nil
}

func resourceBehaviorUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
logger(m).Info("updating location behavior", "name", d.Get("name").(string))
err := validateBehavior(d)
if err != nil {
return diag.FromErr(err)
}
_, _, err = getSupplementFromMetadata(m).UpdateBehavior(ctx, d.Id(), buildBehavior(d))
if err != nil {
return diag.Errorf("failed to update location behavior: %v", err)
}
if d.HasChange("status") {
err := handleBehaviorLifecycle(ctx, d, m)
if err != nil {
return err
}
}
return resourceBehaviorRead(ctx, d, m)
}

func resourceBehaviorDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
logger(m).Info("deleting location behavior", "name", d.Get("name").(string))
_, err := getSupplementFromMetadata(m).DeleteBehavior(ctx, d.Id())
if err != nil {
return diag.Errorf("failed to delete location behavior: %v", err)
}
return nil
}

func buildBehavior(d *schema.ResourceData) sdk.Behavior {
b := sdk.Behavior{
Name: d.Get("name").(string),
Status: d.Get("status").(string),
Settings: make(map[string]interface{}),
Type: d.Get("type").(string),
}
if b.Type == behaviorAnomalousLocation || b.Type == behaviorAnomalousDevice || b.Type == behaviorAnomalousIP {
b.Settings["maxEventsUsedForEvaluation"] = d.Get("number_of_authentications")
}
if b.Type == behaviorAnomalousLocation {
b.Settings["granularity"] = d.Get("location_granularity_type")
if d.Get("location_granularity_type").(string) == "LAT_LONG" {
b.Settings["radiusKilometers"] = d.Get("radius_from_location")
}
}
if b.Type == behaviorVelocity {
b.Settings["velocityKph"] = d.Get("velocity")
}
return b
}

func handleBehaviorLifecycle(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
client := getSupplementFromMetadata(m)
if d.Get("status").(string) == statusActive {
logger(m).Info("activating behavior", "name", d.Get("name").(string))
_, err := client.ActivateBehavior(ctx, d.Id())
if err != nil {
return diag.Errorf("failed to activate behavior: %v", err)
}
return nil
}
logger(m).Info("deactivating behavior", "name", d.Get("name").(string))
_, err := client.DeactivateBehavior(ctx, d.Id())
if err != nil {
return diag.Errorf("failed to deactivate behavior: %v", err)
}
return nil
}

func setSettings(d *schema.ResourceData, typ string, settings map[string]interface{}) {
if typ == behaviorAnomalousLocation || typ == behaviorAnomalousDevice || typ == behaviorAnomalousIP {
_ = d.Set("number_of_authentications", settings["maxEventsUsedForEvaluation"])
}
if typ == behaviorAnomalousLocation {
_ = d.Set("location_granularity_type", settings["granularity"])
if settings["granularity"].(string) == "LAT_LONG" {
_ = d.Set("radius_from_location", settings["radiusKilometers"])
}
}
if typ == behaviorVelocity {
_ = d.Set("velocity", settings["velocityKph"])
}
}

func validateBehavior(d *schema.ResourceData) error {
typ := d.Get("type").(string)
if typ == behaviorAnomalousLocation || typ == behaviorAnomalousDevice || typ == behaviorAnomalousIP {
_, ok := d.GetOk("number_of_authentications")
if !ok {
return fmt.Errorf("'number_of_authentications' should be set for '%s', '%s' and '%s' behavior types", behaviorAnomalousLocation, behaviorAnomalousDevice, behaviorAnomalousDevice)
}
}
if typ == behaviorAnomalousLocation {
lgt, ok := d.GetOk("location_granularity_type")
if !ok {
return fmt.Errorf("'location_granularity_type' should be provided for '%s' behavior type", behaviorAnomalousLocation)
}
_, ok = d.GetOk("radius_from_location")
if lgt.(string) == "LAT_LONG" && !ok {
return errors.New("'radius_from_location' should be set if location_granularity_type='LAT_LONG'")
}
}
if typ == behaviorVelocity {
_, ok := d.GetOk("velocity")
if !ok {
return fmt.Errorf("'velocity' should be set for '%s' behavior type", behaviorVelocity)
}
}
return nil
}
62 changes: 62 additions & 0 deletions okta/resource_okta_behavior_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package okta

import (
"context"
"fmt"
"testing"

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

func TestAccOktaBehavior(t *testing.T) {
ri := acctest.RandInt()
mgr := newFixtureManager(behavior)
config := mgr.GetFixtures("basic.tf", ri, t)
updated := mgr.GetFixtures("updated.tf", ri, t)
inactive := mgr.GetFixtures("inactive.tf", ri, t)
resourceName := fmt.Sprintf("%s.test", behavior)
resource.Test(
t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: testAccProvidersFactories,
CheckDestroy: createCheckResourceDestroy(behavior, doesBehaviorExist),
Steps: []resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "name", buildResourceName(ri)),
resource.TestCheckResourceAttr(resourceName, "number_of_authentications", "50"),
resource.TestCheckResourceAttr(resourceName, "location_granularity_type", "LAT_LONG"),
resource.TestCheckResourceAttr(resourceName, "radius_from_location", "20"),
resource.TestCheckResourceAttr(resourceName, "status", "ACTIVE"),
),
},
{
Config: updated,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "name", buildResourceName(ri)+"_updated"),
resource.TestCheckResourceAttr(resourceName, "number_of_authentications", "100"),
resource.TestCheckResourceAttr(resourceName, "location_granularity_type", "LAT_LONG"),
resource.TestCheckResourceAttr(resourceName, "radius_from_location", "5"),
resource.TestCheckResourceAttr(resourceName, "status", "ACTIVE"),
),
},
{
Config: inactive,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "name", buildResourceName(ri)+"_updated"),
resource.TestCheckResourceAttr(resourceName, "number_of_authentications", "100"),
resource.TestCheckResourceAttr(resourceName, "location_granularity_type", "LAT_LONG"),
resource.TestCheckResourceAttr(resourceName, "radius_from_location", "5"),
resource.TestCheckResourceAttr(resourceName, "status", "INACTIVE"),
),
},
},
})
}

func doesBehaviorExist(id string) (bool, error) {
_, response, err := getSupplementFromMetadata(testAccProvider.Meta()).GetBehavior(context.Background(), id)
return doesResourceExist(response, err)
}
Loading